FINERACT-2141: Delayed settlement implementation for fineract loan investor module
This commit is contained in:
committed by
Arnold Gálovics
parent
23887bdf1c
commit
33e9f7f3db
+4
@@ -147,6 +147,10 @@ public class JournalEntry extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return JournalEntryType.DEBIT.getValue().equals(this.type);
|
return JournalEntryType.DEBIT.getValue().equals(this.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCreditEntry() {
|
||||||
|
return JournalEntryType.CREDIT.getValue().equals(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
public void setReversalJournalEntry(final JournalEntry reversalJournalEntry) {
|
public void setReversalJournalEntry(final JournalEntry reversalJournalEntry) {
|
||||||
this.reversalJournalEntry = reversalJournalEntry;
|
this.reversalJournalEntry = reversalJournalEntry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,14 @@
|
|||||||
"string"
|
"string"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": null,
|
||||||
|
"name": "transferExternalGroupId",
|
||||||
|
"type": [
|
||||||
|
"null",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"default": null,
|
"default": null,
|
||||||
"name": "submittedDate",
|
"name": "submittedDate",
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ compileJava.doLast {
|
|||||||
javaexec {
|
javaexec {
|
||||||
description = 'Performs EclipseLink static weaving of entity classes'
|
description = 'Performs EclipseLink static weaving of entity classes'
|
||||||
def target = source
|
def target = source
|
||||||
mainClass = 'org.eclipse.persistence.tools.weaving.jpa.StaticWeave'
|
main = 'org.eclipse.persistence.tools.weaving.jpa.StaticWeave'
|
||||||
args '-persistenceinfo', source, '-classpath', sourceSets.main.runtimeClasspath, source, target
|
args '-persistenceinfo', source, source, target
|
||||||
classpath sourceSets.main.runtimeClasspath
|
classpath sourceSets.main.runtimeClasspath
|
||||||
}
|
}
|
||||||
delete {
|
delete {
|
||||||
|
|||||||
+8
@@ -3661,6 +3661,14 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder intermediarySaleLoanToExternalAssetOwner(final Long loanId) {
|
||||||
|
this.actionName = "INTERMEDIARYSALE";
|
||||||
|
this.entityName = "LOAN";
|
||||||
|
this.loanId = loanId;
|
||||||
|
this.href = "/external-asset-owners/transfers/loans/" + loanId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder saleLoanToExternalAssetOwner(final Long loanId) {
|
public CommandWrapperBuilder saleLoanToExternalAssetOwner(final Long loanId) {
|
||||||
this.actionName = "SALE";
|
this.actionName = "SALE";
|
||||||
this.entityName = "LOAN";
|
this.entityName = "LOAN";
|
||||||
|
|||||||
-2
@@ -43,9 +43,7 @@ public class CommandProcessingResultBuilder {
|
|||||||
private Long productId;
|
private Long productId;
|
||||||
private boolean rollbackTransaction = false;
|
private boolean rollbackTransaction = false;
|
||||||
private ExternalId entityExternalId = ExternalId.empty();
|
private ExternalId entityExternalId = ExternalId.empty();
|
||||||
|
|
||||||
private ExternalId subEntityExternalId = ExternalId.empty();
|
private ExternalId subEntityExternalId = ExternalId.empty();
|
||||||
|
|
||||||
private ExternalId loanExternalId = ExternalId.empty();
|
private ExternalId loanExternalId = ExternalId.empty();
|
||||||
|
|
||||||
public CommandProcessingResult build() {
|
public CommandProcessingResult build() {
|
||||||
|
|||||||
+12
-7
@@ -20,13 +20,18 @@ package org.apache.fineract.test.data;
|
|||||||
|
|
||||||
public enum AssetExternalizationErrorMessage {
|
public enum AssetExternalizationErrorMessage {
|
||||||
|
|
||||||
LOAN_NOT_ACTIVE("Loan is not in active status"), ASSET_OWNED_CANNOT_BE_SOLD(
|
LOAN_NOT_ACTIVE("Loan is not in active status"), //
|
||||||
"This loan cannot be sold, because it is owned by an external asset owner"), ASSET_NOT_OWNED_CANNOT_BE_BOUGHT(
|
ASSET_OWNED_CANNOT_BE_SOLD("This loan cannot be sold, because it is owned by an external asset owner"), //
|
||||||
"This loan cannot be bought back, it is not owned by an external asset owner"), BUYBACK_ALREADY_IN_PROGRESS_CANNOT_BE_BOUGHT(
|
ASSET_NOT_OWNED_CANNOT_BE_BOUGHT("This loan cannot be bought back, it is not owned by an external asset owner"), //
|
||||||
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"), ALREADY_PENDING(
|
BUYBACK_ALREADY_IN_PROGRESS_CANNOT_BE_BOUGHT(
|
||||||
"External asset owner transfer is already in PENDING state for this loan"), SETTLEMENT_DATE_IN_THE_PAST(
|
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"), //
|
||||||
"Settlement date cannot be in the past"), INVALID_REQUEST(
|
ALREADY_PENDING("External asset owner transfer is already in PENDING state for this loan"), //
|
||||||
"The request was invalid. This typically will happen due to validation errors which are provided.");
|
SETTLEMENT_DATE_IN_THE_PAST("Settlement date cannot be in the past"), //
|
||||||
|
LOAN_OVERPAID_INVALID("Loan status OVERPAID is not valid for transfer."), //
|
||||||
|
LOAN_CLOSED_OBLIGATIONS_MET_INVALID("Loan status CLOSED_OBLIGATIONS_MET is not valid for transfer."), //
|
||||||
|
LOAN_SUBMITTED_AND_PENDING_APPROVAL_INVALID("Loan status SUBMITTED_AND_PENDING_APPROVAL is not valid for transfer."), //
|
||||||
|
LOAN_APPROVED_INVALID("Loan status APPROVED is not valid for transfer."), //
|
||||||
|
INVALID_REQUEST("The request was invalid. This typically will happen due to validation errors which are provided."); //
|
||||||
|
|
||||||
public final String value;
|
public final String value;
|
||||||
|
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ Feature: Asset Externalization
|
|||||||
When Admin sets the business date to "10 May 2023"
|
When Admin sets the business date to "10 May 2023"
|
||||||
And Customer makes "AUTOPAY" repayment on "10 May 2023" with 1000 EUR transaction amount
|
And Customer makes "AUTOPAY" repayment on "10 May 2023" with 1000 EUR transaction amount
|
||||||
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
|
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
|
||||||
Then Asset externalization transaction with the following data results a 403 error and "LOAN_NOT_ACTIVE" error message
|
Then Asset externalization transaction with the following data results a 403 error and "LOAN_CLOSED_OBLIGATIONS_MET_INVALID" error message
|
||||||
| Transaction type | settlementDate | purchasePriceRatio |
|
| Transaction type | settlementDate | purchasePriceRatio |
|
||||||
| sale | 2023-05-21 | 1 |
|
| sale | 2023-05-21 | 1 |
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ Feature: Asset Externalization
|
|||||||
When Admin sets the business date to "10 May 2023"
|
When Admin sets the business date to "10 May 2023"
|
||||||
And Customer makes "AUTOPAY" repayment on "10 May 2023" with 1200 EUR transaction amount
|
And Customer makes "AUTOPAY" repayment on "10 May 2023" with 1200 EUR transaction amount
|
||||||
Then Loan status will be "OVERPAID"
|
Then Loan status will be "OVERPAID"
|
||||||
Then Asset externalization transaction with the following data results a 403 error and "LOAN_NOT_ACTIVE" error message
|
Then Asset externalization transaction with the following data results a 403 error and "LOAN_OVERPAID_INVALID" error message
|
||||||
| Transaction type | settlementDate | purchasePriceRatio |
|
| Transaction type | settlementDate | purchasePriceRatio |
|
||||||
| sale | 2023-05-21 | 1 |
|
| sale | 2023-05-21 | 1 |
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ Feature: Asset Externalization
|
|||||||
When Admin creates a client with random data
|
When Admin creates a client with random data
|
||||||
When Admin creates a new default Loan with date: "1 May 2023"
|
When Admin creates a new default Loan with date: "1 May 2023"
|
||||||
Then Loan status will be "SUBMITTED_AND_PENDING_APPROVAL"
|
Then Loan status will be "SUBMITTED_AND_PENDING_APPROVAL"
|
||||||
Then Asset externalization transaction with the following data results a 403 error and "LOAN_NOT_ACTIVE" error message
|
Then Asset externalization transaction with the following data results a 403 error and "LOAN_SUBMITTED_AND_PENDING_APPROVAL_INVALID" error message
|
||||||
| Transaction type | settlementDate | purchasePriceRatio |
|
| Transaction type | settlementDate | purchasePriceRatio |
|
||||||
| sale | 2023-05-30 | 1 |
|
| sale | 2023-05-30 | 1 |
|
||||||
|
|
||||||
@@ -407,7 +407,7 @@ Feature: Asset Externalization
|
|||||||
When Admin creates a new default Loan with date: "1 May 2023"
|
When Admin creates a new default Loan with date: "1 May 2023"
|
||||||
And Admin successfully approves the loan on "1 May 2023" with "1000" amount and expected disbursement date on "1 May 2023"
|
And Admin successfully approves the loan on "1 May 2023" with "1000" amount and expected disbursement date on "1 May 2023"
|
||||||
Then Loan status will be "APPROVED"
|
Then Loan status will be "APPROVED"
|
||||||
Then Asset externalization transaction with the following data results a 403 error and "LOAN_NOT_ACTIVE" error message
|
Then Asset externalization transaction with the following data results a 403 error and "LOAN_APPROVED_INVALID" error message
|
||||||
| Transaction type | settlementDate | purchasePriceRatio |
|
| Transaction type | settlementDate | purchasePriceRatio |
|
||||||
| sale | 2023-05-30 | 1 |
|
| sale | 2023-05-30 | 1 |
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -220,7 +220,9 @@ public class ExternalAssetOwnersApiResource {
|
|||||||
private String getResult(Long loanId, String apiRequestBodyAsJson, String commandParam) {
|
private String getResult(Long loanId, String apiRequestBodyAsJson, String commandParam) {
|
||||||
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
||||||
CommandWrapper commandRequest = null;
|
CommandWrapper commandRequest = null;
|
||||||
if (CommandParameterUtil.is(commandParam, "sale")) {
|
if (CommandParameterUtil.is(commandParam, "intermediarySale")) {
|
||||||
|
commandRequest = builder.intermediarySaleLoanToExternalAssetOwner(loanId).build();
|
||||||
|
} else if (CommandParameterUtil.is(commandParam, "sale")) {
|
||||||
commandRequest = builder.saleLoanToExternalAssetOwner(loanId).build();
|
commandRequest = builder.saleLoanToExternalAssetOwner(loanId).build();
|
||||||
} else if (CommandParameterUtil.is(commandParam, "buyback")) {
|
} else if (CommandParameterUtil.is(commandParam, "buyback")) {
|
||||||
commandRequest = builder.buybackLoanToExternalAssetOwner(loanId).build();
|
commandRequest = builder.buybackLoanToExternalAssetOwner(loanId).build();
|
||||||
|
|||||||
+6
@@ -61,6 +61,9 @@ final class ExternalAssetOwnersApiResourceSwagger {
|
|||||||
@Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e")
|
@Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e")
|
||||||
public String transferExternalId;
|
public String transferExternalId;
|
||||||
|
|
||||||
|
@Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e")
|
||||||
|
public String transferExternalGroupId;
|
||||||
|
|
||||||
@Schema(example = "1")
|
@Schema(example = "1")
|
||||||
public String purchasePriceRatio;
|
public String purchasePriceRatio;
|
||||||
|
|
||||||
@@ -101,6 +104,9 @@ final class ExternalAssetOwnersApiResourceSwagger {
|
|||||||
@Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e")
|
@Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e")
|
||||||
public String transferExternalId;
|
public String transferExternalId;
|
||||||
|
|
||||||
|
@Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e")
|
||||||
|
public String transferExternalGroupId;
|
||||||
|
|
||||||
@Schema(example = "1.2345678")
|
@Schema(example = "1.2345678")
|
||||||
public String purchasePriceRatio;
|
public String purchasePriceRatio;
|
||||||
|
|
||||||
|
|||||||
+112
-36
@@ -23,16 +23,17 @@ import java.time.LocalDate;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.cob.loan.LoanCOBBusinessStep;
|
import org.apache.fineract.cob.loan.LoanCOBBusinessStep;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition;
|
import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition;
|
||||||
import org.apache.fineract.investor.data.ExternalTransferStatus;
|
import org.apache.fineract.investor.data.ExternalTransferStatus;
|
||||||
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
|
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwner;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferDetails;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferDetails;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
|
||||||
@@ -40,6 +41,8 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping
|
|||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
|
||||||
import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
|
import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
|
||||||
import org.apache.fineract.investor.service.AccountingService;
|
import org.apache.fineract.investor.service.AccountingService;
|
||||||
|
import org.apache.fineract.investor.service.DelayedSettlementAttributeService;
|
||||||
|
import org.apache.fineract.investor.service.LoanTransferabilityService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.springframework.context.annotation.Conditional;
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
@@ -52,10 +55,16 @@ import org.springframework.stereotype.Component;
|
|||||||
public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep {
|
public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep {
|
||||||
|
|
||||||
public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
|
public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
|
||||||
|
public static final List<ExternalTransferStatus> PENDING_STATUSES = List.of(ExternalTransferStatus.PENDING_INTERMEDIATE,
|
||||||
|
ExternalTransferStatus.PENDING);
|
||||||
|
public static final List<ExternalTransferStatus> BUYBACK_STATUSES = List.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE,
|
||||||
|
ExternalTransferStatus.BUYBACK);
|
||||||
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
||||||
private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
|
private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
|
||||||
private final AccountingService accountingService;
|
private final AccountingService accountingService;
|
||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
|
private final LoanTransferabilityService loanTransferabilityService;
|
||||||
|
private final DelayedSettlementAttributeService delayedSettlementAttributeService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Loan execute(Loan loan) {
|
public Loan execute(Loan loan) {
|
||||||
@@ -66,7 +75,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
|||||||
List<ExternalAssetOwnerTransfer> transferDataList = externalAssetOwnerTransferRepository.findAll(
|
List<ExternalAssetOwnerTransfer> transferDataList = externalAssetOwnerTransferRepository.findAll(
|
||||||
(root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loanId),
|
(root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loanId),
|
||||||
criteriaBuilder.equal(root.get("settlementDate"), settlementDate),
|
criteriaBuilder.equal(root.get("settlementDate"), settlementDate),
|
||||||
root.get("status").in(List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK)),
|
root.get("status").in(Stream.concat(PENDING_STATUSES.stream(), BUYBACK_STATUSES.stream()).toList()),
|
||||||
criteriaBuilder.greaterThanOrEqualTo(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)),
|
criteriaBuilder.greaterThanOrEqualTo(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)),
|
||||||
Sort.by(Sort.Direction.ASC, "id"));
|
Sort.by(Sort.Direction.ASC, "id"));
|
||||||
int size = transferDataList.size();
|
int size = transferDataList.size();
|
||||||
@@ -75,6 +84,11 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
|||||||
ExternalTransferStatus firstTransferStatus = transferDataList.get(0).getStatus();
|
ExternalTransferStatus firstTransferStatus = transferDataList.get(0).getStatus();
|
||||||
ExternalTransferStatus secondTransferStatus = transferDataList.get(1).getStatus();
|
ExternalTransferStatus secondTransferStatus = transferDataList.get(1).getStatus();
|
||||||
|
|
||||||
|
if (delayedSettlementAttributeService.isEnabled(loan.getLoanProduct().getId())) {
|
||||||
|
throw new IllegalStateException(String.format("Delayed Settlement enabled, but found 2 transfers of statuses: %s and %s",
|
||||||
|
firstTransferStatus, secondTransferStatus));
|
||||||
|
}
|
||||||
|
|
||||||
if (!ExternalTransferStatus.PENDING.equals(firstTransferStatus)
|
if (!ExternalTransferStatus.PENDING.equals(firstTransferStatus)
|
||||||
|| !ExternalTransferStatus.BUYBACK.equals(secondTransferStatus)) {
|
|| !ExternalTransferStatus.BUYBACK.equals(secondTransferStatus)) {
|
||||||
throw new IllegalStateException(String.format("Illegal transfer found. Expected %s and %s, found: %s and %s",
|
throw new IllegalStateException(String.format("Illegal transfer found. Expected %s and %s, found: %s and %s",
|
||||||
@@ -83,9 +97,9 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
|||||||
handleSameDaySaleAndBuyback(settlementDate, transferDataList, loan);
|
handleSameDaySaleAndBuyback(settlementDate, transferDataList, loan);
|
||||||
} else if (size == 1) {
|
} else if (size == 1) {
|
||||||
ExternalAssetOwnerTransfer transfer = transferDataList.get(0);
|
ExternalAssetOwnerTransfer transfer = transferDataList.get(0);
|
||||||
if (ExternalTransferStatus.PENDING.equals(transfer.getStatus())) {
|
if (PENDING_STATUSES.contains(transfer.getStatus())) {
|
||||||
handleSale(loan, settlementDate, transfer);
|
handleSale(loan, settlementDate, transfer);
|
||||||
} else if (ExternalTransferStatus.BUYBACK.equals(transfer.getStatus())) {
|
} else if (BUYBACK_STATUSES.contains(transfer.getStatus())) {
|
||||||
handleBuyback(loan, settlementDate, transfer);
|
handleBuyback(loan, settlementDate, transfer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,7 +109,8 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleSale(final Loan loan, final LocalDate settlementDate, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
private void handleSale(final Loan loan, final LocalDate settlementDate, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = sellAsset(loan, settlementDate, externalAssetOwnerTransfer);
|
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = sellAssetOrDecline(loan, settlementDate, externalAssetOwnerTransfer);
|
||||||
|
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(newExternalAssetOwnerTransfer, loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(newExternalAssetOwnerTransfer, loan));
|
||||||
if (!ExternalTransferStatus.DECLINED.equals(newExternalAssetOwnerTransfer.getStatus())) {
|
if (!ExternalTransferStatus.DECLINED.equals(newExternalAssetOwnerTransfer.getStatus())) {
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountSnapshotBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountSnapshotBusinessEvent(loan));
|
||||||
@@ -104,14 +119,16 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
|||||||
|
|
||||||
private void handleBuyback(final Loan loan, final LocalDate settlementDate,
|
private void handleBuyback(final Loan loan, final LocalDate settlementDate,
|
||||||
final ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer) {
|
final ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer) {
|
||||||
|
final ExternalTransferStatus expectedActiveStatus = determineExpectedActiveStatus(buybackExternalAssetOwnerTransfer);
|
||||||
|
|
||||||
Optional<ExternalAssetOwnerTransfer> optActiveExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository
|
Optional<ExternalAssetOwnerTransfer> optActiveExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository
|
||||||
.findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
|
.findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
|
||||||
criteriaBuilder.equal(root.get("owner"), buybackExternalAssetOwnerTransfer.getOwner()),
|
criteriaBuilder.equal(root.get("owner"), buybackExternalAssetOwnerTransfer.getOwner()),
|
||||||
criteriaBuilder.equal(root.get("status"), ExternalTransferStatus.ACTIVE),
|
criteriaBuilder.equal(root.get("status"), expectedActiveStatus),
|
||||||
criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)));
|
criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)));
|
||||||
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer;
|
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer;
|
||||||
if (!optActiveExternalAssetOwnerTransfer.isPresent()) {
|
if (!optActiveExternalAssetOwnerTransfer.isPresent()) {
|
||||||
newExternalAssetOwnerTransfer = createNewEntry(settlementDate, buybackExternalAssetOwnerTransfer,
|
newExternalAssetOwnerTransfer = createNewEntryAndExpireOldEntry(settlementDate, buybackExternalAssetOwnerTransfer,
|
||||||
ExternalTransferStatus.CANCELLED, ExternalTransferSubStatus.UNSOLD, settlementDate, settlementDate);
|
ExternalTransferStatus.CANCELLED, ExternalTransferSubStatus.UNSOLD, settlementDate, settlementDate);
|
||||||
} else {
|
} else {
|
||||||
newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, buybackExternalAssetOwnerTransfer,
|
newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, buybackExternalAssetOwnerTransfer,
|
||||||
@@ -134,26 +151,71 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
|||||||
return buybackExternalAssetOwnerTransfer;
|
return buybackExternalAssetOwnerTransfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExternalAssetOwnerTransfer sellAsset(final Loan loan, final LocalDate settlementDate,
|
private ExternalAssetOwnerTransfer sellAssetOrDecline(final Loan loan, final LocalDate settlementDate,
|
||||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer;
|
if (!loanTransferabilityService.isTransferable(loan, externalAssetOwnerTransfer)) {
|
||||||
if (isTransferable(loan)) {
|
// Validation fails. Decline asset sell.
|
||||||
newExternalAssetOwnerTransfer = createActiveEntry(settlementDate, externalAssetOwnerTransfer);
|
ExternalTransferSubStatus declinedSubStatus = loanTransferabilityService.getDeclinedSubStatus(loan);
|
||||||
createActiveMapping(loan.getId(), newExternalAssetOwnerTransfer);
|
return declinePendingEntry(loan, settlementDate, externalAssetOwnerTransfer, declinedSubStatus);
|
||||||
newExternalAssetOwnerTransfer
|
|
||||||
.setExternalAssetOwnerTransferDetails(createAssetOwnerTransferDetails(loan, newExternalAssetOwnerTransfer));
|
|
||||||
accountingService.createJournalEntriesForSaleAssetTransfer(loan, newExternalAssetOwnerTransfer);
|
|
||||||
} else {
|
|
||||||
ExternalTransferSubStatus subStatus = ExternalTransferSubStatus.BALANCE_ZERO;
|
|
||||||
if (MathUtil.nullToDefault(loan.getTotalOverpaid(), BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
subStatus = ExternalTransferSubStatus.BALANCE_NEGATIVE;
|
|
||||||
}
|
|
||||||
newExternalAssetOwnerTransfer = createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.DECLINED,
|
|
||||||
subStatus, settlementDate, settlementDate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = sellAsset(loan, settlementDate, externalAssetOwnerTransfer);
|
||||||
|
createActiveMapping(loan.getId(), newExternalAssetOwnerTransfer);
|
||||||
|
newExternalAssetOwnerTransfer
|
||||||
|
.setExternalAssetOwnerTransferDetails(createAssetOwnerTransferDetails(loan, newExternalAssetOwnerTransfer));
|
||||||
|
|
||||||
return newExternalAssetOwnerTransfer;
|
return newExternalAssetOwnerTransfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwnerTransfer sellAsset(final Loan loan, final LocalDate settlementDate,
|
||||||
|
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
|
ExternalAssetOwner previousOwner = determinePreviousOwnerAndCleanupIfNeeded(loan, settlementDate, externalAssetOwnerTransfer);
|
||||||
|
ExternalTransferStatus activeStatus = determineActiveStatus(externalAssetOwnerTransfer);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer newTransfer = activatePendingEntry(settlementDate, externalAssetOwnerTransfer, activeStatus);
|
||||||
|
accountingService.createJournalEntriesForSaleAssetTransfer(loan, newTransfer, previousOwner);
|
||||||
|
return newTransfer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwner determinePreviousOwnerAndCleanupIfNeeded(final Loan loan, final LocalDate settlementDate,
|
||||||
|
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
|
if (!delayedSettlementAttributeService.isEnabled(loan.getLoanProduct().getId())) {
|
||||||
|
// When delayed settlement is disabled, asset is directly sold to investor, and we are the previous owner.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ExternalTransferStatus.PENDING_INTERMEDIATE == externalAssetOwnerTransfer.getStatus()) {
|
||||||
|
// When delayed settlement is enabled and asset is sold to intermediate, we are the previous owner.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When delayed settlement is enabled and asset is sold from intermediate to investor, the intermediate is the
|
||||||
|
// previous owner.
|
||||||
|
ExternalAssetOwnerTransfer activeIntermediateTransfer = getActiveIntermediateOrThrow(loan);
|
||||||
|
expireTransfer(settlementDate, activeIntermediateTransfer);
|
||||||
|
externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(), activeIntermediateTransfer);
|
||||||
|
|
||||||
|
return activeIntermediateTransfer.getOwner();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalTransferStatus determineActiveStatus(final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
|
if (ExternalTransferStatus.PENDING_INTERMEDIATE == externalAssetOwnerTransfer.getStatus()) {
|
||||||
|
return ExternalTransferStatus.ACTIVE_INTERMEDIATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExternalTransferStatus.ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwnerTransfer getActiveIntermediateOrThrow(final Loan loan) {
|
||||||
|
Optional<ExternalAssetOwnerTransfer> optionalActiveIntermediateTransfer = externalAssetOwnerTransferRepository
|
||||||
|
.findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
|
||||||
|
criteriaBuilder.equal(root.get("status"), ExternalTransferStatus.ACTIVE_INTERMEDIATE),
|
||||||
|
criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)));
|
||||||
|
|
||||||
|
return optionalActiveIntermediateTransfer
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Expected a effective transfer of ACTIVE_INTERMEDIATE status to be present."));
|
||||||
|
}
|
||||||
|
|
||||||
private ExternalAssetOwnerTransferDetails createAssetOwnerTransferDetails(Loan loan,
|
private ExternalAssetOwnerTransferDetails createAssetOwnerTransferDetails(Loan loan,
|
||||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
ExternalAssetOwnerTransferDetails details = new ExternalAssetOwnerTransferDetails();
|
ExternalAssetOwnerTransferDetails details = new ExternalAssetOwnerTransferDetails();
|
||||||
@@ -176,10 +238,6 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
|||||||
externalAssetOwnerTransferLoanMappingRepository.save(externalAssetOwnerTransferLoanMapping);
|
externalAssetOwnerTransferLoanMappingRepository.save(externalAssetOwnerTransferLoanMapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTransferable(final Loan loan) {
|
|
||||||
return MathUtil.nullToDefault(loan.getSummary().getTotalOutstanding(), BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List<ExternalAssetOwnerTransfer> transferDataList,
|
private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List<ExternalAssetOwnerTransfer> transferDataList,
|
||||||
Loan loan) {
|
Loan loan) {
|
||||||
ExternalAssetOwnerTransfer cancelledPendingTransfer = cancelTransfer(settlementDate, transferDataList.get(0));
|
ExternalAssetOwnerTransfer cancelledPendingTransfer = cancelTransfer(settlementDate, transferDataList.get(0));
|
||||||
@@ -190,11 +248,23 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
|||||||
|
|
||||||
private ExternalAssetOwnerTransfer cancelTransfer(final LocalDate settlementDate,
|
private ExternalAssetOwnerTransfer cancelTransfer(final LocalDate settlementDate,
|
||||||
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
return createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.CANCELLED,
|
return createNewEntryAndExpireOldEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.CANCELLED,
|
||||||
ExternalTransferSubStatus.SAMEDAY_TRANSFERS, settlementDate, settlementDate);
|
ExternalTransferSubStatus.SAMEDAY_TRANSFERS, settlementDate, settlementDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExternalAssetOwnerTransfer createNewEntry(final LocalDate settlementDate,
|
private ExternalAssetOwnerTransfer activatePendingEntry(final LocalDate settlementDate,
|
||||||
|
final ExternalAssetOwnerTransfer pendingTransfer, final ExternalTransferStatus activeStatus) {
|
||||||
|
LocalDate effectiveFrom = settlementDate.plusDays(1);
|
||||||
|
return createNewEntryAndExpireOldEntry(settlementDate, pendingTransfer, activeStatus, null, effectiveFrom, FUTURE_DATE_9999_12_31);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwnerTransfer declinePendingEntry(final Loan loan, final LocalDate settlementDate,
|
||||||
|
final ExternalAssetOwnerTransfer pendingTransfer, ExternalTransferSubStatus subStatus) {
|
||||||
|
return createNewEntryAndExpireOldEntry(settlementDate, pendingTransfer, ExternalTransferStatus.DECLINED, subStatus, settlementDate,
|
||||||
|
settlementDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwnerTransfer createNewEntryAndExpireOldEntry(final LocalDate settlementDate,
|
||||||
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer, final ExternalTransferStatus status,
|
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer, final ExternalTransferStatus status,
|
||||||
final ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo) {
|
final ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo) {
|
||||||
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||||
@@ -205,20 +275,26 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
|||||||
newExternalAssetOwnerTransfer.setSettlementDate(settlementDate);
|
newExternalAssetOwnerTransfer.setSettlementDate(settlementDate);
|
||||||
newExternalAssetOwnerTransfer.setLoanId(externalAssetOwnerTransfer.getLoanId());
|
newExternalAssetOwnerTransfer.setLoanId(externalAssetOwnerTransfer.getLoanId());
|
||||||
newExternalAssetOwnerTransfer.setExternalLoanId(externalAssetOwnerTransfer.getExternalLoanId());
|
newExternalAssetOwnerTransfer.setExternalLoanId(externalAssetOwnerTransfer.getExternalLoanId());
|
||||||
|
newExternalAssetOwnerTransfer.setExternalGroupId(externalAssetOwnerTransfer.getExternalGroupId());
|
||||||
newExternalAssetOwnerTransfer.setPurchasePriceRatio(externalAssetOwnerTransfer.getPurchasePriceRatio());
|
newExternalAssetOwnerTransfer.setPurchasePriceRatio(externalAssetOwnerTransfer.getPurchasePriceRatio());
|
||||||
newExternalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
|
newExternalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
|
||||||
newExternalAssetOwnerTransfer.setEffectiveDateTo(effectiveDateTo);
|
newExternalAssetOwnerTransfer.setEffectiveDateTo(effectiveDateTo);
|
||||||
|
|
||||||
externalAssetOwnerTransfer.setEffectiveDateTo(settlementDate);
|
expireTransfer(settlementDate, externalAssetOwnerTransfer);
|
||||||
externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
|
|
||||||
return externalAssetOwnerTransferRepository.save(newExternalAssetOwnerTransfer);
|
return externalAssetOwnerTransferRepository.save(newExternalAssetOwnerTransfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExternalAssetOwnerTransfer createActiveEntry(final LocalDate settlementDate,
|
private void expireTransfer(final LocalDate settlementDate, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
externalAssetOwnerTransfer.setEffectiveDateTo(settlementDate);
|
||||||
LocalDate effectiveFrom = settlementDate.plusDays(1);
|
externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
|
||||||
return createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.ACTIVE, null, effectiveFrom,
|
}
|
||||||
FUTURE_DATE_9999_12_31);
|
|
||||||
|
private ExternalTransferStatus determineExpectedActiveStatus(final ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer) {
|
||||||
|
if (ExternalTransferStatus.BUYBACK_INTERMEDIATE == buybackExternalAssetOwnerTransfer.getStatus()) {
|
||||||
|
return ExternalTransferStatus.ACTIVE_INTERMEDIATE;
|
||||||
|
}
|
||||||
|
return ExternalTransferStatus.ACTIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 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.config;
|
||||||
|
|
||||||
|
import org.apache.fineract.investor.service.DelayedSettlementAttributeService;
|
||||||
|
import org.apache.fineract.investor.service.LoanTransferabilityService;
|
||||||
|
import org.apache.fineract.investor.service.LoanTransferabilityServiceImpl;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class LoanAccountOwnerTransferConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(LoanTransferabilityService.class)
|
||||||
|
public LoanTransferabilityService loanTransferabilityService(DelayedSettlementAttributeService delayedSettlementAttributeService) {
|
||||||
|
return new LoanTransferabilityServiceImpl(delayedSettlementAttributeService);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -29,6 +29,7 @@ public class ExternalTransferData {
|
|||||||
private ExternalTransferLoanData loan;
|
private ExternalTransferLoanData loan;
|
||||||
private ExternalTransferDataDetails details;
|
private ExternalTransferDataDetails details;
|
||||||
private String transferExternalId;
|
private String transferExternalId;
|
||||||
|
private String transferExternalGroupId;
|
||||||
private String purchasePriceRatio;
|
private String purchasePriceRatio;
|
||||||
private LocalDate settlementDate;
|
private LocalDate settlementDate;
|
||||||
private ExternalTransferStatus status;
|
private ExternalTransferStatus status;
|
||||||
|
|||||||
+1
@@ -25,6 +25,7 @@ public final class ExternalTransferRequestParameters {
|
|||||||
public static final String SETTLEMENT_DATE = "settlementDate";
|
public static final String SETTLEMENT_DATE = "settlementDate";
|
||||||
public static final String OWNER_EXTERNAL_ID = "ownerExternalId";
|
public static final String OWNER_EXTERNAL_ID = "ownerExternalId";
|
||||||
public static final String TRANSFER_EXTERNAL_ID = "transferExternalId";
|
public static final String TRANSFER_EXTERNAL_ID = "transferExternalId";
|
||||||
|
public static final String TRANSFER_EXTERNAL_GROUP_ID = "transferExternalGroupId";
|
||||||
public static final String PURCHASE_PRICE_RATIO = "purchasePriceRatio";
|
public static final String PURCHASE_PRICE_RATIO = "purchasePriceRatio";
|
||||||
public static final String DATEFORMAT = "dateFormat";
|
public static final String DATEFORMAT = "dateFormat";
|
||||||
public static final String LOCALE = "locale";
|
public static final String LOCALE = "locale";
|
||||||
|
|||||||
+1
-1
@@ -19,5 +19,5 @@
|
|||||||
package org.apache.fineract.investor.data;
|
package org.apache.fineract.investor.data;
|
||||||
|
|
||||||
public enum ExternalTransferStatus {
|
public enum ExternalTransferStatus {
|
||||||
ACTIVE, DECLINED, PENDING, BUYBACK, CANCELLED
|
ACTIVE, ACTIVE_INTERMEDIATE, DECLINED, PENDING, PENDING_INTERMEDIATE, BUYBACK, BUYBACK_INTERMEDIATE, CANCELLED
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -79,4 +79,6 @@ public class ExternalAssetOwnerTransfer extends AbstractAuditableWithUTCDateTime
|
|||||||
@Column(name = "external_loan_id", length = 100)
|
@Column(name = "external_loan_id", length = 100)
|
||||||
private ExternalId externalLoanId;
|
private ExternalId externalLoanId;
|
||||||
|
|
||||||
|
@Column(name = "external_group_id", length = 100)
|
||||||
|
private ExternalId externalGroupId;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -36,6 +36,7 @@ public class SearchedExternalAssetOwner {
|
|||||||
|
|
||||||
private final ExternalId owner;
|
private final ExternalId owner;
|
||||||
private final ExternalId transferExternalId;
|
private final ExternalId transferExternalId;
|
||||||
|
private final ExternalId transferExternalGroupId;
|
||||||
|
|
||||||
private final ExternalTransferStatus status;
|
private final ExternalTransferStatus status;
|
||||||
private final ExternalTransferSubStatus subStatus;
|
private final ExternalTransferSubStatus subStatus;
|
||||||
|
|||||||
+5
-4
@@ -94,10 +94,11 @@ public class SearchingExternalAssetOwnerRepositoryImpl implements SearchingExter
|
|||||||
query.orderBy(orders);
|
query.orderBy(orders);
|
||||||
|
|
||||||
query.select(cb.construct(SearchedExternalAssetOwner.class, root.get("id"), root.get("loanId"), root.get("externalLoanId"),
|
query.select(cb.construct(SearchedExternalAssetOwner.class, root.get("id"), root.get("loanId"), root.get("externalLoanId"),
|
||||||
owner.get("externalId"), root.get("externalId"), root.get("status"), root.get("subStatus"), root.get("purchasePriceRatio"),
|
owner.get("externalId"), root.get("externalId"), root.get("externalGroupId"), root.get("status"), root.get("subStatus"),
|
||||||
root.get("settlementDate"), root.get("effectiveDateFrom"), root.get("effectiveDateTo"), details.get("id"),
|
root.get("purchasePriceRatio"), root.get("settlementDate"), root.get("effectiveDateFrom"), root.get("effectiveDateTo"),
|
||||||
details.get("totalOutstanding"), details.get("totalPrincipalOutstanding"), details.get("totalInterestOutstanding"),
|
details.get("id"), details.get("totalOutstanding"), details.get("totalPrincipalOutstanding"),
|
||||||
details.get("totalFeeChargesOutstanding"), details.get("totalPenaltyChargesOutstanding"), details.get("totalOverpaid")));
|
details.get("totalInterestOutstanding"), details.get("totalFeeChargesOutstanding"),
|
||||||
|
details.get("totalPenaltyChargesOutstanding"), details.get("totalOverpaid")));
|
||||||
|
|
||||||
TypedQuery<SearchedExternalAssetOwner> queryToExecute = entityManager.createQuery(query);
|
TypedQuery<SearchedExternalAssetOwner> queryToExecute = entityManager.createQuery(query);
|
||||||
return criteriaQueryFactory.readPage(queryToExecute, ExternalAssetOwnerTransfer.class, pageable, spec);
|
return criteriaQueryFactory.readPage(queryToExecute, ExternalAssetOwnerTransfer.class, pageable, spec);
|
||||||
|
|||||||
+2
-1
@@ -18,12 +18,13 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.investor.service;
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwner;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
|
||||||
public interface AccountingService {
|
public interface AccountingService {
|
||||||
|
|
||||||
void createJournalEntriesForSaleAssetTransfer(Loan loan, ExternalAssetOwnerTransfer transfer);
|
void createJournalEntriesForSaleAssetTransfer(Loan loan, ExternalAssetOwnerTransfer transfer, ExternalAssetOwner previousOwner);
|
||||||
|
|
||||||
void createJournalEntriesForBuybackAssetTransfer(Loan loan, ExternalAssetOwnerTransfer transfer);
|
void createJournalEntriesForBuybackAssetTransfer(Loan loan, ExternalAssetOwnerTransfer transfer);
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-22
@@ -31,8 +31,8 @@ import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialA
|
|||||||
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
|
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
||||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
|
|
||||||
import org.apache.fineract.investor.accounting.journalentry.service.InvestorAccountingHelper;
|
import org.apache.fineract.investor.accounting.journalentry.service.InvestorAccountingHelper;
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwner;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
||||||
@@ -53,25 +53,44 @@ public class AccountingServiceImpl implements AccountingService {
|
|||||||
private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository;
|
private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository;
|
||||||
private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository;
|
private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository;
|
||||||
|
|
||||||
private static boolean participateInTransfer(FinancialActivityAccount financialActivityAccount, JournalEntry journalEntry,
|
|
||||||
JournalEntryType filterType) {
|
|
||||||
return filterType.getValue().equals(journalEntry.getType())
|
|
||||||
&& !Objects.equals(financialActivityAccount.getGlAccount().getId(), journalEntry.getGlAccount().getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createJournalEntriesForSaleAssetTransfer(final Loan loan, final ExternalAssetOwnerTransfer transfer) {
|
public void createJournalEntriesForSaleAssetTransfer(final Loan loan, final ExternalAssetOwnerTransfer transfer,
|
||||||
|
final ExternalAssetOwner previousOwner) {
|
||||||
|
|
||||||
|
final ExternalAssetOwner newOwner = transfer.getOwner();
|
||||||
List<JournalEntry> journalEntryList = createJournalEntries(loan, transfer, true);
|
List<JournalEntry> journalEntryList = createJournalEntries(loan, transfer, true);
|
||||||
createMappingToTransfer(transfer, journalEntryList);
|
createMappingToTransfer(transfer, journalEntryList);
|
||||||
createMappingToOwner(transfer, journalEntryList, JournalEntryType.DEBIT);
|
|
||||||
|
FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
|
||||||
|
.findByFinancialActivityTypeWithNotFoundDetection(AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue());
|
||||||
|
journalEntryList.forEach(journalEntry -> {
|
||||||
|
if (isOwnedByFinancialActivityAccount(journalEntry, financialActivityAccount)) {
|
||||||
|
createMappingToOwner(null, journalEntry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExternalAssetOwner owner = determineOwnerForSale(journalEntry, loan, previousOwner, newOwner);
|
||||||
|
createMappingToOwner(owner, journalEntry);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createJournalEntriesForBuybackAssetTransfer(final Loan loan, final ExternalAssetOwnerTransfer transfer) {
|
public void createJournalEntriesForBuybackAssetTransfer(final Loan loan, final ExternalAssetOwnerTransfer transfer) {
|
||||||
|
final ExternalAssetOwner previousOwner = transfer.getOwner();
|
||||||
List<JournalEntry> journalEntryList = createJournalEntries(loan, transfer, false);
|
List<JournalEntry> journalEntryList = createJournalEntries(loan, transfer, false);
|
||||||
createMappingToTransfer(transfer, journalEntryList);
|
createMappingToTransfer(transfer, journalEntryList);
|
||||||
createMappingToOwner(transfer, journalEntryList,
|
|
||||||
LoanStatus.OVERPAID.equals(loan.getStatus()) ? JournalEntryType.DEBIT : JournalEntryType.CREDIT);
|
FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
|
||||||
|
.findByFinancialActivityTypeWithNotFoundDetection(AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue());
|
||||||
|
journalEntryList.forEach(journalEntry -> {
|
||||||
|
if (isOwnedByFinancialActivityAccount(journalEntry, financialActivityAccount)) {
|
||||||
|
createMappingToOwner(null, journalEntry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExternalAssetOwner owner = determineOwnerForBuyback(journalEntry, loan, previousOwner);
|
||||||
|
createMappingToOwner(owner, journalEntry);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -95,18 +114,52 @@ public class AccountingServiceImpl implements AccountingService {
|
|||||||
return journalEntryList;
|
return journalEntryList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createMappingToOwner(ExternalAssetOwnerTransfer transfer, List<JournalEntry> journalEntryList,
|
private void createMappingToOwner(final ExternalAssetOwner owner, final JournalEntry journalEntry) {
|
||||||
JournalEntryType filterType) {
|
if (owner == null) {
|
||||||
FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
|
return;
|
||||||
.findByFinancialActivityTypeWithNotFoundDetection(AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue());
|
}
|
||||||
journalEntryList.forEach(journalEntry -> {
|
|
||||||
if (participateInTransfer(financialActivityAccount, journalEntry, filterType)) {
|
ExternalAssetOwnerJournalEntryMapping mapping = new ExternalAssetOwnerJournalEntryMapping();
|
||||||
ExternalAssetOwnerJournalEntryMapping mapping = new ExternalAssetOwnerJournalEntryMapping();
|
mapping.setJournalEntry(journalEntry);
|
||||||
mapping.setJournalEntry(journalEntry);
|
mapping.setOwner(owner);
|
||||||
mapping.setOwner(transfer.getOwner());
|
externalAssetOwnerJournalEntryMappingRepository.saveAndFlush(mapping);
|
||||||
externalAssetOwnerJournalEntryMappingRepository.saveAndFlush(mapping);
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwner determineOwnerForSale(final JournalEntry journalEntry, final Loan loan,
|
||||||
|
final ExternalAssetOwner previousOwner, final ExternalAssetOwner newOwner) {
|
||||||
|
final boolean isOverpaid = LoanStatus.OVERPAID.equals(loan.getStatus());
|
||||||
|
|
||||||
|
if (isOverpaid) {
|
||||||
|
if (journalEntry.isCreditEntry()) {
|
||||||
|
return newOwner;
|
||||||
}
|
}
|
||||||
});
|
if (journalEntry.isDebitEntry()) {
|
||||||
|
return previousOwner;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (journalEntry.isCreditEntry()) {
|
||||||
|
return previousOwner;
|
||||||
|
}
|
||||||
|
if (journalEntry.isDebitEntry()) {
|
||||||
|
return newOwner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Given journalEntry has invalid type: " + journalEntry.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwner determineOwnerForBuyback(final JournalEntry journalEntry, final Loan loan,
|
||||||
|
final ExternalAssetOwner previousOwner) {
|
||||||
|
final boolean isOverpaid = LoanStatus.OVERPAID.equals(loan.getStatus());
|
||||||
|
if (isOverpaid && journalEntry.isDebitEntry()) {
|
||||||
|
return previousOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOverpaid && journalEntry.isCreditEntry()) {
|
||||||
|
return previousOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createMappingToTransfer(ExternalAssetOwnerTransfer transfer, List<JournalEntry> journalEntryList) {
|
private void createMappingToTransfer(ExternalAssetOwnerTransfer transfer, List<JournalEntry> journalEntryList) {
|
||||||
@@ -211,4 +264,8 @@ public class AccountingServiceImpl implements AccountingService {
|
|||||||
}
|
}
|
||||||
return journalEntryList;
|
return journalEntryList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isOwnedByFinancialActivityAccount(JournalEntry journalEntry, FinancialActivityAccount financialActivityAccount) {
|
||||||
|
return Objects.equals(financialActivityAccount.getGlAccount().getId(), journalEntry.getGlAccount().getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
public interface DelayedSettlementAttributeService {
|
||||||
|
|
||||||
|
boolean isEnabled(Long loanProductId);
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
import static org.apache.fineract.investor.data.attribute.SettlementModelExternalAssetOwnerLoanProductAttribute.DELAYED_SETTLEMENT;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.Page;
|
||||||
|
import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DelayedSettlementAttributeServiceImpl implements DelayedSettlementAttributeService {
|
||||||
|
|
||||||
|
private final ExternalAssetOwnerLoanProductAttributesReadService externalAssetOwnerLoanProductAttributesReadService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled(final Long loanProductId) {
|
||||||
|
Page<ExternalTransferLoanProductAttributesData> attributesDataPage = externalAssetOwnerLoanProductAttributesReadService
|
||||||
|
.retrieveAllLoanProductAttributesByLoanProductId(loanProductId, DELAYED_SETTLEMENT.getAttributeKey());
|
||||||
|
String attributeValue = attributesDataPage.getPageItems().stream().findFirst()
|
||||||
|
.map(ExternalTransferLoanProductAttributesData::getAttributeValue).orElse(null);
|
||||||
|
return DELAYED_SETTLEMENT.getAttributeValue().equals(attributeValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -37,6 +37,7 @@ public interface ExternalAssetOwnersTransferMapper {
|
|||||||
@Mapping(target = "loan.loanId", source = "loanId")
|
@Mapping(target = "loan.loanId", source = "loanId")
|
||||||
@Mapping(target = "loan.externalId", source = "externalLoanId")
|
@Mapping(target = "loan.externalId", source = "externalLoanId")
|
||||||
@Mapping(target = "transferExternalId", source = "externalId")
|
@Mapping(target = "transferExternalId", source = "externalId")
|
||||||
|
@Mapping(target = "transferExternalGroupId", source = "externalGroupId")
|
||||||
@Mapping(target = "effectiveFrom", source = "effectiveDateFrom")
|
@Mapping(target = "effectiveFrom", source = "effectiveDateFrom")
|
||||||
@Mapping(target = "effectiveTo", source = "effectiveDateTo")
|
@Mapping(target = "effectiveTo", source = "effectiveDateTo")
|
||||||
@Mapping(target = "purchasePriceRatio", source = "purchasePriceRatio")
|
@Mapping(target = "purchasePriceRatio", source = "purchasePriceRatio")
|
||||||
|
|||||||
+2
@@ -23,6 +23,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|||||||
|
|
||||||
public interface ExternalAssetOwnersWriteService {
|
public interface ExternalAssetOwnersWriteService {
|
||||||
|
|
||||||
|
CommandProcessingResult intermediarySaleLoanByLoanId(JsonCommand jsonCommand);
|
||||||
|
|
||||||
CommandProcessingResult saleLoanByLoanId(JsonCommand command);
|
CommandProcessingResult saleLoanByLoanId(JsonCommand command);
|
||||||
|
|
||||||
CommandProcessingResult buybackLoanByLoanId(JsonCommand command);
|
CommandProcessingResult buybackLoanByLoanId(JsonCommand command);
|
||||||
|
|||||||
+238
-35
@@ -18,8 +18,12 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.investor.service;
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE_INTERMEDIATE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING_INTERMEDIATE;
|
||||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.ACTIVE;
|
import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.ACTIVE;
|
||||||
|
import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.CLOSED_OBLIGATIONS_MET;
|
||||||
|
import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.OVERPAID;
|
||||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.TRANSFER_IN_PROGRESS;
|
import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.TRANSFER_IN_PROGRESS;
|
||||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.TRANSFER_ON_HOLD;
|
import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.TRANSFER_ON_HOLD;
|
||||||
|
|
||||||
@@ -35,9 +39,10 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.cob.data.LoanIdAndExternalIdAndStatus;
|
import org.apache.fineract.cob.data.LoanDataForExternalTransfer;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
@@ -70,26 +75,55 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
|
|
||||||
private static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
|
private static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
|
||||||
private static final List<LoanStatus> ACTIVE_LOAN_STATUSES = List.of(ACTIVE, TRANSFER_IN_PROGRESS, TRANSFER_ON_HOLD);
|
private static final List<LoanStatus> ACTIVE_LOAN_STATUSES = List.of(ACTIVE, TRANSFER_IN_PROGRESS, TRANSFER_ON_HOLD);
|
||||||
|
private static final List<LoanStatus> VALID_DELAYED_SETTLEMENT_LOAN_STATUSES_BUYBACK_AND_SALE = List.of(ACTIVE, TRANSFER_IN_PROGRESS,
|
||||||
|
TRANSFER_ON_HOLD, OVERPAID, CLOSED_OBLIGATIONS_MET);
|
||||||
private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES = List.of(ExternalTransferStatus.PENDING,
|
private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES = List.of(ExternalTransferStatus.PENDING,
|
||||||
ExternalTransferStatus.ACTIVE);
|
ExternalTransferStatus.ACTIVE);
|
||||||
|
private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT = List
|
||||||
|
.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE, ExternalTransferStatus.ACTIVE);
|
||||||
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
||||||
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
|
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
private final LoanRepository loanRepository;
|
private final LoanRepository loanRepository;
|
||||||
|
private final DelayedSettlementAttributeService delayedSettlementAttributeService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult intermediarySaleLoanByLoanId(JsonCommand command) {
|
||||||
|
final JsonElement json = fromApiJsonHelper.parse(command.json());
|
||||||
|
validateIntermediarySaleRequestBody(command.json());
|
||||||
|
Long loanId = command.getLoanId();
|
||||||
|
LoanDataForExternalTransfer loanDataForExternalTransfer = fetchAndValidateLoanDataForExternalTransfer(loanId);
|
||||||
|
if (!delayedSettlementAttributeService.isEnabled(loanDataForExternalTransfer.getLoanProductId())) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
|
String.format("Delayed Settlement Configuration is not enabled for the loan product: %s",
|
||||||
|
loanDataForExternalTransfer.getLoanProductShortName()));
|
||||||
|
}
|
||||||
|
ExternalId externalId = getTransferExternalIdFromJson(json);
|
||||||
|
validateExternalId(externalId);
|
||||||
|
validateLoanStatusIntermediarySale(loanDataForExternalTransfer);
|
||||||
|
ExternalAssetOwnerTransfer intermediarySaleTransfer = createIntermediarySaleTransfer(loanId, json,
|
||||||
|
loanDataForExternalTransfer.getExternalId());
|
||||||
|
validateIntermediarySale(intermediarySaleTransfer);
|
||||||
|
externalAssetOwnerTransferRepository.saveAndFlush(intermediarySaleTransfer);
|
||||||
|
return buildResponseData(intermediarySaleTransfer);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public CommandProcessingResult saleLoanByLoanId(JsonCommand command) {
|
public CommandProcessingResult saleLoanByLoanId(JsonCommand command) {
|
||||||
final JsonElement json = fromApiJsonHelper.parse(command.json());
|
final JsonElement json = fromApiJsonHelper.parse(command.json());
|
||||||
|
final LoanDataForExternalTransfer loanDataForExternalTransfer = fetchAndValidateLoanDataForExternalTransfer(command.getLoanId());
|
||||||
|
final boolean isDelayedSettlementEnabled = delayedSettlementAttributeService
|
||||||
|
.isEnabled(loanDataForExternalTransfer.getLoanProductId());
|
||||||
validateSaleRequestBody(command.json());
|
validateSaleRequestBody(command.json());
|
||||||
ExternalId externalId = getTransferExternalIdFromJson(json);
|
ExternalId externalId = getTransferExternalIdFromJson(json);
|
||||||
validateExternalId(externalId);
|
validateExternalId(externalId);
|
||||||
Long loanId = command.getLoanId();
|
Long loanId = command.getLoanId();
|
||||||
LoanIdAndExternalIdAndStatus loanIdAndExternalIdAndStatus = fetchLoanDetails(loanId);
|
validateLoanStatus(loanDataForExternalTransfer, isDelayedSettlementEnabled);
|
||||||
validateLoanStatus(loanIdAndExternalIdAndStatus);
|
|
||||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = createSaleTransfer(loanId, json,
|
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = createSaleTransfer(loanId, json,
|
||||||
loanIdAndExternalIdAndStatus.getExternalId());
|
loanDataForExternalTransfer.getExternalId());
|
||||||
validateSale(externalAssetOwnerTransfer);
|
validateSale(externalAssetOwnerTransfer, isDelayedSettlementEnabled);
|
||||||
externalAssetOwnerTransferRepository.saveAndFlush(externalAssetOwnerTransfer);
|
externalAssetOwnerTransferRepository.saveAndFlush(externalAssetOwnerTransfer);
|
||||||
return buildResponseData(externalAssetOwnerTransfer);
|
return buildResponseData(externalAssetOwnerTransfer);
|
||||||
}
|
}
|
||||||
@@ -99,13 +133,13 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
public CommandProcessingResult buybackLoanByLoanId(JsonCommand command) {
|
public CommandProcessingResult buybackLoanByLoanId(JsonCommand command) {
|
||||||
final JsonElement json = fromApiJsonHelper.parse(command.json());
|
final JsonElement json = fromApiJsonHelper.parse(command.json());
|
||||||
validateBuybackRequestBody(command.json());
|
validateBuybackRequestBody(command.json());
|
||||||
Long loanId = command.getLoanId();
|
LoanDataForExternalTransfer loanDataForExternalTransfer = fetchAndValidateLoanDataForExternalTransfer(command.getLoanId());
|
||||||
validateLoan(loanId);
|
|
||||||
LocalDate settlementDate = getSettlementDateFromJson(json);
|
LocalDate settlementDate = getSettlementDateFromJson(json);
|
||||||
ExternalId externalId = getTransferExternalIdFromJson(json);
|
ExternalId externalId = getTransferExternalIdFromJson(json);
|
||||||
validateSettlementDate(settlementDate);
|
validateSettlementDate(settlementDate);
|
||||||
validateExternalId(externalId);
|
validateExternalId(externalId);
|
||||||
ExternalAssetOwnerTransfer effectiveTransfer = fetchAndValidateEffectiveTransferForBuyback(loanId, settlementDate);
|
ExternalAssetOwnerTransfer effectiveTransfer = fetchAndValidateEffectiveTransferForBuyback(loanDataForExternalTransfer,
|
||||||
|
settlementDate);
|
||||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = createBuybackTransfer(effectiveTransfer, settlementDate, externalId);
|
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = createBuybackTransfer(effectiveTransfer, settlementDate, externalId);
|
||||||
externalAssetOwnerTransferRepository.saveAndFlush(externalAssetOwnerTransfer);
|
externalAssetOwnerTransferRepository.saveAndFlush(externalAssetOwnerTransfer);
|
||||||
return buildResponseData(externalAssetOwnerTransfer);
|
return buildResponseData(externalAssetOwnerTransfer);
|
||||||
@@ -120,10 +154,8 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateLoan(Long loanId) {
|
private LoanDataForExternalTransfer fetchAndValidateLoanDataForExternalTransfer(Long loanId) {
|
||||||
if (!loanRepository.existsById(loanId)) {
|
return loanRepository.findLoanDataForExternalTransferByLoanId(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
|
||||||
throw new LoanNotFoundException(loanId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -136,10 +168,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
return buildResponseData(cancelTransfer);
|
return buildResponseData(cancelTransfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateEffectiveTransferForSale(final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
private void validateEffectiveTransferForSale(final List<ExternalAssetOwnerTransfer> effectiveTransfers) {
|
||||||
List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository
|
|
||||||
.findEffectiveTransfersOrderByIdDesc(externalAssetOwnerTransfer.getLoanId(), DateUtils.getBusinessLocalDate());
|
|
||||||
|
|
||||||
if (effectiveTransfers.size() == 2) {
|
if (effectiveTransfers.size() == 2) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
||||||
} else if (effectiveTransfers.size() == 1) {
|
} else if (effectiveTransfers.size() == 1) {
|
||||||
@@ -156,9 +185,47 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuyback(final Long loanId, final LocalDate settlementDate) {
|
private void validateEffectiveTransferForDelayedSettlementSale(final List<ExternalAssetOwnerTransfer> effectiveTransfers) {
|
||||||
|
if (effectiveTransfers.size() > 1) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
||||||
|
} else if (effectiveTransfers.size() == 1) {
|
||||||
|
if (!ACTIVE_INTERMEDIATE.equals(effectiveTransfers.get(0).getStatus())) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
|
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, no effective transfer found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateEffectiveTransferForIntermediarySale(final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository
|
List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository
|
||||||
.findEffectiveTransfersOrderByIdDesc(loanId, DateUtils.getBusinessLocalDate());
|
.findEffectiveTransfersOrderByIdDesc(externalAssetOwnerTransfer.getLoanId(), DateUtils.getBusinessLocalDate());
|
||||||
|
|
||||||
|
if (effectiveTransfers.size() > 1) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
||||||
|
} else if (effectiveTransfers.size() == 1) {
|
||||||
|
if (PENDING_INTERMEDIATE.equals(effectiveTransfers.get(0).getStatus())) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
|
"External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan");
|
||||||
|
} else if (ExternalTransferStatus.ACTIVE.equals(effectiveTransfers.get(0).getStatus())) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
|
"This loan cannot be sold, because it is owned by an external asset owner");
|
||||||
|
} else {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(String.format(
|
||||||
|
"This loan cannot be sold, because it is incorrect state! (transferId = %s)", effectiveTransfers.get(0).getId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuyback(
|
||||||
|
final LoanDataForExternalTransfer loanDataForExternalTransfer, final LocalDate settlementDate) {
|
||||||
|
if (delayedSettlementAttributeService.isEnabled(loanDataForExternalTransfer.getLoanProductId())) {
|
||||||
|
return fetchAndValidateEffectiveTransferForBuybackWithDelayedSettlement(loanDataForExternalTransfer, settlementDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository
|
||||||
|
.findEffectiveTransfersOrderByIdDesc(loanDataForExternalTransfer.getId(), DateUtils.getBusinessLocalDate());
|
||||||
|
|
||||||
if (effectiveTransfers.size() == 0) {
|
if (effectiveTransfers.size() == 0) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
@@ -179,6 +246,39 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
return effectiveTransfers.get(0);
|
return effectiveTransfers.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuybackWithDelayedSettlement(
|
||||||
|
final LoanDataForExternalTransfer loanDataForExternalTransfer, final LocalDate settlementDate) {
|
||||||
|
List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository
|
||||||
|
.findEffectiveTransfersOrderByIdDesc(loanDataForExternalTransfer.getId(), DateUtils.getBusinessLocalDate());
|
||||||
|
|
||||||
|
if (effectiveTransfers.isEmpty()) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
|
"This loan cannot be bought back, it is not owned by an external asset owner");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<ExternalTransferStatus> effectiveTransferStatuses = effectiveTransfers.stream().map(ExternalAssetOwnerTransfer::getStatus)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (Set.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE, ExternalTransferStatus.PENDING).equals(effectiveTransferStatuses)) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be bought back, external asset owner sale is pending");
|
||||||
|
} else if (Set.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE, ExternalTransferStatus.BUYBACK_INTERMEDIATE)
|
||||||
|
.equals(effectiveTransferStatuses)
|
||||||
|
|| Set.of(ExternalTransferStatus.ACTIVE, ExternalTransferStatus.BUYBACK).equals(effectiveTransferStatuses)) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
|
"This loan cannot be bought back, external asset owner buyback transfer is already in progress");
|
||||||
|
} else if (!BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT.contains(effectiveTransfers.get(0).getStatus())) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
|
String.format("This loan cannot be bought back, effective transfer is not in right state: %s",
|
||||||
|
effectiveTransfers.get(0).getStatus()));
|
||||||
|
} else if (DateUtils.isBefore(settlementDate, effectiveTransfers.get(0).getSettlementDate())) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
|
String.format("This loan cannot be bought back, settlement date is earlier than effective transfer settlement date: %s",
|
||||||
|
effectiveTransfers.get(0).getSettlementDate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return effectiveTransfers.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForCancel(final Long transferId) {
|
private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForCancel(final Long transferId) {
|
||||||
ExternalAssetOwnerTransfer selectedTransfer = externalAssetOwnerTransferRepository.findById(transferId)
|
ExternalAssetOwnerTransfer selectedTransfer = externalAssetOwnerTransferRepository.findById(transferId)
|
||||||
.orElseThrow(() -> new ExternalAssetOwnerInitiateTransferException(
|
.orElseThrow(() -> new ExternalAssetOwnerInitiateTransferException(
|
||||||
@@ -206,10 +306,9 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||||
externalAssetOwnerTransfer.setExternalId(externalId);
|
externalAssetOwnerTransfer.setExternalId(externalId);
|
||||||
externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner());
|
externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner());
|
||||||
externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.BUYBACK);
|
externalAssetOwnerTransfer.setStatus(determineStatusAfterBuyback(effectiveTransfer));
|
||||||
externalAssetOwnerTransfer.setLoanId(effectiveTransfer.getLoanId());
|
externalAssetOwnerTransfer.setLoanId(effectiveTransfer.getLoanId());
|
||||||
externalAssetOwnerTransfer.setExternalLoanId(effectiveTransfer.getExternalLoanId());
|
externalAssetOwnerTransfer.setExternalLoanId(effectiveTransfer.getExternalLoanId());
|
||||||
externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner());
|
|
||||||
externalAssetOwnerTransfer.setSettlementDate(settlementDate);
|
externalAssetOwnerTransfer.setSettlementDate(settlementDate);
|
||||||
externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
|
externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
|
||||||
externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31);
|
externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31);
|
||||||
@@ -217,6 +316,16 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
return externalAssetOwnerTransfer;
|
return externalAssetOwnerTransfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ExternalTransferStatus determineStatusAfterBuyback(ExternalAssetOwnerTransfer effectiveTransfer) {
|
||||||
|
return switch (effectiveTransfer.getStatus()) {
|
||||||
|
case PENDING -> ExternalTransferStatus.BUYBACK;
|
||||||
|
case ACTIVE -> ExternalTransferStatus.BUYBACK;
|
||||||
|
case ACTIVE_INTERMEDIATE -> ExternalTransferStatus.BUYBACK_INTERMEDIATE;
|
||||||
|
default -> throw new ExternalAssetOwnerInitiateTransferException(String.format(
|
||||||
|
"This loan cannot be bought back, effective transfer is not in right state: %s", effectiveTransfer.getStatus()));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private ExternalAssetOwnerTransfer createCancelTransfer(ExternalAssetOwnerTransfer effectiveTransfer) {
|
private ExternalAssetOwnerTransfer createCancelTransfer(ExternalAssetOwnerTransfer effectiveTransfer) {
|
||||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||||
externalAssetOwnerTransfer.setExternalId(effectiveTransfer.getExternalId());
|
externalAssetOwnerTransfer.setExternalId(effectiveTransfer.getExternalId());
|
||||||
@@ -224,6 +333,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
externalAssetOwnerTransfer.setSubStatus(ExternalTransferSubStatus.USER_REQUESTED);
|
externalAssetOwnerTransfer.setSubStatus(ExternalTransferSubStatus.USER_REQUESTED);
|
||||||
externalAssetOwnerTransfer.setLoanId(effectiveTransfer.getLoanId());
|
externalAssetOwnerTransfer.setLoanId(effectiveTransfer.getLoanId());
|
||||||
externalAssetOwnerTransfer.setExternalLoanId(effectiveTransfer.getExternalLoanId());
|
externalAssetOwnerTransfer.setExternalLoanId(effectiveTransfer.getExternalLoanId());
|
||||||
|
externalAssetOwnerTransfer.setExternalGroupId(effectiveTransfer.getExternalGroupId());
|
||||||
externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner());
|
externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner());
|
||||||
externalAssetOwnerTransfer.setSettlementDate(effectiveTransfer.getSettlementDate());
|
externalAssetOwnerTransfer.setSettlementDate(effectiveTransfer.getSettlementDate());
|
||||||
externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveTransfer.getEffectiveDateFrom());
|
externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveTransfer.getEffectiveDateFrom());
|
||||||
@@ -241,9 +351,22 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateSale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
private void validateSale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer, boolean isDelayedSettlementEnabled) {
|
||||||
validateSettlementDate(externalAssetOwnerTransfer);
|
validateSettlementDate(externalAssetOwnerTransfer);
|
||||||
validateEffectiveTransferForSale(externalAssetOwnerTransfer);
|
|
||||||
|
final List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository
|
||||||
|
.findEffectiveTransfersOrderByIdDesc(externalAssetOwnerTransfer.getLoanId(), DateUtils.getBusinessLocalDate());
|
||||||
|
|
||||||
|
if (isDelayedSettlementEnabled) {
|
||||||
|
validateEffectiveTransferForDelayedSettlementSale(effectiveTransfers);
|
||||||
|
} else {
|
||||||
|
validateEffectiveTransferForSale(effectiveTransfers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateIntermediarySale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
|
validateSettlementDate(externalAssetOwnerTransfer);
|
||||||
|
validateEffectiveTransferForIntermediarySale(externalAssetOwnerTransfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateSettlementDate(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
private void validateSettlementDate(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
@@ -256,9 +379,25 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateLoanStatus(LoanIdAndExternalIdAndStatus entity) {
|
private void validateLoanStatus(LoanDataForExternalTransfer loanDataForExternalTransfer, boolean isDelayedSettlementEnabled) {
|
||||||
if (!ACTIVE_LOAN_STATUSES.contains(LoanStatus.fromInt(entity.getLoanStatus()))) {
|
LoanStatus loanStatus = LoanStatus.fromInt(loanDataForExternalTransfer.getLoanStatus());
|
||||||
throw new ExternalAssetOwnerInitiateTransferException("Loan is not in active status");
|
if (!getValidLoanStatusList(isDelayedSettlementEnabled).contains(loanStatus)) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(String.format("Loan status %s is not valid for transfer.", loanStatus));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateLoanStatusIntermediarySale(LoanDataForExternalTransfer loanDataForExternalTransfer) {
|
||||||
|
LoanStatus loanStatus = LoanStatus.fromInt(loanDataForExternalTransfer.getLoanStatus());
|
||||||
|
if (!ACTIVE_LOAN_STATUSES.contains(loanStatus)) {
|
||||||
|
throw new ExternalAssetOwnerInitiateTransferException(String.format("Loan status %s is not valid for transfer.", loanStatus));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LoanStatus> getValidLoanStatusList(boolean isDelayedSettlementEnabled) {
|
||||||
|
if (isDelayedSettlementEnabled) {
|
||||||
|
return VALID_DELAYED_SETTLEMENT_LOAN_STATUSES_BUYBACK_AND_SALE;
|
||||||
|
} else {
|
||||||
|
return ACTIVE_LOAN_STATUSES;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,14 +415,33 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31);
|
externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31);
|
||||||
externalAssetOwnerTransfer.setLoanId(loanId);
|
externalAssetOwnerTransfer.setLoanId(loanId);
|
||||||
externalAssetOwnerTransfer.setExternalLoanId(externalLoanId);
|
externalAssetOwnerTransfer.setExternalLoanId(externalLoanId);
|
||||||
|
externalAssetOwnerTransfer.setExternalGroupId(getTransferExternalGroupIdFromJson(json));
|
||||||
|
return externalAssetOwnerTransfer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwnerTransfer createIntermediarySaleTransfer(Long loanId, JsonElement json, ExternalId externalLoanId) {
|
||||||
|
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
LocalDate effectiveFrom = ThreadLocalContextUtil.getBusinessDate();
|
||||||
|
|
||||||
|
ExternalAssetOwner owner = getOwner(json);
|
||||||
|
externalAssetOwnerTransfer.setOwner(owner);
|
||||||
|
externalAssetOwnerTransfer.setExternalId(getTransferExternalIdFromJson(json));
|
||||||
|
externalAssetOwnerTransfer.setStatus(PENDING_INTERMEDIATE);
|
||||||
|
externalAssetOwnerTransfer.setPurchasePriceRatio(getPurchasePriceRatioFromJson(json));
|
||||||
|
externalAssetOwnerTransfer.setSettlementDate(getSettlementDateFromJson(json));
|
||||||
|
externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveFrom);
|
||||||
|
externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31);
|
||||||
|
externalAssetOwnerTransfer.setLoanId(loanId);
|
||||||
|
externalAssetOwnerTransfer.setExternalLoanId(externalLoanId);
|
||||||
|
externalAssetOwnerTransfer.setExternalGroupId(getTransferExternalGroupIdFromJson(json));
|
||||||
return externalAssetOwnerTransfer;
|
return externalAssetOwnerTransfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateSaleRequestBody(String apiRequestBodyAsJson) {
|
private void validateSaleRequestBody(String apiRequestBodyAsJson) {
|
||||||
final Set<String> requestParameters = new HashSet<>(
|
final Set<String> requestParameters = new HashSet<>(Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE,
|
||||||
Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE, ExternalTransferRequestParameters.OWNER_EXTERNAL_ID,
|
ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID,
|
||||||
ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO,
|
ExternalTransferRequestParameters.TRANSFER_EXTERNAL_GROUP_ID, ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO,
|
||||||
ExternalTransferRequestParameters.DATEFORMAT, ExternalTransferRequestParameters.LOCALE));
|
ExternalTransferRequestParameters.DATEFORMAT, ExternalTransferRequestParameters.LOCALE));
|
||||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
||||||
|
|
||||||
}.getType();
|
}.getType();
|
||||||
@@ -308,6 +466,51 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
LocalDate settlementDate = fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, json);
|
LocalDate settlementDate = fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, json);
|
||||||
baseDataValidator.reset().parameter(ExternalTransferRequestParameters.SETTLEMENT_DATE).value(settlementDate).notNull();
|
baseDataValidator.reset().parameter(ExternalTransferRequestParameters.SETTLEMENT_DATE).value(settlementDate).notNull();
|
||||||
|
|
||||||
|
final String transferExternalGroupId = fromApiJsonHelper
|
||||||
|
.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_GROUP_ID, json);
|
||||||
|
baseDataValidator.reset().parameter(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_GROUP_ID).value(transferExternalGroupId)
|
||||||
|
.ignoreIfNull().notExceedingLengthOf(100);
|
||||||
|
|
||||||
|
if (!dataValidationErrors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||||
|
dataValidationErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateIntermediarySaleRequestBody(String apiRequestBodyAsJson) {
|
||||||
|
final Set<String> requestParameters = new HashSet<>(Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE,
|
||||||
|
ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID,
|
||||||
|
ExternalTransferRequestParameters.TRANSFER_EXTERNAL_GROUP_ID, ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO,
|
||||||
|
ExternalTransferRequestParameters.DATEFORMAT, ExternalTransferRequestParameters.LOCALE));
|
||||||
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
||||||
|
|
||||||
|
}.getType();
|
||||||
|
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, apiRequestBodyAsJson, requestParameters);
|
||||||
|
|
||||||
|
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loantransfer");
|
||||||
|
final JsonElement json = fromApiJsonHelper.parse(apiRequestBodyAsJson);
|
||||||
|
|
||||||
|
String ownerExternalId = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, json);
|
||||||
|
baseDataValidator.reset().parameter(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID).value(ownerExternalId).notBlank()
|
||||||
|
.notExceedingLengthOf(100);
|
||||||
|
|
||||||
|
String transferExternalId = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, json);
|
||||||
|
baseDataValidator.reset().parameter(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID).value(transferExternalId).ignoreIfNull()
|
||||||
|
.notExceedingLengthOf(100);
|
||||||
|
|
||||||
|
String purchasePriceRatio = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, json);
|
||||||
|
baseDataValidator.reset().parameter(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO).value(purchasePriceRatio).notBlank()
|
||||||
|
.notExceedingLengthOf(50);
|
||||||
|
|
||||||
|
LocalDate settlementDate = fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, json);
|
||||||
|
baseDataValidator.reset().parameter(ExternalTransferRequestParameters.SETTLEMENT_DATE).value(settlementDate).notNull();
|
||||||
|
|
||||||
|
String transferExternalGroupId = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_GROUP_ID,
|
||||||
|
json);
|
||||||
|
baseDataValidator.reset().parameter(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_GROUP_ID).value(transferExternalGroupId)
|
||||||
|
.ignoreIfNull().notExceedingLengthOf(100);
|
||||||
|
|
||||||
if (!dataValidationErrors.isEmpty()) {
|
if (!dataValidationErrors.isEmpty()) {
|
||||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||||
dataValidationErrors);
|
dataValidationErrors);
|
||||||
@@ -352,6 +555,12 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
return StringUtils.isEmpty(transferExternalId) ? ExternalId.generate() : ExternalIdFactory.produce(transferExternalId);
|
return StringUtils.isEmpty(transferExternalId) ? ExternalId.generate() : ExternalIdFactory.produce(transferExternalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ExternalId getTransferExternalGroupIdFromJson(JsonElement json) {
|
||||||
|
String transferExternalGroupId = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_GROUP_ID,
|
||||||
|
json);
|
||||||
|
return StringUtils.isEmpty(transferExternalGroupId) ? null : ExternalIdFactory.produce(transferExternalGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
private String getPurchasePriceRatioFromJson(JsonElement json) {
|
private String getPurchasePriceRatioFromJson(JsonElement json) {
|
||||||
return fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, json);
|
return fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, json);
|
||||||
}
|
}
|
||||||
@@ -368,10 +577,4 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
externalAssetOwner.setExternalId(ExternalIdFactory.produce(externalId));
|
externalAssetOwner.setExternalId(ExternalIdFactory.produce(externalId));
|
||||||
return externalAssetOwnerRepository.saveAndFlush(externalAssetOwner);
|
return externalAssetOwnerRepository.saveAndFlush(externalAssetOwner);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoanIdAndExternalIdAndStatus fetchLoanDetails(Long loanId) {
|
|
||||||
Optional<LoanIdAndExternalIdAndStatus> loanIdAndExternalIdAndStatusResult = loanRepository
|
|
||||||
.findLoanIdAndExternalIdAndStatusByLoanId(loanId);
|
|
||||||
return loanIdAndExternalIdAndStatusResult.orElseThrow(() -> new LoanNotFoundException(loanId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "LOAN", action = "INTERMEDIARYSALE")
|
||||||
|
public class IntermediarySaleToExternalAssetOwnerHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final ExternalAssetOwnersWriteService externalAssetOwnersWriteService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(JsonCommand command) {
|
||||||
|
return externalAssetOwnersWriteService.intermediarySaleLoanByLoanId(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+20
-19
@@ -19,10 +19,14 @@
|
|||||||
package org.apache.fineract.investor.service;
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
import static org.apache.fineract.infrastructure.core.service.DateUtils.getBusinessLocalDate;
|
import static org.apache.fineract.infrastructure.core.service.DateUtils.getBusinessLocalDate;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE_INTERMEDIATE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK_INTERMEDIATE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.CANCELLED;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.CANCELLED;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.DECLINED;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.DECLINED;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING_INTERMEDIATE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_NEGATIVE;
|
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_NEGATIVE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_ZERO;
|
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_ZERO;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.SAMEDAY_TRANSFERS;
|
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.SAMEDAY_TRANSFERS;
|
||||||
@@ -37,7 +41,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
import org.apache.fineract.investor.data.ExternalTransferStatus;
|
|
||||||
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
|
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferDetails;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferDetails;
|
||||||
@@ -64,25 +67,21 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
|||||||
@Override
|
@Override
|
||||||
public void handleLoanClosedOrOverpaid(Loan loan) {
|
public void handleLoanClosedOrOverpaid(Loan loan) {
|
||||||
Long loanId = loan.getId();
|
Long loanId = loan.getId();
|
||||||
List<ExternalAssetOwnerTransfer> transferDataList = findAllPendingOrBuybackTransfers(loanId);
|
List<ExternalAssetOwnerTransfer> transferDataList = findAllPendingOrBuybackOrIntermediateTransfers(loanId);
|
||||||
|
|
||||||
if (transferDataList.size() == 2) {
|
if (transferDataList.size() > 1) {
|
||||||
ExternalTransferSubStatus subStatus;
|
|
||||||
ExternalAssetOwnerTransfer pendingSaleTransfer = transferDataList.get(0);
|
|
||||||
ExternalAssetOwnerTransfer pendingBuybackTransfer = transferDataList.get(1);
|
|
||||||
if (isSameDayTransfers(transferDataList)) {
|
if (isSameDayTransfers(transferDataList)) {
|
||||||
subStatus = SAMEDAY_TRANSFERS;
|
transferDataList.forEach(externalAssetOwnerTransfer -> cancelTransfer(loan, externalAssetOwnerTransfer, SAMEDAY_TRANSFERS));
|
||||||
cancelTransfer(loan, pendingSaleTransfer, subStatus);
|
|
||||||
cancelTransfer(loan, pendingBuybackTransfer, subStatus);
|
|
||||||
} else {
|
} else {
|
||||||
declineTransfer(loan, pendingSaleTransfer);
|
// decline first and cancel the rest
|
||||||
cancelTransfer(loan, pendingBuybackTransfer, UNSOLD);
|
declineTransfer(loan, transferDataList.get(0));
|
||||||
|
transferDataList.stream().skip(1).forEach(assetOwnerTransfer -> cancelTransfer(loan, assetOwnerTransfer, UNSOLD));
|
||||||
}
|
}
|
||||||
} else if (transferDataList.size() == 1) {
|
} else if (transferDataList.size() == 1) {
|
||||||
ExternalAssetOwnerTransfer transfer = transferDataList.get(0);
|
ExternalAssetOwnerTransfer transfer = transferDataList.get(0);
|
||||||
if (PENDING.equals(transfer.getStatus())) {
|
if (PENDING.equals(transfer.getStatus()) || PENDING_INTERMEDIATE.equals(transfer.getStatus())) {
|
||||||
declineTransfer(loan, transfer);
|
declineTransfer(loan, transfer);
|
||||||
} else if (BUYBACK.equals(transfer.getStatus())) {
|
} else if (BUYBACK.equals(transfer.getStatus()) || BUYBACK_INTERMEDIATE.equals(transfer.getStatus())) {
|
||||||
executePendingBuybackTransfer(loan, transfer);
|
executePendingBuybackTransfer(loan, transfer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +102,7 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void executePendingBuybackTransfer(final Loan loan, ExternalAssetOwnerTransfer buybackTransfer) {
|
private void executePendingBuybackTransfer(final Loan loan, ExternalAssetOwnerTransfer buybackTransfer) {
|
||||||
ExternalAssetOwnerTransfer activeTransfer = findActiveTransfer(loan, buybackTransfer);
|
ExternalAssetOwnerTransfer activeTransfer = findActiveOrActiveIntermediateTransfer(loan, buybackTransfer);
|
||||||
updateActiveTransfer(activeTransfer);
|
updateActiveTransfer(activeTransfer);
|
||||||
buybackTransfer = updatePendingBuybackTransfer(loan, buybackTransfer);
|
buybackTransfer = updatePendingBuybackTransfer(loan, buybackTransfer);
|
||||||
|
|
||||||
@@ -119,6 +118,7 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
|||||||
ExternalAssetOwnerTransfer cancelledTransfer = new ExternalAssetOwnerTransfer();
|
ExternalAssetOwnerTransfer cancelledTransfer = new ExternalAssetOwnerTransfer();
|
||||||
cancelledTransfer.setOwner(pendingTransfer.getOwner());
|
cancelledTransfer.setOwner(pendingTransfer.getOwner());
|
||||||
cancelledTransfer.setExternalId(pendingTransfer.getExternalId());
|
cancelledTransfer.setExternalId(pendingTransfer.getExternalId());
|
||||||
|
cancelledTransfer.setExternalGroupId(pendingTransfer.getExternalGroupId());
|
||||||
cancelledTransfer.setStatus(CANCELLED);
|
cancelledTransfer.setStatus(CANCELLED);
|
||||||
cancelledTransfer.setSubStatus(subStatus);
|
cancelledTransfer.setSubStatus(subStatus);
|
||||||
cancelledTransfer.setSettlementDate(pendingTransfer.getSettlementDate());
|
cancelledTransfer.setSettlementDate(pendingTransfer.getSettlementDate());
|
||||||
@@ -134,6 +134,7 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
|||||||
ExternalAssetOwnerTransfer declinedTransfer = new ExternalAssetOwnerTransfer();
|
ExternalAssetOwnerTransfer declinedTransfer = new ExternalAssetOwnerTransfer();
|
||||||
declinedTransfer.setOwner(pendingSaleTransfer.getOwner());
|
declinedTransfer.setOwner(pendingSaleTransfer.getOwner());
|
||||||
declinedTransfer.setExternalId(pendingSaleTransfer.getExternalId());
|
declinedTransfer.setExternalId(pendingSaleTransfer.getExternalId());
|
||||||
|
declinedTransfer.setExternalGroupId(pendingSaleTransfer.getExternalGroupId());
|
||||||
declinedTransfer.setStatus(DECLINED);
|
declinedTransfer.setStatus(DECLINED);
|
||||||
declinedTransfer.setSubStatus(isBiggerThanZero(loan.getTotalOverpaid()) ? BALANCE_NEGATIVE : BALANCE_ZERO);
|
declinedTransfer.setSubStatus(isBiggerThanZero(loan.getTotalOverpaid()) ? BALANCE_NEGATIVE : BALANCE_ZERO);
|
||||||
declinedTransfer.setSettlementDate(pendingSaleTransfer.getSettlementDate());
|
declinedTransfer.setSettlementDate(pendingSaleTransfer.getSettlementDate());
|
||||||
@@ -176,20 +177,20 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
|||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExternalAssetOwnerTransfer findActiveTransfer(Loan loan, ExternalAssetOwnerTransfer buybackTransfer) {
|
private ExternalAssetOwnerTransfer findActiveOrActiveIntermediateTransfer(Loan loan, ExternalAssetOwnerTransfer buybackTransfer) {
|
||||||
return externalAssetOwnerTransferRepository
|
return externalAssetOwnerTransferRepository
|
||||||
.findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
|
.findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
|
||||||
criteriaBuilder.equal(root.get("owner"), buybackTransfer.getOwner()),
|
criteriaBuilder.equal(root.get("owner"), buybackTransfer.getOwner()),
|
||||||
criteriaBuilder.equal(root.get("status"), ExternalTransferStatus.ACTIVE),
|
root.get("status").in(List.of(ACTIVE, ACTIVE_INTERMEDIATE)),
|
||||||
criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)))
|
criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)))
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ExternalAssetOwnerTransfer> findAllPendingOrBuybackTransfers(Long loanId) {
|
private List<ExternalAssetOwnerTransfer> findAllPendingOrBuybackOrIntermediateTransfers(Long loanId) {
|
||||||
return externalAssetOwnerTransferRepository
|
return externalAssetOwnerTransferRepository
|
||||||
.findAll(
|
.findAll(
|
||||||
(root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loanId),
|
(root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loanId),
|
||||||
root.get("status").in(List.of(PENDING, BUYBACK)),
|
root.get("status").in(List.of(PENDING, BUYBACK, PENDING_INTERMEDIATE, BUYBACK_INTERMEDIATE)),
|
||||||
criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)),
|
criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)),
|
||||||
Sort.by(Sort.Direction.ASC, "id"));
|
Sort.by(Sort.Direction.ASC, "id"));
|
||||||
}
|
}
|
||||||
@@ -199,6 +200,6 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isSameDayTransfers(List<ExternalAssetOwnerTransfer> transferDataList) {
|
private static boolean isSameDayTransfers(List<ExternalAssetOwnerTransfer> transferDataList) {
|
||||||
return Objects.equals(transferDataList.get(0).getSettlementDate(), transferDataList.get(1).getSettlementDate());
|
return (transferDataList.stream().map(ExternalAssetOwnerTransfer::getSettlementDate).distinct().count() == 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
|
||||||
|
public interface LoanTransferabilityService {
|
||||||
|
|
||||||
|
boolean isTransferable(Loan loan, ExternalAssetOwnerTransfer externalAssetOwnerTransfer);
|
||||||
|
|
||||||
|
ExternalTransferSubStatus getDeclinedSubStatus(Loan loan);
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
|
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.portfolio.loanaccount.domain.Loan;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LoanTransferabilityServiceImpl implements LoanTransferabilityService {
|
||||||
|
|
||||||
|
private final DelayedSettlementAttributeService delayedSettlementAttributeService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTransferable(final Loan loan, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
|
if (shouldValidateTransferable(loan, externalAssetOwnerTransfer)) {
|
||||||
|
return MathUtil.nullToDefault(loan.getSummary().getTotalOutstanding(), BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExternalTransferSubStatus getDeclinedSubStatus(final Loan loan) {
|
||||||
|
if (MathUtil.nullToDefault(loan.getTotalOverpaid(), BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
return ExternalTransferSubStatus.BALANCE_NEGATIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExternalTransferSubStatus.BALANCE_ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldValidateTransferable(final Loan loan, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||||
|
if (!delayedSettlementAttributeService.isEnabled(loan.getLoanProduct().getId())) {
|
||||||
|
// When delayed settlement is disabled, asset is directly sold to investor. Need to validate.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ExternalTransferStatus.PENDING_INTERMEDIATE == externalAssetOwnerTransfer.getStatus()) {
|
||||||
|
// When delayed settlement is enabled and asset is sold to intermediate. Need to validate.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When delayed settlement is enabled and asset is sold from intermediate to investor. No need to validate.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-2
@@ -20,14 +20,18 @@ package org.apache.fineract.investor.service.serialization.serializer.investor;
|
|||||||
|
|
||||||
import static org.apache.fineract.infrastructure.core.service.DateUtils.DEFAULT_DATE_FORMATTER;
|
import static org.apache.fineract.infrastructure.core.service.DateUtils.DEFAULT_DATE_FORMATTER;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE_INTERMEDIATE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK_INTERMEDIATE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.CANCELLED;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.CANCELLED;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.DECLINED;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.DECLINED;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING_INTERMEDIATE;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.avro.generic.GenericContainer;
|
import org.apache.avro.generic.GenericContainer;
|
||||||
import org.apache.fineract.avro.generator.ByteBufferSerializable;
|
import org.apache.fineract.avro.generator.ByteBufferSerializable;
|
||||||
@@ -50,6 +54,9 @@ import org.springframework.stereotype.Component;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InvestorBusinessEventSerializer implements BusinessEventSerializer {
|
public class InvestorBusinessEventSerializer implements BusinessEventSerializer {
|
||||||
|
|
||||||
|
private static final Set<ExternalTransferStatus> EXECUTED_TRANSFER_STATUSES = Set.of(ACTIVE, ACTIVE_INTERMEDIATE, BUYBACK,
|
||||||
|
BUYBACK_INTERMEDIATE);
|
||||||
|
|
||||||
private final ExternalAssetOwnersReadService externalAssetOwnersReadService;
|
private final ExternalAssetOwnersReadService externalAssetOwnersReadService;
|
||||||
|
|
||||||
private static CurrencyDataV1 getCurrencyFromEvent(InvestorBusinessEvent event) {
|
private static CurrencyDataV1 getCurrencyFromEvent(InvestorBusinessEvent event) {
|
||||||
@@ -83,6 +90,7 @@ public class InvestorBusinessEventSerializer implements BusinessEventSerializer
|
|||||||
LoanOwnershipTransferDataV1.Builder builder = LoanOwnershipTransferDataV1.newBuilder().setLoanId(transferData.getLoan().getLoanId())
|
LoanOwnershipTransferDataV1.Builder builder = LoanOwnershipTransferDataV1.newBuilder().setLoanId(transferData.getLoan().getLoanId())
|
||||||
.setLoanExternalId(transferData.getLoan().getExternalId()).setTransferExternalId(transferData.getTransferExternalId())
|
.setLoanExternalId(transferData.getLoan().getExternalId()).setTransferExternalId(transferData.getTransferExternalId())
|
||||||
.setAssetOwnerExternalId(transferData.getOwner().getExternalId())
|
.setAssetOwnerExternalId(transferData.getOwner().getExternalId())
|
||||||
|
.setTransferExternalGroupId(transferData.getTransferExternalGroupId())
|
||||||
.setPurchasePriceRatio(transferData.getPurchasePriceRatio()).setCurrency(getCurrencyFromEvent(event))
|
.setPurchasePriceRatio(transferData.getPurchasePriceRatio()).setCurrency(getCurrencyFromEvent(event))
|
||||||
.setSettlementDate(transferData.getSettlementDate().format(DEFAULT_DATE_FORMATTER))
|
.setSettlementDate(transferData.getSettlementDate().format(DEFAULT_DATE_FORMATTER))
|
||||||
.setSubmittedDate(transferData.getSettlementDate().format(DEFAULT_DATE_FORMATTER)).setType(transferType)
|
.setSubmittedDate(transferData.getSettlementDate().format(DEFAULT_DATE_FORMATTER)).setType(transferType)
|
||||||
@@ -103,7 +111,15 @@ public class InvestorBusinessEventSerializer implements BusinessEventSerializer
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private static String getType(ExternalTransferStatus transferStatus) {
|
private static String getType(ExternalTransferStatus transferStatus) {
|
||||||
return transferStatus == BUYBACK ? "BUYBACK" : "SALE";
|
if (transferStatus == BUYBACK || transferStatus == BUYBACK_INTERMEDIATE) {
|
||||||
|
return "BUYBACK";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transferStatus == ACTIVE_INTERMEDIATE || transferStatus == PENDING_INTERMEDIATE) {
|
||||||
|
return "INTERMEDIARYSALE";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "SALE";
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UnpaidChargeDataV1> getUnpaidChargeData(InvestorBusinessEvent event) {
|
private List<UnpaidChargeDataV1> getUnpaidChargeData(InvestorBusinessEvent event) {
|
||||||
@@ -126,7 +142,7 @@ public class InvestorBusinessEventSerializer implements BusinessEventSerializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getStatus(ExternalTransferStatus status) {
|
private String getStatus(ExternalTransferStatus status) {
|
||||||
if (ACTIVE.equals(status) || BUYBACK.equals(status)) {
|
if (EXECUTED_TRANSFER_STATUSES.contains(status)) {
|
||||||
return "EXECUTED";
|
return "EXECUTED";
|
||||||
} else if (DECLINED.equals(status) || CANCELLED.equals(status)) {
|
} else if (DECLINED.equals(status) || CANCELLED.equals(status)) {
|
||||||
return status.name();
|
return status.name();
|
||||||
|
|||||||
+2
@@ -36,4 +36,6 @@
|
|||||||
<include relativeToChangelogFile="true" file="parts/0012_add_external_asset_owner_transfer_index.xml"/>
|
<include relativeToChangelogFile="true" file="parts/0012_add_external_asset_owner_transfer_index.xml"/>
|
||||||
<include relativeToChangelogFile="true" file="parts/0013_add_additional_asset_owner_indices.xml"/>
|
<include relativeToChangelogFile="true" file="parts/0013_add_additional_asset_owner_indices.xml"/>
|
||||||
<include relativeToChangelogFile="true" file="parts/0014_add_external_asset_owner_loan_product_configurable_attributes.xml"/>
|
<include relativeToChangelogFile="true" file="parts/0014_add_external_asset_owner_loan_product_configurable_attributes.xml"/>
|
||||||
|
<include relativeToChangelogFile="true" file="parts/0015_add_intermediary_sale_command.xml"/>
|
||||||
|
<include relativeToChangelogFile="true" file="parts/0016_add_external_reference_id.xml"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
<?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 id="1" author="fineract">
|
||||||
|
<insert tableName="m_permission">
|
||||||
|
<column name="grouping" value="transaction_loan"/>
|
||||||
|
<column name="code" value="INTERMEDIARYSALE_LOAN"/>
|
||||||
|
<column name="entity_name" value="LOAN"/>
|
||||||
|
<column name="action_name" value="INTERMEDIARYSALE"/>
|
||||||
|
<column name="can_maker_checker" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
<?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 id="1" author="fineract">
|
||||||
|
<addColumn tableName="m_external_asset_owner_transfer">
|
||||||
|
<column name="external_group_id" type="VARCHAR(100)"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+293
-166
@@ -24,18 +24,20 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyNoInteractions;
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ActionContext;
|
import org.apache.fineract.infrastructure.core.domain.ActionContext;
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||||
@@ -45,19 +47,26 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccount
|
|||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
import org.apache.fineract.investor.data.ExternalTransferStatus;
|
import org.apache.fineract.investor.data.ExternalTransferStatus;
|
||||||
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
|
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwner;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
|
||||||
import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
|
import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
|
||||||
import org.apache.fineract.investor.service.AccountingService;
|
import org.apache.fineract.investor.service.AccountingService;
|
||||||
|
import org.apache.fineract.investor.service.DelayedSettlementAttributeService;
|
||||||
|
import org.apache.fineract.investor.service.LoanTransferabilityService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
@@ -69,17 +78,27 @@ import org.springframework.data.jpa.domain.Specification;
|
|||||||
public class LoanAccountOwnerTransferBusinessStepTest {
|
public class LoanAccountOwnerTransferBusinessStepTest {
|
||||||
|
|
||||||
public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
|
public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
|
||||||
|
private static final Long LOAN_PRODUCT_ID = 2L;
|
||||||
private final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault());
|
private final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault());
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
|
private ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AccountingService accountingService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private BusinessEventNotifierService businessEventNotifierService;
|
private BusinessEventNotifierService businessEventNotifierService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private AccountingService accountingService;
|
private LoanTransferabilityService loanTransferabilityService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DelayedSettlementAttributeService delayedSettlementAttributeService;
|
||||||
|
|
||||||
private LoanAccountOwnerTransferBusinessStep underTest;
|
private LoanAccountOwnerTransferBusinessStep underTest;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@@ -88,7 +107,8 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
|||||||
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
|
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
|
||||||
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate)));
|
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate)));
|
||||||
underTest = new LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository,
|
underTest = new LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository,
|
||||||
externalAssetOwnerTransferLoanMappingRepository, accountingService, businessEventNotifierService);
|
externalAssetOwnerTransferLoanMappingRepository, accountingService, businessEventNotifierService,
|
||||||
|
loanTransferabilityService, delayedSettlementAttributeService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@@ -106,15 +126,21 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
|||||||
final Loan processedLoan = underTest.execute(loanForProcessing);
|
final Loan processedLoan = underTest.execute(loanForProcessing);
|
||||||
// then
|
// then
|
||||||
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||||
verifyNoInteractions(businessEventNotifierService);
|
verifyNoInteractions(businessEventNotifierService, loanTransferabilityService, accountingService);
|
||||||
assertEquals(processedLoan, loanForProcessing);
|
assertEquals(processedLoan, loanForProcessing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void givenLoanTwoTransferButInvalidTransfers() {
|
public void givenLoanTwoTransferButInvalidTransfers() {
|
||||||
// given
|
// given
|
||||||
|
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
|
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
|
||||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
|
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||||
|
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(false);
|
||||||
|
|
||||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
|
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
|
||||||
@@ -127,14 +153,46 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
|||||||
// then
|
// then
|
||||||
assertEquals("Illegal transfer found. Expected PENDING and BUYBACK, found: PENDING and ACTIVE", exception.getMessage());
|
assertEquals("Illegal transfer found. Expected PENDING and BUYBACK, found: PENDING and ACTIVE", exception.getMessage());
|
||||||
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||||
verifyNoInteractions(businessEventNotifierService);
|
verifyNoInteractions(businessEventNotifierService, loanTransferabilityService, accountingService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenSameDaySaleAndBuybackWithDelayedSettlement() {
|
||||||
|
// given
|
||||||
|
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
|
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
|
||||||
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
|
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||||
|
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
|
||||||
|
|
||||||
|
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);
|
||||||
|
// when
|
||||||
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing));
|
||||||
|
// then
|
||||||
|
assertEquals("Delayed Settlement enabled, but found 2 transfers of statuses: PENDING and BUYBACK", exception.getMessage());
|
||||||
|
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||||
|
verifyNoInteractions(businessEventNotifierService, loanTransferabilityService, accountingService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void givenLoanTwoTransferSameDay() {
|
public void givenLoanTwoTransferSameDay() {
|
||||||
// given
|
// given
|
||||||
|
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
|
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
|
||||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
|
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||||
|
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(false);
|
||||||
|
|
||||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
@@ -192,13 +250,20 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
|||||||
|
|
||||||
assertEquals(processedLoan, loanForProcessing);
|
assertEquals(processedLoan, loanForProcessing);
|
||||||
|
|
||||||
|
verifyNoInteractions(loanTransferabilityService, accountingService);
|
||||||
|
|
||||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
||||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, secondSaveResult);
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, secondSaveResult);
|
||||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, fourthSaveResult);
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, fourthSaveResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private static Stream<Arguments> buybackStatusDataProvider() {
|
||||||
public void givenLoanBuyback() {
|
return Stream.of(Arguments.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE), Arguments.of(ExternalTransferStatus.BUYBACK));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("buybackStatusDataProvider")
|
||||||
|
public void givenLoanBuyback(final ExternalTransferStatus buybackStatus) {
|
||||||
// given
|
// given
|
||||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
@@ -206,7 +271,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
|||||||
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
||||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK);
|
when(firstResponseItem.getStatus()).thenReturn(buybackStatus);
|
||||||
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
|
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
|
||||||
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
|
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
|
||||||
.thenReturn(response);
|
.thenReturn(response);
|
||||||
@@ -218,6 +283,8 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
|||||||
// when
|
// when
|
||||||
final Loan processedLoan = underTest.execute(loanForProcessing);
|
final Loan processedLoan = underTest.execute(loanForProcessing);
|
||||||
// then
|
// then
|
||||||
|
verifyNoInteractions(loanTransferabilityService);
|
||||||
|
|
||||||
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||||
verify(firstResponseItem).setEffectiveDateTo(actualDate);
|
verify(firstResponseItem).setEffectiveDateTo(actualDate);
|
||||||
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
|
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
@@ -226,147 +293,248 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
|||||||
|
|
||||||
assertEquals(processedLoan, loanForProcessing);
|
assertEquals(processedLoan, loanForProcessing);
|
||||||
|
|
||||||
|
verify(accountingService).createJournalEntriesForBuybackAssetTransfer(loanForProcessing, firstResponseItem);
|
||||||
|
verifyNoMoreInteractions(accountingService);
|
||||||
|
|
||||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
||||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, firstResponseItem);
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, firstResponseItem);
|
||||||
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
|
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private static Stream<Arguments> loanSaleTransferableDataProvider() {
|
||||||
public void givenLoanSale() {
|
return Stream.of(Arguments.of(false, ExternalTransferStatus.PENDING, ExternalTransferStatus.ACTIVE),
|
||||||
|
Arguments.of(true, ExternalTransferStatus.PENDING_INTERMEDIATE, ExternalTransferStatus.ACTIVE_INTERMEDIATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("loanSaleTransferableDataProvider")
|
||||||
|
public void givenLoanSaleTransferable(final boolean isDelayedSettlementEnabled, final ExternalTransferStatus pendingStatus,
|
||||||
|
final ExternalTransferStatus expectedActiveStatus) {
|
||||||
// given
|
// given
|
||||||
|
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
|
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
|
||||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||||
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
|
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(isDelayedSettlementEnabled);
|
||||||
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);
|
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
|
||||||
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
||||||
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE);
|
|
||||||
when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE);
|
ExternalAssetOwnerTransfer pendingTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
pendingTransfer.setStatus(pendingStatus);
|
||||||
|
List<ExternalAssetOwnerTransfer> response = List.of(pendingTransfer);
|
||||||
|
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
|
||||||
|
.thenReturn(response);
|
||||||
|
|
||||||
|
when(loanTransferabilityService.isTransferable(loanForProcessing, pendingTransfer)).thenReturn(true);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer savedNewTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
savedNewTransfer.setStatus(expectedActiveStatus);
|
||||||
|
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(pendingTransfer).thenReturn(savedNewTransfer);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final Loan processedLoan = underTest.execute(loanForProcessing);
|
final Loan processedLoan = underTest.execute(loanForProcessing);
|
||||||
// then
|
// then
|
||||||
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
verify(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer);
|
||||||
verify(firstResponseItem).setEffectiveDateTo(actualDate);
|
verifyNoMoreInteractions(loanTransferabilityService);
|
||||||
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
|
verify(externalAssetOwnerTransferRepository).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||||
|
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
|
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
|
.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
|
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
|
ExternalAssetOwnerTransfer capturedPendingTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0);
|
||||||
assertEquals(ExternalTransferStatus.ACTIVE, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
|
ExternalAssetOwnerTransfer capturedActiveTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1);
|
||||||
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
|
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(),
|
assertEquals(actualDate, capturedPendingTransfer.getEffectiveDateTo());
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId());
|
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(),
|
assertCommonFieldsOfPendingAndActiveTransfers(capturedPendingTransfer, capturedActiveTransfer);
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio());
|
assertEquals(expectedActiveStatus, capturedActiveTransfer.getStatus());
|
||||||
assertEquals(actualDate.plusDays(1), externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
|
assertEquals(actualDate, capturedActiveTransfer.getSettlementDate());
|
||||||
assertEquals(FUTURE_DATE_9999_12_31, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
|
assertEquals(actualDate.plusDays(1), capturedActiveTransfer.getEffectiveDateFrom());
|
||||||
|
assertEquals(FUTURE_DATE_9999_12_31, capturedActiveTransfer.getEffectiveDateTo());
|
||||||
|
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransferLoanMapping> externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransferLoanMapping.class);
|
||||||
|
verify(externalAssetOwnerTransferLoanMappingRepository).save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture());
|
||||||
|
assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId());
|
||||||
|
assertEquals(savedNewTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
|
||||||
|
assertEquals(processedLoan, loanForProcessing);
|
||||||
|
|
||||||
|
verify(externalAssetOwnerTransferLoanMappingRepository).save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture());
|
||||||
|
|
||||||
|
verify(accountingService).createJournalEntriesForSaleAssetTransfer(loanForProcessing, savedNewTransfer, null);
|
||||||
|
verifyNoMoreInteractions(accountingService);
|
||||||
|
|
||||||
|
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
||||||
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, savedNewTransfer);
|
||||||
|
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> loanSaleNotTransferableDataProvider() {
|
||||||
|
return Stream.of(Arguments.of(ExternalTransferStatus.PENDING, ExternalTransferSubStatus.BALANCE_ZERO),
|
||||||
|
Arguments.of(ExternalTransferStatus.PENDING, ExternalTransferSubStatus.BALANCE_NEGATIVE),
|
||||||
|
Arguments.of(ExternalTransferStatus.PENDING_INTERMEDIATE, ExternalTransferSubStatus.BALANCE_ZERO),
|
||||||
|
Arguments.of(ExternalTransferStatus.PENDING_INTERMEDIATE, ExternalTransferSubStatus.BALANCE_NEGATIVE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("loanSaleNotTransferableDataProvider")
|
||||||
|
public void givenLoanSaleNotTransferable(final ExternalTransferStatus pendingStatus,
|
||||||
|
final ExternalTransferSubStatus expectedSubStatus) {
|
||||||
|
// given
|
||||||
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer pendingTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
pendingTransfer.setStatus(pendingStatus);
|
||||||
|
List<ExternalAssetOwnerTransfer> response = List.of(pendingTransfer);
|
||||||
|
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
|
||||||
|
.thenReturn(response);
|
||||||
|
|
||||||
|
when(loanTransferabilityService.isTransferable(loanForProcessing, pendingTransfer)).thenReturn(false);
|
||||||
|
when(loanTransferabilityService.getDeclinedSubStatus(loanForProcessing)).thenReturn(expectedSubStatus);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer savedNewTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
when(savedNewTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED);
|
||||||
|
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(pendingTransfer).thenReturn(savedNewTransfer);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final Loan processedLoan = underTest.execute(loanForProcessing);
|
||||||
|
// then
|
||||||
|
verify(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer);
|
||||||
|
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||||
|
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
|
ExternalAssetOwnerTransfer capturedPendingTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0);
|
||||||
|
ExternalAssetOwnerTransfer capturedActiveTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1);
|
||||||
|
|
||||||
|
assertEquals(actualDate, capturedPendingTransfer.getEffectiveDateTo());
|
||||||
|
|
||||||
|
assertCommonFieldsOfPendingAndActiveTransfers(capturedPendingTransfer, capturedActiveTransfer);
|
||||||
|
assertEquals(ExternalTransferStatus.DECLINED, capturedActiveTransfer.getStatus());
|
||||||
|
assertEquals(expectedSubStatus, capturedActiveTransfer.getSubStatus());
|
||||||
|
assertEquals(actualDate, capturedActiveTransfer.getSettlementDate());
|
||||||
|
assertEquals(actualDate, capturedActiveTransfer.getEffectiveDateFrom());
|
||||||
|
assertEquals(actualDate, capturedActiveTransfer.getEffectiveDateTo());
|
||||||
|
assertEquals(processedLoan, loanForProcessing);
|
||||||
|
|
||||||
|
verifyNoInteractions(accountingService);
|
||||||
|
|
||||||
|
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(1);
|
||||||
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, savedNewTransfer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaleLoanWithDelayedSettlementFromIntermediateToInvestor() {
|
||||||
|
// given
|
||||||
|
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
|
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
|
||||||
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
|
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||||
|
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
|
||||||
|
|
||||||
|
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
|
||||||
|
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
||||||
|
|
||||||
|
ExternalAssetOwner previousOwner = new ExternalAssetOwner();
|
||||||
|
ExternalAssetOwnerTransfer activeIntermediateTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
activeIntermediateTransfer.setOwner(previousOwner);
|
||||||
|
activeIntermediateTransfer.setStatus(ExternalTransferStatus.ACTIVE_INTERMEDIATE);
|
||||||
|
when(externalAssetOwnerTransferRepository.findOne(any(Specification.class))).thenReturn(Optional.of(activeIntermediateTransfer));
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer pendingTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
pendingTransfer.setStatus(ExternalTransferStatus.PENDING);
|
||||||
|
List<ExternalAssetOwnerTransfer> response = List.of(pendingTransfer);
|
||||||
|
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
|
||||||
|
.thenReturn(response);
|
||||||
|
|
||||||
|
when(loanTransferabilityService.isTransferable(loanForProcessing, pendingTransfer)).thenReturn(true);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer savedNewTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
savedNewTransfer.setStatus(ExternalTransferStatus.ACTIVE);
|
||||||
|
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(pendingTransfer).thenReturn(savedNewTransfer);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final Loan processedLoan = underTest.execute(loanForProcessing);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer);
|
||||||
|
verifyNoMoreInteractions(loanTransferabilityService);
|
||||||
|
verify(externalAssetOwnerTransferRepository).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||||
|
verify(externalAssetOwnerTransferRepository).findOne(any(Specification.class));
|
||||||
|
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
verify(externalAssetOwnerTransferRepository, times(3)).save(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
|
ExternalAssetOwnerTransfer capturedActiveIntermediateTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0);
|
||||||
|
ExternalAssetOwnerTransfer capturedPendingTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1);
|
||||||
|
ExternalAssetOwnerTransfer capturedActiveTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(2);
|
||||||
|
|
||||||
|
assertEquals(actualDate, capturedActiveIntermediateTransfer.getEffectiveDateTo());
|
||||||
|
assertEquals(actualDate, capturedPendingTransfer.getEffectiveDateTo());
|
||||||
|
|
||||||
|
assertCommonFieldsOfPendingAndActiveTransfers(capturedPendingTransfer, capturedActiveTransfer);
|
||||||
|
assertEquals(ExternalTransferStatus.ACTIVE, capturedActiveTransfer.getStatus());
|
||||||
|
assertEquals(actualDate, capturedActiveTransfer.getSettlementDate());
|
||||||
|
assertEquals(actualDate.plusDays(1), capturedActiveTransfer.getEffectiveDateFrom());
|
||||||
|
assertEquals(FUTURE_DATE_9999_12_31, capturedActiveTransfer.getEffectiveDateTo());
|
||||||
|
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransferLoanMapping> externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransferLoanMapping.class);
|
||||||
verify(externalAssetOwnerTransferLoanMappingRepository, times(1))
|
verify(externalAssetOwnerTransferLoanMappingRepository, times(1))
|
||||||
.save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture());
|
.save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture());
|
||||||
assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId());
|
assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId());
|
||||||
assertEquals(newTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
|
assertEquals(savedNewTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
|
||||||
assertEquals(processedLoan, loanForProcessing);
|
assertEquals(processedLoan, loanForProcessing);
|
||||||
|
|
||||||
|
verify(accountingService).createJournalEntriesForSaleAssetTransfer(loanForProcessing, savedNewTransfer, previousOwner);
|
||||||
|
verifyNoMoreInteractions(accountingService);
|
||||||
|
|
||||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
||||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer);
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, savedNewTransfer);
|
||||||
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
|
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void givenLoanSaleButBalanceIsZero() {
|
public void testSaleLoanWithDelayedSettlementFromIntermediateToInvestorActiveIntermediateTransferNotFound() {
|
||||||
// given
|
// given
|
||||||
|
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
|
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
|
||||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||||
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
|
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
|
||||||
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
|
|
||||||
|
when(externalAssetOwnerTransferRepository.findOne(any(Specification.class))).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer pendingTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
pendingTransfer.setStatus(ExternalTransferStatus.PENDING);
|
||||||
|
List<ExternalAssetOwnerTransfer> response = List.of(pendingTransfer);
|
||||||
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
|
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
|
||||||
.thenReturn(response);
|
.thenReturn(response);
|
||||||
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
when(loanTransferabilityService.isTransferable(loanForProcessing, pendingTransfer)).thenReturn(true);
|
||||||
.forClass(ExternalAssetOwnerTransfer.class);
|
|
||||||
ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
|
||||||
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
|
|
||||||
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
|
|
||||||
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
|
||||||
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ZERO);
|
|
||||||
when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ZERO);
|
|
||||||
when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED);
|
|
||||||
// when
|
// when
|
||||||
final Loan processedLoan = underTest.execute(loanForProcessing);
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
assertEquals("Expected a effective transfer of ACTIVE_INTERMEDIATE status to be present.", exception.getMessage());
|
||||||
verify(firstResponseItem).setEffectiveDateTo(actualDate);
|
|
||||||
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
|
|
||||||
|
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
|
verify(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer);
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
|
verifyNoMoreInteractions(loanTransferabilityService);
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
|
verifyNoInteractions(accountingService);
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
|
verify(externalAssetOwnerTransferRepository).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||||
assertEquals(ExternalTransferStatus.DECLINED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
|
verify(externalAssetOwnerTransferRepository).findOne(any(Specification.class));
|
||||||
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
|
verify(externalAssetOwnerTransferRepository, never()).save(any(ExternalAssetOwnerTransfer.class));
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(),
|
verifyNoInteractions(externalAssetOwnerTransferLoanMappingRepository);
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId());
|
verifyBusinessEvents(0);
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(),
|
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio());
|
|
||||||
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
|
|
||||||
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
|
|
||||||
assertEquals(processedLoan, loanForProcessing);
|
|
||||||
|
|
||||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(1);
|
|
||||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void givenLoanSaleButBalanceIsNegative() {
|
|
||||||
// given
|
|
||||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
|
||||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
|
||||||
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
|
|
||||||
List<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);
|
|
||||||
ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
|
||||||
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
|
|
||||||
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
|
|
||||||
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
|
||||||
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE.negate());
|
|
||||||
when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ONE.negate());
|
|
||||||
when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED);
|
|
||||||
// when
|
|
||||||
final Loan processedLoan = underTest.execute(loanForProcessing);
|
|
||||||
// then
|
|
||||||
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
|
||||||
verify(firstResponseItem).setEffectiveDateTo(actualDate);
|
|
||||||
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
|
|
||||||
|
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
|
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
|
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
|
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
|
|
||||||
assertEquals(ExternalTransferStatus.DECLINED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
|
|
||||||
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
|
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(),
|
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId());
|
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(),
|
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio());
|
|
||||||
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
|
|
||||||
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
|
|
||||||
assertEquals(processedLoan, loanForProcessing);
|
|
||||||
|
|
||||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(1);
|
|
||||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -383,55 +551,6 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
|||||||
assertEquals("Execute external asset owner transfer", actualEnumName);
|
assertEquals("Execute external asset owner transfer", actualEnumName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void givenLoanSaleAnsBuyBackButBalanceIsNegative() {
|
|
||||||
// given
|
|
||||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
|
||||||
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
|
|
||||||
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
|
||||||
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ZERO);
|
|
||||||
when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ONE);
|
|
||||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
|
||||||
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
|
||||||
secondResponseItem.setSettlementDate(actualDate.plusDays(2));
|
|
||||||
|
|
||||||
ExternalAssetOwnerTransfer firstSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
|
||||||
ExternalAssetOwnerTransfer secondSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
|
||||||
secondSaveResult.setSettlementDate(actualDate.plusDays(2));
|
|
||||||
ExternalAssetOwnerTransfer thirdSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
|
||||||
ExternalAssetOwnerTransfer fourthSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
|
||||||
|
|
||||||
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
|
|
||||||
when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(firstSaveResult)
|
|
||||||
.thenReturn(secondSaveResult).thenReturn(thirdSaveResult).thenReturn(fourthSaveResult);
|
|
||||||
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);
|
|
||||||
// when
|
|
||||||
Loan processedLoan = underTest.execute(loanForProcessing);
|
|
||||||
// then
|
|
||||||
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
|
||||||
verify(firstResponseItem).setEffectiveDateTo(actualDate);
|
|
||||||
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
|
|
||||||
|
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
|
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
|
|
||||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
|
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
|
|
||||||
assertEquals(ExternalTransferStatus.DECLINED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
|
|
||||||
assertEquals(ExternalTransferSubStatus.BALANCE_NEGATIVE,
|
|
||||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSubStatus());
|
|
||||||
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
|
|
||||||
|
|
||||||
assertEquals(processedLoan, loanForProcessing);
|
|
||||||
|
|
||||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
|
||||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, secondSaveResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private ArgumentCaptor<BusinessEvent<?>> verifyBusinessEvents(int expectedBusinessEvents) {
|
private ArgumentCaptor<BusinessEvent<?>> verifyBusinessEvents(int expectedBusinessEvents) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@@ -453,4 +572,12 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
|||||||
assertTrue(businessEventArgumentCaptor.getAllValues().get(index) instanceof LoanAccountSnapshotBusinessEvent);
|
assertTrue(businessEventArgumentCaptor.getAllValues().get(index) instanceof LoanAccountSnapshotBusinessEvent);
|
||||||
assertEquals(expectedLoan, ((LoanAccountSnapshotBusinessEvent) businessEventArgumentCaptor.getAllValues().get(index)).get());
|
assertEquals(expectedLoan, ((LoanAccountSnapshotBusinessEvent) businessEventArgumentCaptor.getAllValues().get(index)).get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertCommonFieldsOfPendingAndActiveTransfers(final ExternalAssetOwnerTransfer pendingTransfer,
|
||||||
|
final ExternalAssetOwnerTransfer activeTransfer) {
|
||||||
|
assertEquals(pendingTransfer.getOwner(), activeTransfer.getOwner());
|
||||||
|
assertEquals(pendingTransfer.getExternalId(), activeTransfer.getExternalId());
|
||||||
|
assertEquals(pendingTransfer.getLoanId(), activeTransfer.getLoanId());
|
||||||
|
assertEquals(pendingTransfer.getPurchasePriceRatio(), activeTransfer.getPurchasePriceRatio());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+594
@@ -0,0 +1,594 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
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.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.apache.fineract.accounting.common.AccountingConstants;
|
||||||
|
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount;
|
||||||
|
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
||||||
|
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
|
import org.apache.fineract.investor.accounting.journalentry.service.InvestorAccountingHelper;
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwner;
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping;
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository;
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMapping;
|
||||||
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMappingRepository;
|
||||||
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
|
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.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class AccountingServiceImplTest {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BUSINESS_DATE, LocalDate.of(2024, 9, 27))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createJournalEntriesForSaleAssetTransfer() {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
Loan loan = testContext.createMockedLoan();
|
||||||
|
|
||||||
|
ExternalAssetOwner previousOwner = new ExternalAssetOwner();
|
||||||
|
ExternalAssetOwner newOwner = new ExternalAssetOwner();
|
||||||
|
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
|
||||||
|
transfer.setOwner(newOwner);
|
||||||
|
|
||||||
|
JournalEntry principleAndInterestDebitJournalEntry = createJournalEntry(11, testContext.principleAndInterestAccount,
|
||||||
|
JournalEntryType.DEBIT);
|
||||||
|
JournalEntry principleAndInterestCreditJournalEntry = createJournalEntry(12, testContext.principleAndInterestAccount,
|
||||||
|
JournalEntryType.CREDIT);
|
||||||
|
JournalEntry feeAndPenaltyDebitJournalEntry = createJournalEntry(13, testContext.feeAndPenaltyAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry feeAndPenaltyCreditJournalEntry = createJournalEntry(14, testContext.feeAndPenaltyAccount, JournalEntryType.CREDIT);
|
||||||
|
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
|
||||||
|
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(false), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestCreditJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(false), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyCreditJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(true), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(true), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
|
||||||
|
|
||||||
|
// when
|
||||||
|
testContext.testSubject.createJournalEntriesForSaleAssetTransfer(loan, transfer, previousOwner);
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(4))
|
||||||
|
.saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List.of(
|
||||||
|
ownerJournalEntryMapping(principleAndInterestCreditJournalEntry, previousOwner),
|
||||||
|
ownerJournalEntryMapping(feeAndPenaltyCreditJournalEntry, previousOwner),
|
||||||
|
ownerJournalEntryMapping(principleAndInterestDebitJournalEntry, newOwner),
|
||||||
|
ownerJournalEntryMapping(feeAndPenaltyDebitJournalEntry, newOwner));
|
||||||
|
assertNotNull(capturedOwnerJournalEntryMappings);
|
||||||
|
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
|
||||||
|
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(6))
|
||||||
|
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
|
||||||
|
transferJournalEntryMapping(principleAndInterestCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(feeAndPenaltyCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(principleAndInterestDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(feeAndPenaltyDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferCreditJournalEntry, transfer));
|
||||||
|
assertNotNull(capturedTransferJournalEntryMappings);
|
||||||
|
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createJournalEntriesForSaleAssetTransferOfOverpaidLoan() {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
Loan loan = testContext.createMockedOverpaidLoan();
|
||||||
|
|
||||||
|
ExternalAssetOwner previousOwner = new ExternalAssetOwner();
|
||||||
|
ExternalAssetOwner newOwner = new ExternalAssetOwner();
|
||||||
|
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
|
||||||
|
transfer.setOwner(newOwner);
|
||||||
|
|
||||||
|
JournalEntry overpaidDebitJournalEntry = createJournalEntry(11, testContext.overpaymentAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry overpaidCreditJournalEntry = createJournalEntry(12, testContext.overpaymentAccount, JournalEntryType.CREDIT);
|
||||||
|
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
|
||||||
|
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(false), eq(testContext.overpaymentAccount))).thenReturn(overpaidCreditJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(true), eq(testContext.overpaymentAccount))).thenReturn(overpaidDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
|
||||||
|
|
||||||
|
// when
|
||||||
|
testContext.testSubject.createJournalEntriesForSaleAssetTransfer(loan, transfer, previousOwner);
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(2))
|
||||||
|
.saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List.of(
|
||||||
|
ownerJournalEntryMapping(overpaidCreditJournalEntry, newOwner),
|
||||||
|
ownerJournalEntryMapping(overpaidDebitJournalEntry, previousOwner));
|
||||||
|
assertNotNull(capturedOwnerJournalEntryMappings);
|
||||||
|
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
|
||||||
|
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(4))
|
||||||
|
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
|
||||||
|
transferJournalEntryMapping(overpaidCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(overpaidDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferCreditJournalEntry, transfer));
|
||||||
|
assertNotNull(capturedTransferJournalEntryMappings);
|
||||||
|
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createJournalEntriesForSaleAssetTransferWithNullPreviousOwner() {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
Loan loan = testContext.createMockedLoan();
|
||||||
|
|
||||||
|
ExternalAssetOwner newOwner = new ExternalAssetOwner();
|
||||||
|
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
|
||||||
|
transfer.setOwner(newOwner);
|
||||||
|
|
||||||
|
JournalEntry principleAndInterestDebitJournalEntry = createJournalEntry(11, testContext.principleAndInterestAccount,
|
||||||
|
JournalEntryType.DEBIT);
|
||||||
|
JournalEntry principleAndInterestCreditJournalEntry = createJournalEntry(12, testContext.principleAndInterestAccount,
|
||||||
|
JournalEntryType.CREDIT);
|
||||||
|
JournalEntry feeAndPenaltyDebitJournalEntry = createJournalEntry(13, testContext.feeAndPenaltyAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry feeAndPenaltyCreditJournalEntry = createJournalEntry(14, testContext.feeAndPenaltyAccount, JournalEntryType.CREDIT);
|
||||||
|
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
|
||||||
|
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(false), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestCreditJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(false), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyCreditJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(true), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(true), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
|
||||||
|
|
||||||
|
// when
|
||||||
|
testContext.testSubject.createJournalEntriesForSaleAssetTransfer(loan, transfer, null);
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(2))
|
||||||
|
.saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List.of(
|
||||||
|
ownerJournalEntryMapping(principleAndInterestDebitJournalEntry, newOwner),
|
||||||
|
ownerJournalEntryMapping(feeAndPenaltyDebitJournalEntry, newOwner));
|
||||||
|
assertNotNull(capturedOwnerJournalEntryMappings);
|
||||||
|
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
|
||||||
|
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(6))
|
||||||
|
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
|
||||||
|
transferJournalEntryMapping(principleAndInterestCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(feeAndPenaltyCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(principleAndInterestDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(feeAndPenaltyDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferCreditJournalEntry, transfer));
|
||||||
|
assertNotNull(capturedTransferJournalEntryMappings);
|
||||||
|
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createJournalEntriesForSaleAssetTransferOfOverpaidLoanWithNullPreviousOwner() {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
Loan loan = testContext.createMockedOverpaidLoan();
|
||||||
|
|
||||||
|
ExternalAssetOwner newOwner = new ExternalAssetOwner();
|
||||||
|
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
|
||||||
|
transfer.setOwner(newOwner);
|
||||||
|
|
||||||
|
JournalEntry overpaidDebitJournalEntry = createJournalEntry(11, testContext.overpaymentAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry overpaidCreditJournalEntry = createJournalEntry(12, testContext.overpaymentAccount, JournalEntryType.CREDIT);
|
||||||
|
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
|
||||||
|
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(false), eq(testContext.overpaymentAccount))).thenReturn(overpaidCreditJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(true), eq(testContext.overpaymentAccount))).thenReturn(overpaidDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
|
||||||
|
|
||||||
|
// when
|
||||||
|
testContext.testSubject.createJournalEntriesForSaleAssetTransfer(loan, transfer, null);
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerJournalEntryMappingRepository).saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List
|
||||||
|
.of(ownerJournalEntryMapping(overpaidCreditJournalEntry, newOwner));
|
||||||
|
assertNotNull(capturedOwnerJournalEntryMappings);
|
||||||
|
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
|
||||||
|
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(4))
|
||||||
|
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
|
||||||
|
transferJournalEntryMapping(overpaidCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(overpaidDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferCreditJournalEntry, transfer));
|
||||||
|
assertNotNull(capturedTransferJournalEntryMappings);
|
||||||
|
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createJournalEntriesForBuybackAssetTransfer() {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
Loan loan = testContext.createMockedLoan();
|
||||||
|
|
||||||
|
ExternalAssetOwner previousOwner = new ExternalAssetOwner();
|
||||||
|
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
|
||||||
|
transfer.setOwner(previousOwner);
|
||||||
|
|
||||||
|
JournalEntry principleAndInterestDebitJournalEntry = createJournalEntry(11, testContext.principleAndInterestAccount,
|
||||||
|
JournalEntryType.DEBIT);
|
||||||
|
JournalEntry principleAndInterestCreditJournalEntry = createJournalEntry(12, testContext.principleAndInterestAccount,
|
||||||
|
JournalEntryType.CREDIT);
|
||||||
|
JournalEntry feeAndPenaltyDebitJournalEntry = createJournalEntry(13, testContext.feeAndPenaltyAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry feeAndPenaltyCreditJournalEntry = createJournalEntry(14, testContext.feeAndPenaltyAccount, JournalEntryType.CREDIT);
|
||||||
|
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
|
||||||
|
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(false), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestCreditJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(false), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyCreditJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(true), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(true), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
|
||||||
|
|
||||||
|
// when
|
||||||
|
testContext.testSubject.createJournalEntriesForBuybackAssetTransfer(loan, transfer);
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(2))
|
||||||
|
.saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List.of(
|
||||||
|
ownerJournalEntryMapping(principleAndInterestCreditJournalEntry, previousOwner),
|
||||||
|
ownerJournalEntryMapping(feeAndPenaltyCreditJournalEntry, previousOwner));
|
||||||
|
assertNotNull(capturedOwnerJournalEntryMappings);
|
||||||
|
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
|
||||||
|
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(6))
|
||||||
|
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
|
||||||
|
transferJournalEntryMapping(principleAndInterestDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(feeAndPenaltyDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(principleAndInterestCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(feeAndPenaltyCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferDebitJournalEntry, transfer));
|
||||||
|
assertNotNull(capturedTransferJournalEntryMappings);
|
||||||
|
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createJournalEntriesForBuybackAssetTransferOfOverpaidLoan() {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
Loan loan = testContext.createMockedOverpaidLoan();
|
||||||
|
|
||||||
|
ExternalAssetOwner previousOwner = new ExternalAssetOwner();
|
||||||
|
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
|
||||||
|
transfer.setOwner(previousOwner);
|
||||||
|
|
||||||
|
JournalEntry overpaidDebitJournalEntry = createJournalEntry(11, testContext.overpaymentAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry overpaidCreditJournalEntry = createJournalEntry(12, testContext.overpaymentAccount, JournalEntryType.CREDIT);
|
||||||
|
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
|
||||||
|
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
|
||||||
|
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(false), eq(testContext.overpaymentAccount))).thenReturn(overpaidCreditJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
|
||||||
|
eq(true), eq(testContext.overpaymentAccount))).thenReturn(overpaidDebitJournalEntry);
|
||||||
|
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
|
||||||
|
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
|
||||||
|
|
||||||
|
// when
|
||||||
|
testContext.testSubject.createJournalEntriesForBuybackAssetTransfer(loan, transfer);
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(1))
|
||||||
|
.saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List
|
||||||
|
.of(ownerJournalEntryMapping(overpaidDebitJournalEntry, previousOwner));
|
||||||
|
assertNotNull(capturedOwnerJournalEntryMappings);
|
||||||
|
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
|
||||||
|
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
|
||||||
|
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(4))
|
||||||
|
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
|
||||||
|
.getAllValues();
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
|
||||||
|
transferJournalEntryMapping(overpaidDebitJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(overpaidCreditJournalEntry, transfer),
|
||||||
|
transferJournalEntryMapping(transferDebitJournalEntry, transfer));
|
||||||
|
assertNotNull(capturedTransferJournalEntryMappings);
|
||||||
|
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JournalEntry createJournalEntry(long id, GLAccount glAccount, JournalEntryType journalEntryType) {
|
||||||
|
JournalEntry journalEntry = JournalEntry.createNew(null, null, glAccount, null, null, false, null, journalEntryType, null, null,
|
||||||
|
null, null, null, null, null, null, null);
|
||||||
|
journalEntry.setId(id);
|
||||||
|
return journalEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwnerJournalEntryMapping ownerJournalEntryMapping(JournalEntry journalEntry, ExternalAssetOwner owner) {
|
||||||
|
ExternalAssetOwnerJournalEntryMapping ownerJournalEntryMapping = new ExternalAssetOwnerJournalEntryMapping();
|
||||||
|
ownerJournalEntryMapping.setJournalEntry(journalEntry);
|
||||||
|
ownerJournalEntryMapping.setOwner(owner);
|
||||||
|
return ownerJournalEntryMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOwnerJournalEntryMappings(List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings,
|
||||||
|
List<ExternalAssetOwnerJournalEntryMapping> actualOwnerJournalEntryMappings) {
|
||||||
|
assertEquals(expectedOwnerJournalEntryMappings.size(), actualOwnerJournalEntryMappings.size());
|
||||||
|
for (int i = 0; i < expectedOwnerJournalEntryMappings.size(); i++) {
|
||||||
|
assertOwnerJournalEntryMapping(expectedOwnerJournalEntryMappings.get(i), actualOwnerJournalEntryMappings.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOwnerJournalEntryMapping(ExternalAssetOwnerJournalEntryMapping expectedOwnerJournalEntryMapping,
|
||||||
|
ExternalAssetOwnerJournalEntryMapping actualOwnerJournalEntryMapping) {
|
||||||
|
assertNotNull(actualOwnerJournalEntryMapping);
|
||||||
|
assertSame(expectedOwnerJournalEntryMapping.getJournalEntry(), actualOwnerJournalEntryMapping.getJournalEntry());
|
||||||
|
assertSame(expectedOwnerJournalEntryMapping.getOwner(), actualOwnerJournalEntryMapping.getOwner());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalAssetOwnerTransferJournalEntryMapping transferJournalEntryMapping(JournalEntry journalEntry,
|
||||||
|
ExternalAssetOwnerTransfer transfer) {
|
||||||
|
ExternalAssetOwnerTransferJournalEntryMapping transferJournalEntryMapping = new ExternalAssetOwnerTransferJournalEntryMapping();
|
||||||
|
transferJournalEntryMapping.setJournalEntry(journalEntry);
|
||||||
|
transferJournalEntryMapping.setOwnerTransfer(transfer);
|
||||||
|
return transferJournalEntryMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertTransferJournalEntryMappings(
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings,
|
||||||
|
List<ExternalAssetOwnerTransferJournalEntryMapping> actualTransferJournalEntryMappings) {
|
||||||
|
assertEquals(expectedTransferJournalEntryMappings.size(), actualTransferJournalEntryMappings.size());
|
||||||
|
for (int i = 0; i < expectedTransferJournalEntryMappings.size(); i++) {
|
||||||
|
assertTransferJournalEntryMapping(expectedTransferJournalEntryMappings.get(i), actualTransferJournalEntryMappings.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertTransferJournalEntryMapping(ExternalAssetOwnerTransferJournalEntryMapping expectedTransferJournalEntryMapping,
|
||||||
|
ExternalAssetOwnerTransferJournalEntryMapping actualTransferJournalEntryMapping) {
|
||||||
|
assertNotNull(actualTransferJournalEntryMapping);
|
||||||
|
assertSame(expectedTransferJournalEntryMapping.getJournalEntry(), actualTransferJournalEntryMapping.getJournalEntry());
|
||||||
|
assertSame(expectedTransferJournalEntryMapping.getOwnerTransfer(), actualTransferJournalEntryMapping.getOwnerTransfer());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestContext {
|
||||||
|
|
||||||
|
private static final Long LOAN_ID = 3001L;
|
||||||
|
private static final Long LOAN_PRODUCT_ID = 3002L;
|
||||||
|
private static final Long TRANSFER_ID = 3003L;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private InvestorAccountingHelper investorAccountingHelper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExternalAssetOwnerTransferJournalEntryMappingRepository externalAssetOwnerTransferJournalEntryMappingRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private AccountingServiceImpl testSubject;
|
||||||
|
|
||||||
|
private final GLAccount principleAndInterestAccount = new GLAccount();
|
||||||
|
private final GLAccount feeAndPenaltyAccount = new GLAccount();
|
||||||
|
private final GLAccount transferAccount = new GLAccount();
|
||||||
|
private final GLAccount overpaymentAccount = new GLAccount();
|
||||||
|
|
||||||
|
TestContext() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
setupAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Loan createMockedLoan() {
|
||||||
|
Loan loan = mock(Loan.class);
|
||||||
|
when(loan.getId()).thenReturn(LOAN_ID);
|
||||||
|
when(loan.productId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
|
||||||
|
Office office = Office.headOffice("office", LocalDate.of(2024, 9, 27), new ExternalId("officeId"));
|
||||||
|
when(loan.getOffice()).thenReturn(office);
|
||||||
|
|
||||||
|
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
|
||||||
|
when(loanSummary.getTotalPrincipalOutstanding()).thenReturn(BigDecimal.ONE);
|
||||||
|
when(loanSummary.getTotalInterestOutstanding()).thenReturn(BigDecimal.ONE);
|
||||||
|
when(loanSummary.getTotalFeeChargesOutstanding()).thenReturn(BigDecimal.ONE);
|
||||||
|
when(loanSummary.getTotalPenaltyChargesOutstanding()).thenReturn(BigDecimal.ONE);
|
||||||
|
when(loan.getSummary()).thenReturn(loanSummary);
|
||||||
|
|
||||||
|
return loan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Loan createMockedOverpaidLoan() {
|
||||||
|
Loan loan = mock(Loan.class);
|
||||||
|
when(loan.getId()).thenReturn(LOAN_ID);
|
||||||
|
when(loan.productId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
when(loan.getStatus()).thenReturn(LoanStatus.OVERPAID);
|
||||||
|
|
||||||
|
Office office = Office.headOffice("office", LocalDate.of(2024, 9, 27), new ExternalId("officeId"));
|
||||||
|
when(loan.getOffice()).thenReturn(office);
|
||||||
|
|
||||||
|
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
|
||||||
|
when(loan.getSummary()).thenReturn(loanSummary);
|
||||||
|
|
||||||
|
when(loan.getTotalOverpaid()).thenReturn(BigDecimal.ONE);
|
||||||
|
|
||||||
|
return loan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupAccounts() {
|
||||||
|
principleAndInterestAccount.setId(1L);
|
||||||
|
feeAndPenaltyAccount.setId(2L);
|
||||||
|
transferAccount.setId(3L);
|
||||||
|
overpaymentAccount.setId(4L);
|
||||||
|
|
||||||
|
FinancialActivityAccount financialActivityAccount = new FinancialActivityAccount(transferAccount,
|
||||||
|
AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue());
|
||||||
|
when(financialActivityAccountRepository
|
||||||
|
.findByFinancialActivityTypeWithNotFoundDetection(AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue()))
|
||||||
|
.thenReturn(financialActivityAccount);
|
||||||
|
|
||||||
|
lenient().when(investorAccountingHelper.getLinkedGLAccountForLoanProduct(LOAN_PRODUCT_ID,
|
||||||
|
AccountingConstants.AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue())).thenReturn(principleAndInterestAccount);
|
||||||
|
lenient()
|
||||||
|
.when(investorAccountingHelper.getLinkedGLAccountForLoanProduct(LOAN_PRODUCT_ID,
|
||||||
|
AccountingConstants.AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue()))
|
||||||
|
.thenReturn(principleAndInterestAccount);
|
||||||
|
lenient().when(investorAccountingHelper.getLinkedGLAccountForLoanProduct(LOAN_PRODUCT_ID,
|
||||||
|
AccountingConstants.AccrualAccountsForLoan.FEES_RECEIVABLE.getValue())).thenReturn(feeAndPenaltyAccount);
|
||||||
|
lenient().when(investorAccountingHelper.getLinkedGLAccountForLoanProduct(LOAN_PRODUCT_ID,
|
||||||
|
AccountingConstants.AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue())).thenReturn(feeAndPenaltyAccount);
|
||||||
|
lenient().when(investorAccountingHelper.getLinkedGLAccountForLoanProduct(LOAN_PRODUCT_ID,
|
||||||
|
AccountingConstants.AccrualAccountsForLoan.OVERPAYMENT.getValue())).thenReturn(overpaymentAccount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
import static org.apache.fineract.investor.data.attribute.SettlementModelExternalAssetOwnerLoanProductAttribute.DEFAULT_SETTLEMENT;
|
||||||
|
import static org.apache.fineract.investor.data.attribute.SettlementModelExternalAssetOwnerLoanProductAttribute.DELAYED_SETTLEMENT;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.Page;
|
||||||
|
import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
|
public class DelayedSettlementAttributeServiceImplTest {
|
||||||
|
|
||||||
|
private static final Long LOAN_PRODUCT_ID = 1L;
|
||||||
|
|
||||||
|
private static Stream<Arguments> attributesDataProvider() {
|
||||||
|
ExternalTransferLoanProductAttributesData enabledAttributesData = new ExternalTransferLoanProductAttributesData();
|
||||||
|
enabledAttributesData.setAttributeValue(DELAYED_SETTLEMENT.getAttributeValue());
|
||||||
|
|
||||||
|
ExternalTransferLoanProductAttributesData disabledAttributesData = new ExternalTransferLoanProductAttributesData();
|
||||||
|
disabledAttributesData.setAttributeValue(DEFAULT_SETTLEMENT.getAttributeValue());
|
||||||
|
|
||||||
|
return Stream.of(Arguments.of(new Page(List.of(enabledAttributesData), 1), true),
|
||||||
|
Arguments.of(new Page(List.of(disabledAttributesData), 1), false), Arguments.of(new Page(List.of(), 0), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("attributesDataProvider")
|
||||||
|
void isEnabled(final Page<ExternalTransferLoanProductAttributesData> attributesDataPage, final boolean expectedResult) {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
when(testContext.externalAssetOwnerLoanProductAttributesReadService.retrieveAllLoanProductAttributesByLoanProductId(LOAN_PRODUCT_ID,
|
||||||
|
DELAYED_SETTLEMENT.getAttributeKey())).thenReturn(attributesDataPage);
|
||||||
|
|
||||||
|
// when
|
||||||
|
boolean result = testContext.testSubject.isEnabled(LOAN_PRODUCT_ID);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(expectedResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestContext {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExternalAssetOwnerLoanProductAttributesReadService externalAssetOwnerLoanProductAttributesReadService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private DelayedSettlementAttributeServiceImpl testSubject;
|
||||||
|
|
||||||
|
TestContext() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+932
@@ -0,0 +1,932 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
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.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
|
import org.apache.fineract.cob.data.LoanDataForExternalTransfer;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
|
import org.apache.fineract.investor.data.ExternalTransferRequestParameters;
|
||||||
|
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.ExternalAssetOwnerTransferRepository;
|
||||||
|
import org.apache.fineract.investor.exception.ExternalAssetOwnerInitiateTransferException;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class ExternalAssetOwnersWriteServiceTest {
|
||||||
|
|
||||||
|
final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault());
|
||||||
|
private static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BUSINESS_DATE, actualDate)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIntermediarySaleLoanByLoanIdHappyPath() {
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class)))
|
||||||
|
.thenReturn(Optional.of(testContext.externalAssetOwner));
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
|
||||||
|
|
||||||
|
// when
|
||||||
|
testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
|
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
|
||||||
|
assertAssertOwnerTransferValues(testContext, externalAssetOwnerTransferArgumentCaptor.getValue(),
|
||||||
|
ExternalTransferStatus.PENDING_INTERMEDIATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIntermediarySaleLoanByLoanIdDelayedSettlementIsNotEnabled() {
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
|
||||||
|
|
||||||
|
// when
|
||||||
|
ExternalAssetOwnerInitiateTransferException thrownException = Assert.assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command));
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository, times(0)).saveAndFlush(any(ExternalAssetOwnerTransfer.class));
|
||||||
|
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
Assertions.assertEquals(thrownException.getMessage(), "Delayed Settlement Configuration is not enabled for the loan product: "
|
||||||
|
+ testContext.loanDataForExternalTransfer.getLoanProductShortName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("effectiveTransferDataProviderIntermediarySaleTests")
|
||||||
|
public void testValidateEffectiveTransferForIntermediarySale(final String testName,
|
||||||
|
final List<ExternalAssetOwnerTransfer> externalAssetOwnerTransferList, final String expectedErrorString) {
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
|
||||||
|
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class))).thenReturn(externalAssetOwnerTransferList);
|
||||||
|
|
||||||
|
// when
|
||||||
|
ExternalAssetOwnerInitiateTransferException thrownException = Assert.assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command));
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository, times(0)).saveAndFlush(any(ExternalAssetOwnerTransfer.class));
|
||||||
|
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
Assertions.assertEquals(thrownException.getMessage(), expectedErrorString);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("loanStatusValidationDataProviderValidActive")
|
||||||
|
public void testValidateValidActiveLoanStatus(final String testName, final LoanStatus loanStatus) {
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class)))
|
||||||
|
.thenReturn(Optional.of(testContext.externalAssetOwner));
|
||||||
|
when(testContext.loanDataForExternalTransfer.getLoanStatus()).thenReturn(loanStatus.getValue());
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
|
||||||
|
|
||||||
|
// when
|
||||||
|
testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
|
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
|
||||||
|
assertAssertOwnerTransferValues(testContext, externalAssetOwnerTransferArgumentCaptor.getValue(),
|
||||||
|
ExternalTransferStatus.PENDING_INTERMEDIATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("loanStatusValidationDataProviderInvalidActive")
|
||||||
|
public void testValidateInvalidActiveLoanStatus(final String testName, final LoanStatus loanStatus) {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.loanDataForExternalTransfer.getLoanStatus()).thenReturn(loanStatus.getValue());
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
|
||||||
|
// when
|
||||||
|
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command));
|
||||||
|
|
||||||
|
assertEquals(exception.getMessage(), String.format("Loan status %s is not valid for transfer.", loanStatus.name()));
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
||||||
|
verify(testContext.loanRepository, times(1)).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository, times(1)).exists(any(Specification.class));
|
||||||
|
verifyNoInteractions(testContext.externalAssetOwnerRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("loanStatusValidationDataProviderValidDelayedSettlement")
|
||||||
|
public void testValidateValidDelayedSettlementLoanStatus(final String testName, final LoanStatus loanStatus) {
|
||||||
|
final ExternalAssetOwnerTransfer effectiveTransferForDelayedSettlement = new ExternalAssetOwnerTransfer();
|
||||||
|
effectiveTransferForDelayedSettlement.setStatus(ExternalTransferStatus.ACTIVE_INTERMEDIATE);
|
||||||
|
List<ExternalAssetOwnerTransfer> assetOwnerTransfers = new ArrayList<>();
|
||||||
|
assetOwnerTransfers.add(effectiveTransferForDelayedSettlement);
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
final ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.loanDataForExternalTransfer.getLoanStatus()).thenReturn(loanStatus.getValue());
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class))).thenReturn(assetOwnerTransfers);
|
||||||
|
|
||||||
|
// when
|
||||||
|
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
|
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class));
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer savedTransfer = externalAssetOwnerTransferArgumentCaptor.getValue();
|
||||||
|
assertAssertOwnerTransferValues(testContext, savedTransfer, ExternalTransferStatus.PENDING);
|
||||||
|
|
||||||
|
assertEquals(savedTransfer.getId(), result.getResourceId());
|
||||||
|
assertEquals(savedTransfer.getExternalId(), result.getResourceExternalId());
|
||||||
|
assertEquals(savedTransfer.getLoanId(), result.getSubResourceId());
|
||||||
|
assertEquals(savedTransfer.getExternalLoanId(), result.getSubResourceExternalId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("loanStatusValidationDataProviderInvalidDelayedSettlement")
|
||||||
|
public void testValidateInvalidDelayedSettlementLoanStatus(final String testName, final LoanStatus loanStatus) {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.loanDataForExternalTransfer.getLoanStatus()).thenReturn(loanStatus.getValue());
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
|
||||||
|
// when
|
||||||
|
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
||||||
|
|
||||||
|
assertEquals(exception.getMessage(), String.format("Loan status %s is not valid for transfer.", loanStatus.name()));
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
||||||
|
verify(testContext.loanRepository, times(1)).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository, times(1)).exists(any(Specification.class));
|
||||||
|
verifyNoInteractions(testContext.externalAssetOwnerRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("effectiveTransferDataProvider")
|
||||||
|
public void verifyWhenLoanSaleIsInitiatedThenAssetOwnerTransferIsCreated(
|
||||||
|
final List<ExternalAssetOwnerTransfer> externalAssetOwnerTransferList, final boolean isDelayedSettlementEnabled) {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
final ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(isDelayedSettlementEnabled);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class))).thenReturn(externalAssetOwnerTransferList);
|
||||||
|
|
||||||
|
// when
|
||||||
|
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
|
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class));
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer savedTransfer = externalAssetOwnerTransferArgumentCaptor.getValue();
|
||||||
|
assertAssertOwnerTransferValues(testContext, savedTransfer, ExternalTransferStatus.PENDING);
|
||||||
|
|
||||||
|
assertEquals(savedTransfer.getId(), result.getResourceId());
|
||||||
|
assertEquals(savedTransfer.getExternalId(), result.getResourceExternalId());
|
||||||
|
assertEquals(savedTransfer.getLoanId(), result.getSubResourceId());
|
||||||
|
assertEquals(savedTransfer.getExternalLoanId(), result.getSubResourceExternalId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateSettlementDateInThePastTest() {
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
final JsonElement jsonCommandElement = testContext.fromJsonHelper.parse(testContext.jsonCommand);
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
|
lenient().when(
|
||||||
|
testContext.fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, jsonCommandElement))
|
||||||
|
.thenReturn(LocalDate.EPOCH);
|
||||||
|
lenient().when(testContext.fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE,
|
||||||
|
jsonCommandElement, testContext.DATE_FORMAT, Locale.GERMANY)).thenReturn(LocalDate.EPOCH);
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
|
||||||
|
|
||||||
|
// when
|
||||||
|
ExternalAssetOwnerInitiateTransferException thrownException = Assert.assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command));
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository, times(0)).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
|
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
Assertions.assertEquals(thrownException.getMessage(), "Settlement date cannot be in the past");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> effectiveTransferDataProvider() {
|
||||||
|
final ExternalAssetOwnerTransfer effectiveTransferForDelayedSettlement = new ExternalAssetOwnerTransfer();
|
||||||
|
effectiveTransferForDelayedSettlement.setStatus(ExternalTransferStatus.ACTIVE_INTERMEDIATE);
|
||||||
|
|
||||||
|
return Stream.of(Arguments.of(Collections.emptyList(), false), Arguments.of(List.of(effectiveTransferForDelayedSettlement), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> effectiveTransferDataProviderIntermediarySaleTests() {
|
||||||
|
final ExternalAssetOwnerTransfer activeIntermediate = new ExternalAssetOwnerTransfer();
|
||||||
|
activeIntermediate.setStatus(ExternalTransferStatus.ACTIVE_INTERMEDIATE);
|
||||||
|
final ExternalAssetOwnerTransfer active = new ExternalAssetOwnerTransfer();
|
||||||
|
active.setStatus(ExternalTransferStatus.ACTIVE);
|
||||||
|
final ExternalAssetOwnerTransfer pendingIntermediate = new ExternalAssetOwnerTransfer();
|
||||||
|
pendingIntermediate.setStatus(ExternalTransferStatus.PENDING_INTERMEDIATE);
|
||||||
|
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of("Incorrect State", List.of(activeIntermediate),
|
||||||
|
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)",
|
||||||
|
activeIntermediate.getId())),
|
||||||
|
Arguments.of("Already In Progress", List.of(activeIntermediate, active),
|
||||||
|
"This loan cannot be sold, there is already an in progress transfer"),
|
||||||
|
Arguments.of("Already Pending Intermediary", List.of(pendingIntermediate),
|
||||||
|
"External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan"),
|
||||||
|
Arguments.of("Already Owned by External Asset Owner", List.of(active),
|
||||||
|
"This loan cannot be sold, because it is owned by an external asset owner"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> loanStatusValidationDataProviderValidActive() {
|
||||||
|
return Stream.of(Arguments.of("Active Loan Status", LoanStatus.ACTIVE),
|
||||||
|
Arguments.of("Transfer In Progress Loan Status", LoanStatus.TRANSFER_IN_PROGRESS),
|
||||||
|
Arguments.of("Transfer On Hold Loan Status", LoanStatus.TRANSFER_ON_HOLD));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> loanStatusValidationDataProviderValidDelayedSettlement() {
|
||||||
|
return Stream.of(Arguments.of("Active Loan Status", LoanStatus.ACTIVE),
|
||||||
|
Arguments.of("Transfer On Hold Loan Status", LoanStatus.TRANSFER_ON_HOLD),
|
||||||
|
Arguments.of("Transfer In Progress Status", LoanStatus.TRANSFER_IN_PROGRESS),
|
||||||
|
Arguments.of("Overpaid Loan Status", LoanStatus.OVERPAID),
|
||||||
|
Arguments.of("Closed Obligations Met Loan Status", LoanStatus.CLOSED_OBLIGATIONS_MET));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> loanStatusValidationDataProviderInvalidActive() {
|
||||||
|
return Stream.of(Arguments.of("Invalid Loan Status", LoanStatus.INVALID), Arguments.of("Overpaid Loan Status", LoanStatus.OVERPAID),
|
||||||
|
Arguments.of("Approved Loan Status", LoanStatus.APPROVED), Arguments.of("Rejected Loan Status", LoanStatus.REJECTED),
|
||||||
|
Arguments.of("Submitted and Pending Approval Loan Status", LoanStatus.SUBMITTED_AND_PENDING_APPROVAL),
|
||||||
|
Arguments.of("Withdrawn By Client Loan Status", LoanStatus.WITHDRAWN_BY_CLIENT),
|
||||||
|
Arguments.of("Closed Written Off Loan Status", LoanStatus.CLOSED_WRITTEN_OFF),
|
||||||
|
Arguments.of("Closed Reschedule Outstanding Amount Loan Status", LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT),
|
||||||
|
Arguments.of("Closed Obligations Met Loan Status", LoanStatus.CLOSED_OBLIGATIONS_MET));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> loanStatusValidationDataProviderInvalidDelayedSettlement() {
|
||||||
|
return Stream.of(Arguments.of("Invalid Loan Status", LoanStatus.INVALID), Arguments.of("Approved Loan Status", LoanStatus.APPROVED),
|
||||||
|
Arguments.of("Rejected Loan Status", LoanStatus.REJECTED),
|
||||||
|
Arguments.of("Submitted and Pending Approval Loan Status", LoanStatus.SUBMITTED_AND_PENDING_APPROVAL),
|
||||||
|
Arguments.of("Withdrawn By Client Loan Status", LoanStatus.WITHDRAWN_BY_CLIENT),
|
||||||
|
Arguments.of("Closed Written Off Loan Status", LoanStatus.CLOSED_WRITTEN_OFF),
|
||||||
|
Arguments.of("Closed Reschedule Outstanding Amount Loan Status", LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyWhenLoanSaleIsInitiatedWithoutOwnerThenAssetOwnerTransferIsCreated() {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
final ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
||||||
|
.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class))).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
// when
|
||||||
|
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
|
verify(testContext.externalAssetOwnerRepository).saveAndFlush(any(ExternalAssetOwner.class));
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class));
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer savedTransfer = externalAssetOwnerTransferArgumentCaptor.getValue();
|
||||||
|
assertAssertOwnerTransferValues(testContext, savedTransfer, ExternalTransferStatus.PENDING);
|
||||||
|
|
||||||
|
assertEquals(savedTransfer.getId(), result.getResourceId());
|
||||||
|
assertEquals(savedTransfer.getExternalId(), result.getResourceExternalId());
|
||||||
|
assertEquals(savedTransfer.getLoanId(), result.getSubResourceId());
|
||||||
|
assertEquals(savedTransfer.getExternalLoanId(), result.getSubResourceExternalId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyWhenLoanIsNotFoundThenExceptionIsThrown() {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
// when
|
||||||
|
assertThrows(LoanNotFoundException.class, () -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.fromApiJsonHelper, times(1)).parse(command.json());
|
||||||
|
verifyNoMoreInteractions(testContext.loanRepository);
|
||||||
|
verifyNoInteractions(testContext.externalAssetOwnerRepository, testContext.externalAssetOwnerTransferRepository,
|
||||||
|
testContext.delayedSettlementAttributeService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("invalidFieldsDataProvider")
|
||||||
|
public void verifyWhenFieldValueInvalidThenExceptionIsThrown(final String ownerExternalId, final String transferExternalId,
|
||||||
|
final String purchaseRatio, final LocalDate settlementDate) {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
final JsonElement jsonCommandElement = testContext.fromJsonHelper.parse(testContext.jsonCommand);
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, jsonCommandElement))
|
||||||
|
.thenReturn(ownerExternalId);
|
||||||
|
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, jsonCommandElement))
|
||||||
|
.thenReturn(transferExternalId);
|
||||||
|
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, jsonCommandElement))
|
||||||
|
.thenReturn(purchaseRatio);
|
||||||
|
when(testContext.fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, jsonCommandElement))
|
||||||
|
.thenReturn(settlementDate);
|
||||||
|
|
||||||
|
// when
|
||||||
|
assertThrows(PlatformApiDataValidationException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
||||||
|
verifyNoMoreInteractions(testContext.loanRepository);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
verifyNoInteractions(testContext.externalAssetOwnerRepository, testContext.externalAssetOwnerTransferRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> invalidFieldsDataProvider() {
|
||||||
|
// ownerExternalId, transferExternalId, purchaseRatio, settlementDate
|
||||||
|
return Stream.of(
|
||||||
|
// settlement date cannot be null
|
||||||
|
Arguments.of("value", "value", "value", null),
|
||||||
|
// purchaseRatio cannot be null
|
||||||
|
Arguments.of("value", "value", null, LocalDate.now(ZoneId.systemDefault()).plusDays(1)),
|
||||||
|
// purchaseRatio length cannot be > 50
|
||||||
|
Arguments.of("value", "value", RandomStringUtils.randomAlphanumeric(51), LocalDate.now(ZoneId.systemDefault()).plusDays(1)),
|
||||||
|
// transferExternalId length cannot be > 100
|
||||||
|
Arguments.of("value", RandomStringUtils.randomAlphanumeric(101), "value",
|
||||||
|
LocalDate.now(ZoneId.systemDefault()).plusDays(1)),
|
||||||
|
// ownerExternalId cannot be null
|
||||||
|
Arguments.of(null, "value", "value", LocalDate.now(ZoneId.systemDefault()).plusDays(1)),
|
||||||
|
// ownerExternalId length cannot be > 100
|
||||||
|
Arguments.of(RandomStringUtils.randomAlphanumeric(101), "value", "value",
|
||||||
|
LocalDate.now(ZoneId.systemDefault()).plusDays(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyWhenTransferExternalIdExistsThenExceptionIsThrown() {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerTransferRepository.exists(any(Specification.class))).thenReturn(true);
|
||||||
|
|
||||||
|
// when
|
||||||
|
assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
||||||
|
verify(testContext.loanRepository, times(1)).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository, times(1)).exists(any(Specification.class));
|
||||||
|
verifyNoInteractions(testContext.externalAssetOwnerRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("delayedSettlementFlagDataProvider")
|
||||||
|
public void verifyWhenTooManyEffectiveTransfersThenExceptionIsThrown(final boolean isDelayedSettlementEnabled) {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(isDelayedSettlementEnabled);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class))).thenReturn(List.of(new ExternalAssetOwnerTransfer(), new ExternalAssetOwnerTransfer()));
|
||||||
|
|
||||||
|
// when
|
||||||
|
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
||||||
|
|
||||||
|
assertEquals(exception.getMessage(), "This loan cannot be sold, there is already an in progress transfer");
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).exists(any(Specification.class));
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository, times(1)).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class));
|
||||||
|
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> delayedSettlementFlagDataProvider() {
|
||||||
|
return Stream.of(Arguments.of(false), Arguments.of(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyWhenNoEffectiveTransfersWithDelayedSettlementThenExceptionIsThrown() {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class))).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
// when
|
||||||
|
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
||||||
|
|
||||||
|
assertEquals(exception.getMessage(), "This loan cannot be sold, no effective transfer found.");
|
||||||
|
// then
|
||||||
|
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).exists(any(Specification.class));
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository, times(1)).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class));
|
||||||
|
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("invalidTransferStatusDataProvider")
|
||||||
|
public void verifyWhenInvalidTransferStatusThenExceptionIsThrown(final ExternalTransferStatus externalTransferStatus,
|
||||||
|
final boolean isDelayedSettlementEnabled, final String expectedExceptionMessage) {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
externalAssetOwnerTransfer.setStatus(externalTransferStatus);
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(isDelayedSettlementEnabled);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class))).thenReturn(List.of(externalAssetOwnerTransfer));
|
||||||
|
|
||||||
|
// when
|
||||||
|
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
||||||
|
|
||||||
|
assertEquals(exception.getMessage(), expectedExceptionMessage);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
||||||
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).exists(any(Specification.class));
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository, times(1)).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
|
any(LocalDate.class));
|
||||||
|
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> invalidTransferStatusDataProvider() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of(ExternalTransferStatus.PENDING, false,
|
||||||
|
"External asset owner transfer is already in PENDING state for this loan"),
|
||||||
|
Arguments.of(ExternalTransferStatus.ACTIVE, false,
|
||||||
|
"This loan cannot be sold, because it is owned by an external asset owner"),
|
||||||
|
Arguments.of(ExternalTransferStatus.PENDING_INTERMEDIATE, true,
|
||||||
|
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
|
||||||
|
Arguments.of(ExternalTransferStatus.ACTIVE, true,
|
||||||
|
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
|
||||||
|
Arguments.of(ExternalTransferStatus.DECLINED, true,
|
||||||
|
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
|
||||||
|
Arguments.of(ExternalTransferStatus.PENDING, true,
|
||||||
|
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
|
||||||
|
Arguments.of(ExternalTransferStatus.BUYBACK, true,
|
||||||
|
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
|
||||||
|
Arguments.of(ExternalTransferStatus.CANCELLED, true,
|
||||||
|
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("buybackValidationWithDelaySettlementSuccessfulDataProvider")
|
||||||
|
void buybackLoanByLoanIdWhenDelaySettlementEnabledSuccess(final List<ExternalTransferStatus> transferStatuses,
|
||||||
|
final ExternalTransferStatus expectedStatus) {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerTransfer> transfers = transferStatuses.stream()
|
||||||
|
.map(transferStatus -> createExternalAssetOwnerTransfer(testContext, transferStatus)).toList();
|
||||||
|
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(testContext.loanId, actualDate))
|
||||||
|
.thenReturn(transfers);
|
||||||
|
|
||||||
|
// when
|
||||||
|
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.buybackLoanByLoanId(command);
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<ExternalAssetOwnerTransfer> savedTransferCaptor = ArgumentCaptor.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(savedTransferCaptor.capture());
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer savedTransfer = savedTransferCaptor.getValue();
|
||||||
|
assertNotNull(savedTransfer);
|
||||||
|
assertFalse(transfers.isEmpty());
|
||||||
|
ExternalAssetOwnerTransfer expectedEffectiveTransfer = transfers.get(0);
|
||||||
|
assertEquals(testContext.transferExternalId, savedTransfer.getExternalId().getValue());
|
||||||
|
assertEquals(expectedEffectiveTransfer.getOwner(), savedTransfer.getOwner());
|
||||||
|
assertEquals(expectedStatus, savedTransfer.getStatus());
|
||||||
|
assertEquals(expectedEffectiveTransfer.getLoanId(), savedTransfer.getLoanId());
|
||||||
|
assertEquals(expectedEffectiveTransfer.getExternalLoanId(), savedTransfer.getExternalLoanId());
|
||||||
|
assertEquals(testContext.settlementDate, savedTransfer.getSettlementDate());
|
||||||
|
assertEquals(actualDate, savedTransfer.getEffectiveDateFrom());
|
||||||
|
assertEquals(FUTURE_DATE_9999_12_31, savedTransfer.getEffectiveDateTo());
|
||||||
|
assertEquals(expectedEffectiveTransfer.getPurchasePriceRatio(), savedTransfer.getPurchasePriceRatio());
|
||||||
|
|
||||||
|
assertEquals(savedTransfer.getId(), result.getResourceId());
|
||||||
|
assertEquals(savedTransfer.getExternalId(), result.getResourceExternalId());
|
||||||
|
assertEquals(savedTransfer.getLoanId(), result.getSubResourceId());
|
||||||
|
assertEquals(savedTransfer.getExternalLoanId(), result.getSubResourceExternalId());
|
||||||
|
|
||||||
|
verifyNoInteractions(testContext.externalAssetOwnerRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> buybackValidationWithDelaySettlementSuccessfulDataProvider() {
|
||||||
|
return Stream.of(Arguments.of(List.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE), ExternalTransferStatus.BUYBACK_INTERMEDIATE),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.ACTIVE), ExternalTransferStatus.BUYBACK));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("buybackValidationWithDelaySettlementFailureDataProvider")
|
||||||
|
void buybackLoanByLoanIdWhenDelaySettlementEnabledFailure(final List<ExternalTransferStatus> transferStatuses,
|
||||||
|
final String expectedExceptionMessage) {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerTransfer> transfers = transferStatuses.stream()
|
||||||
|
.map(transferStatus -> createExternalAssetOwnerTransfer(testContext, transferStatus)).toList();
|
||||||
|
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(testContext.loanId, actualDate))
|
||||||
|
.thenReturn(transfers);
|
||||||
|
|
||||||
|
// when
|
||||||
|
ExternalAssetOwnerInitiateTransferException actualException = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
|
() -> testContext.externalAssetOwnersWriteServiceImpl.buybackLoanByLoanId(command));
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(expectedExceptionMessage, actualException.getMessage());
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository, never()).saveAndFlush(any());
|
||||||
|
verifyNoInteractions(testContext.externalAssetOwnerRepository);
|
||||||
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> buybackValidationWithDelaySettlementFailureDataProvider() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of(Collections.emptyList(), "This loan cannot be bought back, it is not owned by an external asset owner"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.PENDING_INTERMEDIATE),
|
||||||
|
"This loan cannot be bought back, effective transfer is not in right state: PENDING_INTERMEDIATE"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.PENDING),
|
||||||
|
"This loan cannot be bought back, effective transfer is not in right state: PENDING"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.DECLINED),
|
||||||
|
"This loan cannot be bought back, effective transfer is not in right state: DECLINED"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.BUYBACK),
|
||||||
|
"This loan cannot be bought back, effective transfer is not in right state: BUYBACK"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE),
|
||||||
|
"This loan cannot be bought back, effective transfer is not in right state: BUYBACK_INTERMEDIATE"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.CANCELLED),
|
||||||
|
"This loan cannot be bought back, effective transfer is not in right state: CANCELLED"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE, ExternalTransferStatus.PENDING),
|
||||||
|
"This loan cannot be bought back, external asset owner sale is pending"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.ACTIVE_INTERMEDIATE),
|
||||||
|
"This loan cannot be bought back, external asset owner sale is pending"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE, ExternalTransferStatus.BUYBACK_INTERMEDIATE),
|
||||||
|
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE, ExternalTransferStatus.ACTIVE_INTERMEDIATE),
|
||||||
|
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.ACTIVE, ExternalTransferStatus.BUYBACK),
|
||||||
|
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"),
|
||||||
|
Arguments.of(List.of(ExternalTransferStatus.BUYBACK, ExternalTransferStatus.ACTIVE),
|
||||||
|
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create {@link JsonCommand} object from json command string.
|
||||||
|
*
|
||||||
|
* @param jsonCommand
|
||||||
|
* the json command string
|
||||||
|
* @param loanId
|
||||||
|
* the loan id
|
||||||
|
* @return the {@link JsonCommand} object.
|
||||||
|
*/
|
||||||
|
private JsonCommand createJsonCommand(final String jsonCommand, final Long loanId) {
|
||||||
|
return new JsonCommand(null, jsonCommand, null, null, null, null, null, null, null, loanId, null, null, null, null, null, null,
|
||||||
|
null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create {@link ExternalAssetOwnerTransfer} object.
|
||||||
|
*
|
||||||
|
* @param status
|
||||||
|
* the {@link ExternalTransferStatus}
|
||||||
|
* @return the {@link ExternalAssetOwnerTransfer} object.
|
||||||
|
*/
|
||||||
|
private ExternalAssetOwnerTransfer createExternalAssetOwnerTransfer(final TestContext testContext,
|
||||||
|
final ExternalTransferStatus status) {
|
||||||
|
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
|
||||||
|
transfer.setExternalId(new ExternalId(RandomStringUtils.randomAlphanumeric(10)));
|
||||||
|
transfer.setOwner(new ExternalAssetOwner());
|
||||||
|
transfer.setStatus(status);
|
||||||
|
transfer.setLoanId(testContext.loanId);
|
||||||
|
transfer.setExternalLoanId(new ExternalId(testContext.externalLoanId));
|
||||||
|
transfer.setExternalGroupId(new ExternalId(testContext.externalLoanId));
|
||||||
|
transfer.setPurchasePriceRatio(TestContext.PURCHASE_RATIO.toString());
|
||||||
|
|
||||||
|
return transfer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts on the {@link ExternalAssetOwnerTransfer} object values against test values.
|
||||||
|
*
|
||||||
|
* @param testContext
|
||||||
|
* the test context with expected values.
|
||||||
|
* @param externalAssetOwnerTransfer
|
||||||
|
* the {@link ExternalAssetOwnerTransfer} object.
|
||||||
|
* @param expectedTransferStatus
|
||||||
|
* the expected transfer status.
|
||||||
|
*/
|
||||||
|
private void assertAssertOwnerTransferValues(final TestContext testContext, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer,
|
||||||
|
final ExternalTransferStatus expectedTransferStatus) {
|
||||||
|
assertEquals(testContext.loanId, externalAssetOwnerTransfer.getLoanId());
|
||||||
|
assertEquals(testContext.externalLoanId, externalAssetOwnerTransfer.getExternalLoanId().getValue());
|
||||||
|
assertEquals(testContext.ownerExternalId, externalAssetOwnerTransfer.getOwner().getExternalId().getValue());
|
||||||
|
assertEquals(testContext.transferExternalId, externalAssetOwnerTransfer.getExternalId().getValue());
|
||||||
|
assertEquals(testContext.transferExternalGroupId, externalAssetOwnerTransfer.getExternalGroupId().getValue());
|
||||||
|
assertEquals(expectedTransferStatus, externalAssetOwnerTransfer.getStatus());
|
||||||
|
assertEquals(TestContext.PURCHASE_RATIO.toString(), externalAssetOwnerTransfer.getPurchasePriceRatio());
|
||||||
|
assertEquals(testContext.settlementDate, externalAssetOwnerTransfer.getSettlementDate());
|
||||||
|
assertEquals(actualDate, externalAssetOwnerTransfer.getEffectiveDateFrom());
|
||||||
|
assertEquals(FUTURE_DATE_9999_12_31, externalAssetOwnerTransfer.getEffectiveDateTo());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressFBWarnings({ "VA_FORMAT_STRING_USES_NEWLINE" })
|
||||||
|
static class TestContext {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExternalAssetOwnerRepository externalAssetOwnerRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FromJsonHelper fromApiJsonHelper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LoanRepository loanRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DelayedSettlementAttributeService delayedSettlementAttributeService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LoanDataForExternalTransfer loanDataForExternalTransfer;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ExternalAssetOwnersWriteServiceImpl externalAssetOwnersWriteServiceImpl;
|
||||||
|
|
||||||
|
private static final BigDecimal PURCHASE_RATIO = BigDecimal.valueOf(Float.parseFloat(RandomStringUtils.randomNumeric(1, 3)) / 100)
|
||||||
|
.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
private static final String DATE_FORMAT = "yyyy-MM-dd";
|
||||||
|
private static final String LOCALE = "de_DE";
|
||||||
|
|
||||||
|
private final FromJsonHelper fromJsonHelper = new FromJsonHelper();
|
||||||
|
private final ExternalAssetOwner externalAssetOwner = new ExternalAssetOwner();
|
||||||
|
private final Long loanId = Long.valueOf(RandomStringUtils.randomNumeric(2));
|
||||||
|
private final String externalLoanId = RandomStringUtils.randomAlphanumeric(10);
|
||||||
|
private final Long loanProductId = Long.valueOf(RandomStringUtils.randomNumeric(2));
|
||||||
|
private final String loanProductShortName = RandomStringUtils.randomAlphanumeric(10);
|
||||||
|
private final String ownerExternalId = RandomStringUtils.randomAlphanumeric(10);
|
||||||
|
private final String transferExternalId = RandomStringUtils.randomAlphanumeric(10);
|
||||||
|
private final String transferExternalGroupId = RandomStringUtils.randomAlphanumeric(10);
|
||||||
|
private final LocalDate settlementDate = LocalDate.parse("9999-08-22");
|
||||||
|
private final String jsonCommand = String.format("""
|
||||||
|
{
|
||||||
|
"settlementDate": "%s",
|
||||||
|
"ownerExternalId": "%s",
|
||||||
|
"transferExternalId": "%s",
|
||||||
|
"transferExternalGroupId": "%s",
|
||||||
|
"purchasePriceRatio": "%s",
|
||||||
|
"dateFormat": "%s",
|
||||||
|
"locale": "%s"
|
||||||
|
}
|
||||||
|
""", settlementDate, ownerExternalId, transferExternalId, transferExternalGroupId, PURCHASE_RATIO, DATE_FORMAT, LOCALE);
|
||||||
|
|
||||||
|
@SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
|
||||||
|
TestContext() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
final JsonElement jsonCommandElement = fromJsonHelper.parse(jsonCommand);
|
||||||
|
externalAssetOwner.setExternalId(new ExternalId(ownerExternalId));
|
||||||
|
|
||||||
|
lenient().when(fromApiJsonHelper.parse(anyString())).thenReturn(jsonCommandElement);
|
||||||
|
lenient().when(fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, jsonCommandElement))
|
||||||
|
.thenReturn(ownerExternalId);
|
||||||
|
lenient().when(fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, jsonCommandElement))
|
||||||
|
.thenReturn(transferExternalId);
|
||||||
|
lenient().when(
|
||||||
|
fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_GROUP_ID, jsonCommandElement))
|
||||||
|
.thenReturn(transferExternalGroupId);
|
||||||
|
lenient().when(fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, jsonCommandElement))
|
||||||
|
.thenReturn(PURCHASE_RATIO.toString());
|
||||||
|
lenient().when(fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.DATEFORMAT, jsonCommandElement))
|
||||||
|
.thenReturn(DATE_FORMAT);
|
||||||
|
lenient().when(fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.LOCALE, jsonCommandElement))
|
||||||
|
.thenReturn(LOCALE);
|
||||||
|
lenient().when(fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, jsonCommandElement))
|
||||||
|
.thenReturn(settlementDate);
|
||||||
|
lenient().when(fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, jsonCommandElement,
|
||||||
|
DATE_FORMAT, Locale.GERMANY)).thenReturn(settlementDate);
|
||||||
|
|
||||||
|
lenient().when(externalAssetOwnerRepository.findByExternalId(any(ExternalId.class)))
|
||||||
|
.thenReturn(Optional.of(externalAssetOwner));
|
||||||
|
lenient().when(externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(loanId), any(LocalDate.class)))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
lenient().when(externalAssetOwnerRepository.saveAndFlush(any(ExternalAssetOwner.class))).thenReturn(externalAssetOwner);
|
||||||
|
|
||||||
|
lenient().when(loanDataForExternalTransfer.getId()).thenReturn(loanId);
|
||||||
|
lenient().when(loanDataForExternalTransfer.getExternalId()).thenReturn(new ExternalId(externalLoanId));
|
||||||
|
lenient().when(loanDataForExternalTransfer.getLoanStatus()).thenReturn(LoanStatus.ACTIVE.getValue());
|
||||||
|
lenient().when(loanDataForExternalTransfer.getLoanProductId()).thenReturn(loanProductId);
|
||||||
|
lenient().when(loanDataForExternalTransfer.getLoanProductShortName()).thenReturn(loanProductShortName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
-11
@@ -19,8 +19,6 @@
|
|||||||
package org.apache.fineract.investor.service;
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
|
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
|
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
@@ -36,10 +34,12 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
|
import org.apache.fineract.investor.data.ExternalTransferStatus;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
|
||||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
|
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
|
||||||
@@ -50,6 +50,9 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
@@ -80,38 +83,88 @@ public class LoanAccountOwnerTransferServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void verifyWhenCancelPendingSaleAndBuybackTransferThenBusinessEventsAreSent() {
|
public void verifyWhenCancelPendingAndIntermediateSaleAndBuybackTransferThenBusinessEventsAreSent() {
|
||||||
// given
|
// given
|
||||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
|
|
||||||
ExternalAssetOwnerTransfer pendingSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer pendingSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
ExternalAssetOwnerTransfer pendingIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
ExternalAssetOwnerTransfer pendingBuyBackIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
ExternalAssetOwnerTransfer cancelledSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer cancelledSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
ExternalAssetOwnerTransfer cancelledBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer cancelledBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
ExternalAssetOwnerTransfer cancelledIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
ExternalAssetOwnerTransfer cancelledBuyBackIntermediateSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
List<ExternalAssetOwnerTransfer> response = List.of(pendingSaleTransfer, pendingBuybackTransfer);
|
List<ExternalAssetOwnerTransfer> response = List.of(pendingSaleTransfer, pendingBuybackTransfer, pendingIntermediateTransfer,
|
||||||
|
pendingBuyBackIntermediateTransfer);
|
||||||
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(ASC, "id")))).thenReturn(response);
|
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(ASC, "id")))).thenReturn(response);
|
||||||
when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(pendingSaleTransfer)
|
when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(pendingSaleTransfer)
|
||||||
.thenReturn(cancelledSaleTransfer).thenReturn(pendingBuybackTransfer).thenReturn(cancelledBuybackTransfer);
|
.thenReturn(cancelledSaleTransfer).thenReturn(pendingBuybackTransfer).thenReturn(cancelledBuybackTransfer)
|
||||||
|
.thenReturn(pendingIntermediateTransfer).thenReturn(cancelledIntermediateTransfer)
|
||||||
|
.thenReturn(pendingBuyBackIntermediateTransfer).thenReturn(cancelledBuyBackIntermediateSaleTransfer);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
underTest.handleLoanClosedOrOverpaid(loanForProcessing);
|
underTest.handleLoanClosedOrOverpaid(loanForProcessing);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(4);
|
||||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, cancelledSaleTransfer);
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, cancelledSaleTransfer);
|
||||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, cancelledBuybackTransfer);
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, cancelledBuybackTransfer);
|
||||||
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 2, loanForProcessing, cancelledIntermediateTransfer);
|
||||||
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 3, loanForProcessing, cancelledBuyBackIntermediateSaleTransfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void verifyWhenDeclinePendingSaleTransferThenBusinessEventIsSent() {
|
public void verifyWhenDeclineCancelPendingAndIntermediateSaleAndBuybackTransferThenBusinessEventsAreSent() {
|
||||||
// given
|
// given
|
||||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
|
|
||||||
ExternalAssetOwnerTransfer pendingSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer pendingSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
when(pendingSaleTransfer.getStatus()).thenReturn(PENDING);
|
when(pendingSaleTransfer.getSettlementDate()).thenReturn(actualDate.minusDays(1));
|
||||||
|
ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
ExternalAssetOwnerTransfer pendingIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
ExternalAssetOwnerTransfer pendingBuyBackIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
|
when(pendingSaleTransfer.getSettlementDate()).thenReturn(actualDate.minusDays(1));
|
||||||
|
when(pendingBuybackTransfer.getSettlementDate()).thenReturn(actualDate.minusDays(1));
|
||||||
|
when(pendingIntermediateTransfer.getSettlementDate()).thenReturn(actualDate);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer declinedSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
ExternalAssetOwnerTransfer cancelledBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
ExternalAssetOwnerTransfer cancelledIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
ExternalAssetOwnerTransfer cancelledBuyBackIntermediateSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
|
List<ExternalAssetOwnerTransfer> response = List.of(pendingSaleTransfer, pendingBuybackTransfer, pendingIntermediateTransfer,
|
||||||
|
pendingBuyBackIntermediateTransfer);
|
||||||
|
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(ASC, "id")))).thenReturn(response);
|
||||||
|
when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(pendingSaleTransfer)
|
||||||
|
.thenReturn(declinedSaleTransfer).thenReturn(pendingBuybackTransfer).thenReturn(cancelledBuybackTransfer)
|
||||||
|
.thenReturn(pendingIntermediateTransfer).thenReturn(cancelledIntermediateTransfer)
|
||||||
|
.thenReturn(pendingBuyBackIntermediateTransfer).thenReturn(cancelledBuyBackIntermediateSaleTransfer);
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.handleLoanClosedOrOverpaid(loanForProcessing);
|
||||||
|
|
||||||
|
// then
|
||||||
|
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(4);
|
||||||
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, pendingSaleTransfer);
|
||||||
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, cancelledBuybackTransfer);
|
||||||
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 2, loanForProcessing, cancelledIntermediateTransfer);
|
||||||
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 3, loanForProcessing, cancelledBuyBackIntermediateSaleTransfer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("pendingStatusDataProvider")
|
||||||
|
public void verifyWhenDeclinePendingSaleTransferThenBusinessEventIsSent(final ExternalTransferStatus pendingStatus) {
|
||||||
|
// given
|
||||||
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer pendingSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
|
when(pendingSaleTransfer.getStatus()).thenReturn(pendingStatus);
|
||||||
ExternalAssetOwnerTransfer declineTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer declineTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
List<ExternalAssetOwnerTransfer> response = List.of(pendingSaleTransfer);
|
List<ExternalAssetOwnerTransfer> response = List.of(pendingSaleTransfer);
|
||||||
|
|
||||||
@@ -127,8 +180,9 @@ public class LoanAccountOwnerTransferServiceTest {
|
|||||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, declineTransfer);
|
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, declineTransfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
public void verifyWhenExecutePendingBuybackTransferThenBusinessEventIsSent() {
|
@MethodSource("buybackStatusDataProvider")
|
||||||
|
public void verifyWhenExecutePendingBuybackTransferThenBusinessEventIsSent(final ExternalTransferStatus buybackStatus) {
|
||||||
// given
|
// given
|
||||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
@@ -136,7 +190,7 @@ public class LoanAccountOwnerTransferServiceTest {
|
|||||||
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
||||||
|
|
||||||
ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
when(pendingBuybackTransfer.getStatus()).thenReturn(BUYBACK);
|
when(pendingBuybackTransfer.getStatus()).thenReturn(buybackStatus);
|
||||||
ExternalAssetOwnerTransfer activeTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
ExternalAssetOwnerTransfer activeTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||||
List<ExternalAssetOwnerTransfer> response = List.of(pendingBuybackTransfer);
|
List<ExternalAssetOwnerTransfer> response = List.of(pendingBuybackTransfer);
|
||||||
|
|
||||||
@@ -154,6 +208,14 @@ public class LoanAccountOwnerTransferServiceTest {
|
|||||||
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
|
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> pendingStatusDataProvider() {
|
||||||
|
return Stream.of(Arguments.of(ExternalTransferStatus.PENDING_INTERMEDIATE), Arguments.of(ExternalTransferStatus.PENDING));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> buybackStatusDataProvider() {
|
||||||
|
return Stream.of(Arguments.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE), Arguments.of(ExternalTransferStatus.BUYBACK));
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private ArgumentCaptor<BusinessEvent<?>> verifyBusinessEvents(int expectedBusinessEvents) {
|
private ArgumentCaptor<BusinessEvent<?>> verifyBusinessEvents(int expectedBusinessEvents) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|||||||
+163
@@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.investor.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
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.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class LoanTransferabilityServiceImplTest {
|
||||||
|
|
||||||
|
private static final Long LOAN_PRODUCT_ID = 1L;
|
||||||
|
|
||||||
|
private static Stream<Arguments> amountDataProvider() {
|
||||||
|
return Stream.of(Arguments.of(BigDecimal.ONE, true), Arguments.of(BigDecimal.ZERO, false),
|
||||||
|
Arguments.of(BigDecimal.ONE.negate(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("amountDataProvider")
|
||||||
|
void isTransferableWhenDelayedSettlementDisabled(final BigDecimal loanOutstandingAmount, final boolean expectedResult) {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
|
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
|
||||||
|
when(loanSummary.getTotalOutstanding()).thenReturn(loanOutstandingAmount);
|
||||||
|
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
when(loan.getLoanProduct()).thenReturn(loanProduct);
|
||||||
|
when(loan.getSummary()).thenReturn(loanSummary);
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(false);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.PENDING);
|
||||||
|
|
||||||
|
// when
|
||||||
|
boolean result = testContext.testSubject.isTransferable(loan, externalAssetOwnerTransfer);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(expectedResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("amountDataProvider")
|
||||||
|
void isTransferableWhenDelayedSettlementEnabledAndSellingToIntermediate(final BigDecimal loanOutstandingAmount,
|
||||||
|
final boolean expectedResult) {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
|
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
|
||||||
|
when(loanSummary.getTotalOutstanding()).thenReturn(loanOutstandingAmount);
|
||||||
|
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
when(loan.getLoanProduct()).thenReturn(loanProduct);
|
||||||
|
when(loan.getSummary()).thenReturn(loanSummary);
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.PENDING_INTERMEDIATE);
|
||||||
|
|
||||||
|
// when
|
||||||
|
boolean result = testContext.testSubject.isTransferable(loan, externalAssetOwnerTransfer);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(expectedResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isTransferableWhenDelayedSettlementEnabledAndSellingToInvestor() {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
|
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
|
||||||
|
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
when(loan.getLoanProduct()).thenReturn(loanProduct);
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||||
|
externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.PENDING);
|
||||||
|
|
||||||
|
// when
|
||||||
|
boolean result = testContext.testSubject.isTransferable(loan, externalAssetOwnerTransfer);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> declinedSubStatusDataProvider() {
|
||||||
|
return Stream.of(Arguments.of(BigDecimal.ONE, ExternalTransferSubStatus.BALANCE_NEGATIVE),
|
||||||
|
Arguments.of(BigDecimal.ZERO, ExternalTransferSubStatus.BALANCE_ZERO),
|
||||||
|
Arguments.of(null, ExternalTransferSubStatus.BALANCE_ZERO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("declinedSubStatusDataProvider")
|
||||||
|
void getDeclinedSubStatus(final BigDecimal totalOverpaidAmount, final ExternalTransferSubStatus expectedSubStatus) {
|
||||||
|
// given
|
||||||
|
TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
when(loan.getTotalOverpaid()).thenReturn(totalOverpaidAmount);
|
||||||
|
|
||||||
|
// when
|
||||||
|
ExternalTransferSubStatus result = testContext.testSubject.getDeclinedSubStatus(loan);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(expectedSubStatus, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestContext {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DelayedSettlementAttributeService delayedSettlementAttributeService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private LoanTransferabilityServiceImpl testSubject;
|
||||||
|
|
||||||
|
TestContext() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
-8
@@ -19,9 +19,13 @@
|
|||||||
package org.apache.fineract.investor.service.serialization.serializer.investor;
|
package org.apache.fineract.investor.service.serialization.serializer.investor;
|
||||||
|
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE_INTERMEDIATE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK_INTERMEDIATE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.CANCELLED;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.CANCELLED;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferStatus.DECLINED;
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.DECLINED;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
|
||||||
|
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING_INTERMEDIATE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_NEGATIVE;
|
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_NEGATIVE;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_ZERO;
|
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_ZERO;
|
||||||
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.SAMEDAY_TRANSFERS;
|
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.SAMEDAY_TRANSFERS;
|
||||||
@@ -67,35 +71,61 @@ public class InvestorBusinessEventSerializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSerializationSellOK() {
|
public void testSerializationSellOK() {
|
||||||
doTest(ACTIVE, null, "SALE", "EXECUTED", null);
|
doTest(ACTIVE, null, null, "SALE", "EXECUTED", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSerializationBuybackOK() {
|
public void testSerializationBuybackOK() {
|
||||||
doTest(BUYBACK, null, "BUYBACK", "EXECUTED", null);
|
doTest(BUYBACK, null, null, "BUYBACK", "EXECUTED", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializationIntermediarySaleOK() {
|
||||||
|
doTest(ACTIVE_INTERMEDIATE, null, null, "INTERMEDIARYSALE", "EXECUTED", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializationBuybackIntermediateOK() {
|
||||||
|
doTest(BUYBACK_INTERMEDIATE, null, null, "BUYBACK", "EXECUTED", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSerializationDeclinedNegativeBalance() {
|
public void testSerializationDeclinedNegativeBalance() {
|
||||||
doTest(DECLINED, BALANCE_NEGATIVE, "SALE", "DECLINED", "BALANCE_NEGATIVE");
|
doTest(DECLINED, BALANCE_NEGATIVE, PENDING, "SALE", "DECLINED", "BALANCE_NEGATIVE");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSerializationDeclinedBalanceZero() {
|
public void testSerializationDeclinedBalanceZero() {
|
||||||
doTest(DECLINED, BALANCE_ZERO, "SALE", "DECLINED", "BALANCE_ZERO");
|
doTest(DECLINED, BALANCE_ZERO, PENDING, "SALE", "DECLINED", "BALANCE_ZERO");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializationDeclinedPendingIntermediateNegativeBalance() {
|
||||||
|
doTest(DECLINED, BALANCE_NEGATIVE, PENDING_INTERMEDIATE, "INTERMEDIARYSALE", "DECLINED", "BALANCE_NEGATIVE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializationDeclinedPendingIntermediateBalanceZero() {
|
||||||
|
doTest(DECLINED, BALANCE_ZERO, PENDING_INTERMEDIATE, "INTERMEDIARYSALE", "DECLINED", "BALANCE_ZERO");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSerializationCancelledSameDayTransfer() {
|
public void testSerializationCancelledSameDayTransfer() {
|
||||||
doTest(CANCELLED, SAMEDAY_TRANSFERS, "SALE", "CANCELLED", "SAMEDAY_TRANSFERS");
|
doTest(CANCELLED, SAMEDAY_TRANSFERS, PENDING, "SALE", "CANCELLED", "SAMEDAY_TRANSFERS");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doTest(ExternalTransferStatus status, ExternalTransferSubStatus subStatus, String expectedType, String expectedStatus,
|
@Test
|
||||||
String expectedReason) {
|
public void testSerializationCancelledPendingIntermediateSameDayTransfer() {
|
||||||
|
doTest(CANCELLED, SAMEDAY_TRANSFERS, PENDING_INTERMEDIATE, "INTERMEDIARYSALE", "CANCELLED", "SAMEDAY_TRANSFERS");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doTest(ExternalTransferStatus status, ExternalTransferSubStatus subStatus, ExternalTransferStatus firstTransferStatus,
|
||||||
|
String expectedType, String expectedStatus, String expectedReason) {
|
||||||
// given
|
// given
|
||||||
ExternalAssetOwnersReadService mockReadService = Mockito.mock(ExternalAssetOwnersReadService.class);
|
ExternalAssetOwnersReadService mockReadService = Mockito.mock(ExternalAssetOwnersReadService.class);
|
||||||
when(mockReadService.retrieveTransferData(123L)).thenReturn(createTransferData(status, subStatus));
|
when(mockReadService.retrieveTransferData(123L)).thenReturn(createTransferData(status, subStatus));
|
||||||
when(mockReadService.retrieveFirstTransferByExternalId(any(ExternalId.class))).thenReturn(createTransferData(ACTIVE, null));
|
when(mockReadService.retrieveFirstTransferByExternalId(any(ExternalId.class)))
|
||||||
|
.thenReturn(createTransferData(firstTransferStatus, null));
|
||||||
Loan loan = Mockito.mock(Loan.class);
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
when(loan.getCurrency()).thenReturn(new MonetaryCurrency("EUR", 2, 1));
|
when(loan.getCurrency()).thenReturn(new MonetaryCurrency("EUR", 2, 1));
|
||||||
List<LoanCharge> loanCharges = createMockCharges();
|
List<LoanCharge> loanCharges = createMockCharges();
|
||||||
|
|||||||
+3
-1
@@ -24,9 +24,11 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
|||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
public class LoanIdAndExternalIdAndStatus {
|
public class LoanDataForExternalTransfer {
|
||||||
|
|
||||||
Long id;
|
Long id;
|
||||||
ExternalId externalId;
|
ExternalId externalId;
|
||||||
Integer loanStatus;
|
Integer loanStatus;
|
||||||
|
Long loanProductId;
|
||||||
|
String loanProductShortName;
|
||||||
}
|
}
|
||||||
+4
-4
@@ -22,8 +22,8 @@ import java.time.LocalDate;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import org.apache.fineract.cob.data.LoanDataForExternalTransfer;
|
||||||
import org.apache.fineract.cob.data.LoanIdAndExternalIdAndAccountNo;
|
import org.apache.fineract.cob.data.LoanIdAndExternalIdAndAccountNo;
|
||||||
import org.apache.fineract.cob.data.LoanIdAndExternalIdAndStatus;
|
|
||||||
import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
|
import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
@@ -80,7 +80,7 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
|
|||||||
|
|
||||||
String FIND_BY_ACCOUNT_NUMBER = "select loan from Loan loan where loan.accountNumber = :accountNumber";
|
String FIND_BY_ACCOUNT_NUMBER = "select loan from Loan loan where loan.accountNumber = :accountNumber";
|
||||||
|
|
||||||
String FIND_LOAN_ID_AND_EXTERNAL_ID_AND_STATUS = "select new org.apache.fineract.cob.data.LoanIdAndExternalIdAndStatus(loan.id, loan.externalId, loan.loanStatus) from Loan loan where loan.id = :loanId";
|
String FIND_LOAN_DATA_FOR_EXTERNAL_TRANSFER = "select new org.apache.fineract.cob.data.LoanDataForExternalTransfer(loan.id, loan.externalId, loan.loanStatus, loan.loanProduct.id, loan.loanProduct.shortName) from Loan loan where loan.id = :loanId";
|
||||||
String EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID = "select case when (count (loan) > 0) then 'true' else 'false' end from Loan loan where loan.externalId = :externalLoanId and loan.loanStatus in (100,200,300,303,304)";
|
String EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID = "select case when (count (loan) > 0) then 'true' else 'false' end from Loan loan where loan.externalId = :externalLoanId and loan.loanStatus in (100,200,300,303,304)";
|
||||||
|
|
||||||
String FIND_ID_BY_EXTERNAL_ID = "SELECT loan.id FROM Loan loan WHERE loan.externalId = :externalId";
|
String FIND_ID_BY_EXTERNAL_ID = "SELECT loan.id FROM Loan loan WHERE loan.externalId = :externalId";
|
||||||
@@ -205,8 +205,8 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
|
|||||||
@Query(FIND_BY_ACCOUNT_NUMBER)
|
@Query(FIND_BY_ACCOUNT_NUMBER)
|
||||||
Loan findLoanAccountByAccountNumber(@Param("accountNumber") String accountNumber);
|
Loan findLoanAccountByAccountNumber(@Param("accountNumber") String accountNumber);
|
||||||
|
|
||||||
@Query(FIND_LOAN_ID_AND_EXTERNAL_ID_AND_STATUS)
|
@Query(FIND_LOAN_DATA_FOR_EXTERNAL_TRANSFER)
|
||||||
Optional<LoanIdAndExternalIdAndStatus> findLoanIdAndExternalIdAndStatusByLoanId(@Param("loanId") Long loanId);
|
Optional<LoanDataForExternalTransfer> findLoanDataForExternalTransferByLoanId(@Param("loanId") Long loanId);
|
||||||
|
|
||||||
@Query(EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID)
|
@Query(EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID)
|
||||||
boolean existsNonClosedLoanByExternalLoanId(@Param("externalLoanId") ExternalId externalLoanId);
|
boolean existsNonClosedLoanByExternalLoanId(@Param("externalLoanId") ExternalId externalLoanId);
|
||||||
|
|||||||
@@ -186,4 +186,5 @@
|
|||||||
<include file="parts/0165_add_credited_interest_to_instalment.xml" relativeToChangelogFile="true" />
|
<include file="parts/0165_add_credited_interest_to_instalment.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0166_transaction_summary_with_asset_owner_report_charge_adjustment_fix.xml" relativeToChangelogFile="true" />
|
<include file="parts/0166_transaction_summary_with_asset_owner_report_charge_adjustment_fix.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0167_create_m_calendar_instance_index.xml" relativeToChangelogFile="true" />
|
<include file="parts/0167_create_m_calendar_instance_index.xml" relativeToChangelogFile="true" />
|
||||||
|
<include file="parts/0168_transaction_summary_with_asset_owner_report_add_active_intermediate_filtering.xml" relativeToChangelogFile="true" />
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+1120
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -123,15 +123,17 @@ public class ExternalAssetOwnerTransferTest extends BaseLoanIntegrationTest {
|
|||||||
|
|
||||||
protected PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate) {
|
protected PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate) {
|
||||||
String transferExternalId = UUID.randomUUID().toString();
|
String transferExternalId = UUID.randomUUID().toString();
|
||||||
|
String transferExternalGroupId = UUID.randomUUID().toString();
|
||||||
ownerExternalId = UUID.randomUUID().toString();
|
ownerExternalId = UUID.randomUUID().toString();
|
||||||
return createSaleTransfer(loanID, settlementDate, transferExternalId, ownerExternalId, "1.0");
|
return createSaleTransfer(loanID, settlementDate, transferExternalId, transferExternalGroupId, ownerExternalId, "1.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate, String transferExternalId,
|
protected PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate, String transferExternalId,
|
||||||
String ownerExternalId, String purchasePriceRatio) {
|
String transferExternalGroupId, String ownerExternalId, String purchasePriceRatio) {
|
||||||
PostInitiateTransferResponse saleResponse = EXTERNAL_ASSET_OWNER_HELPER.initiateTransferByLoanId(loanID.longValue(), "sale",
|
PostInitiateTransferResponse saleResponse = EXTERNAL_ASSET_OWNER_HELPER.initiateTransferByLoanId(loanID.longValue(), "sale",
|
||||||
new PostInitiateTransferRequest().settlementDate(settlementDate).dateFormat("yyyy-MM-dd").locale("en")
|
new PostInitiateTransferRequest().settlementDate(settlementDate).dateFormat("yyyy-MM-dd").locale("en")
|
||||||
.transferExternalId(transferExternalId).ownerExternalId(ownerExternalId).purchasePriceRatio(purchasePriceRatio));
|
.transferExternalId(transferExternalId).transferExternalGroupId(transferExternalGroupId)
|
||||||
|
.ownerExternalId(ownerExternalId).purchasePriceRatio(purchasePriceRatio));
|
||||||
assertEquals(transferExternalId, saleResponse.getResourceExternalId());
|
assertEquals(transferExternalId, saleResponse.getResourceExternalId());
|
||||||
return saleResponse;
|
return saleResponse;
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-10
@@ -88,6 +88,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
|||||||
import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
|
import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||||
import org.apache.fineract.integrationtests.common.report.ReportHelper;
|
import org.apache.fineract.integrationtests.common.report.ReportHelper;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
@@ -631,9 +632,12 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
|||||||
|
|
||||||
LOAN_TRANSACTION_HELPER.makeRepayment("04 March 2020", 16000.0f, loanID);
|
LOAN_TRANSACTION_HELPER.makeRepayment("04 March 2020", 16000.0f, loanID);
|
||||||
|
|
||||||
|
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(REQUEST_SPEC, RESPONSE_SPEC, loanID);
|
||||||
|
LoanStatus loanStatus = LoanStatus.fromInt((Integer) loanStatusHashMap.get("id"));
|
||||||
|
|
||||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||||
() -> createSaleTransfer(loanID, "2020-03-02"));
|
() -> createSaleTransfer(loanID, "2020-03-02"));
|
||||||
assertTrue(exception.getMessage().contains("Loan is not in active status"));
|
assertTrue(exception.getMessage().contains(String.format("Loan status %s is not valid for transfer.", loanStatus)));
|
||||||
} finally {
|
} finally {
|
||||||
cleanUpAndRestoreBusinessDate();
|
cleanUpAndRestoreBusinessDate();
|
||||||
}
|
}
|
||||||
@@ -868,10 +872,12 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
|||||||
assertTrue(exception5.getMessage().contains("Loan with identifier -1 does not exist"));
|
assertTrue(exception5.getMessage().contains("Loan with identifier -1 does not exist"));
|
||||||
|
|
||||||
String externalId = UUID.randomUUID().toString();
|
String externalId = UUID.randomUUID().toString();
|
||||||
|
String transferExternalGroupId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
CallFailedRuntimeException exception6 = assertThrows(CallFailedRuntimeException.class, () -> {
|
CallFailedRuntimeException exception6 = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||||
Integer clientID = createClient();
|
Integer clientID = createClient();
|
||||||
Integer loanID = createLoanForClient(clientID);
|
Integer loanID = createLoanForClient(clientID);
|
||||||
createSaleTransfer(loanID, "2020-03-03", externalId, "1", "1.0");
|
createSaleTransfer(loanID, "2020-03-03", externalId, transferExternalGroupId, "1", "1.0");
|
||||||
createBuybackTransfer(loanID, "2020-03-02", externalId);
|
createBuybackTransfer(loanID, "2020-03-02", externalId);
|
||||||
});
|
});
|
||||||
assertTrue(exception6.getMessage()
|
assertTrue(exception6.getMessage()
|
||||||
@@ -892,12 +898,12 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
|||||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, () -> createSaleTransfer(loanID, null));
|
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, () -> createSaleTransfer(loanID, null));
|
||||||
assertTrue(exception.getMessage().contains("The parameter `settlementDate` is mandatory."));
|
assertTrue(exception.getMessage().contains("The parameter `settlementDate` is mandatory."));
|
||||||
|
|
||||||
CallFailedRuntimeException exception2 = assertThrows(CallFailedRuntimeException.class,
|
CallFailedRuntimeException exception2 = assertThrows(CallFailedRuntimeException.class, () -> createSaleTransfer(loanID,
|
||||||
() -> createSaleTransfer(loanID, "2020-03-02", UUID.randomUUID().toString(), null, "1.0"));
|
"2020-03-02", UUID.randomUUID().toString(), UUID.randomUUID().toString(), null, "1.0"));
|
||||||
assertTrue(exception2.getMessage().contains("The parameter `ownerExternalId` is mandatory."));
|
assertTrue(exception2.getMessage().contains("The parameter `ownerExternalId` is mandatory."));
|
||||||
|
|
||||||
CallFailedRuntimeException exception3 = assertThrows(CallFailedRuntimeException.class,
|
CallFailedRuntimeException exception3 = assertThrows(CallFailedRuntimeException.class,
|
||||||
() -> createSaleTransfer(loanID, "2020-03-02", null, UUID.randomUUID().toString(), null));
|
() -> createSaleTransfer(loanID, "2020-03-02", null, UUID.randomUUID().toString(), UUID.randomUUID().toString(), null));
|
||||||
assertTrue(exception3.getMessage().contains("The parameter `purchasePriceRatio` is mandatory."));
|
assertTrue(exception3.getMessage().contains("The parameter `purchasePriceRatio` is mandatory."));
|
||||||
|
|
||||||
CallFailedRuntimeException exception4 = assertThrows(CallFailedRuntimeException.class,
|
CallFailedRuntimeException exception4 = assertThrows(CallFailedRuntimeException.class,
|
||||||
@@ -918,10 +924,11 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
|||||||
});
|
});
|
||||||
assertTrue(exception6.getMessage().contains("This loan cannot be sold, because it is owned by an external asset owner"));
|
assertTrue(exception6.getMessage().contains("This loan cannot be sold, because it is owned by an external asset owner"));
|
||||||
String externalId = UUID.randomUUID().toString();
|
String externalId = UUID.randomUUID().toString();
|
||||||
|
String transferExternalGroupId = UUID.randomUUID().toString();
|
||||||
CallFailedRuntimeException exception7 = assertThrows(CallFailedRuntimeException.class, () -> {
|
CallFailedRuntimeException exception7 = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||||
Integer loanID2 = createLoanForClient(clientID);
|
Integer loanID2 = createLoanForClient(clientID);
|
||||||
createSaleTransfer(loanID2, "2020-03-05", externalId, "1", "1.0");
|
createSaleTransfer(loanID2, "2020-03-05", externalId, transferExternalGroupId, "1", "1.0");
|
||||||
createSaleTransfer(loanID2, "2020-03-05", externalId, "1", "1.0");
|
createSaleTransfer(loanID2, "2020-03-05", externalId, transferExternalGroupId, "1", "1.0");
|
||||||
});
|
});
|
||||||
assertTrue(exception7.getMessage()
|
assertTrue(exception7.getMessage()
|
||||||
.contains(String.format("Already existing an asset transfer with the provided transfer external id: %s", externalId)));
|
.contains(String.format("Already existing an asset transfer with the provided transfer external id: %s", externalId)));
|
||||||
@@ -1178,15 +1185,17 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
|||||||
|
|
||||||
private PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate) {
|
private PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate) {
|
||||||
String transferExternalId = UUID.randomUUID().toString();
|
String transferExternalId = UUID.randomUUID().toString();
|
||||||
|
String transferExternalGroupId = UUID.randomUUID().toString();
|
||||||
ownerExternalId = UUID.randomUUID().toString();
|
ownerExternalId = UUID.randomUUID().toString();
|
||||||
return createSaleTransfer(loanID, settlementDate, transferExternalId, ownerExternalId, "1.0");
|
return createSaleTransfer(loanID, settlementDate, transferExternalId, transferExternalGroupId, ownerExternalId, "1.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
private PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate, String transferExternalId,
|
private PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate, String transferExternalId,
|
||||||
String ownerExternalId, String purchasePriceRatio) {
|
String transferExternalGroupId, String ownerExternalId, String purchasePriceRatio) {
|
||||||
PostInitiateTransferResponse saleResponse = EXTERNAL_ASSET_OWNER_HELPER.initiateTransferByLoanId(loanID.longValue(), "sale",
|
PostInitiateTransferResponse saleResponse = EXTERNAL_ASSET_OWNER_HELPER.initiateTransferByLoanId(loanID.longValue(), "sale",
|
||||||
new PostInitiateTransferRequest().settlementDate(settlementDate).dateFormat("yyyy-MM-dd").locale("en")
|
new PostInitiateTransferRequest().settlementDate(settlementDate).dateFormat("yyyy-MM-dd").locale("en")
|
||||||
.transferExternalId(transferExternalId).ownerExternalId(ownerExternalId).purchasePriceRatio(purchasePriceRatio));
|
.transferExternalId(transferExternalId).transferExternalGroupId(transferExternalGroupId)
|
||||||
|
.ownerExternalId(ownerExternalId).purchasePriceRatio(purchasePriceRatio));
|
||||||
assertEquals(transferExternalId, saleResponse.getResourceExternalId());
|
assertEquals(transferExternalId, saleResponse.getResourceExternalId());
|
||||||
return saleResponse;
|
return saleResponse;
|
||||||
}
|
}
|
||||||
@@ -1315,6 +1324,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
|||||||
assertTrue(first.isPresent());
|
assertTrue(first.isPresent());
|
||||||
ExternalTransferData etd = first.get();
|
ExternalTransferData etd = first.get();
|
||||||
assertEquals(expected.transferExternalId, etd.getTransferExternalId());
|
assertEquals(expected.transferExternalId, etd.getTransferExternalId());
|
||||||
|
|
||||||
assertEquals(expected.status, etd.getStatus());
|
assertEquals(expected.status, etd.getStatus());
|
||||||
assertEquals(LocalDate.parse(expected.settlementDate), etd.getSettlementDate());
|
assertEquals(LocalDate.parse(expected.settlementDate), etd.getSettlementDate());
|
||||||
assertEquals(LocalDate.parse(expected.effectiveFrom), etd.getEffectiveFrom());
|
assertEquals(LocalDate.parse(expected.effectiveFrom), etd.getEffectiveFrom());
|
||||||
@@ -1415,6 +1425,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
|||||||
private final String effectiveTo;
|
private final String effectiveTo;
|
||||||
private final ExternalTransferData.SubStatusEnum subStatus;
|
private final ExternalTransferData.SubStatusEnum subStatus;
|
||||||
private final boolean detailsExpected;
|
private final boolean detailsExpected;
|
||||||
|
|
||||||
private final BigDecimal totalOutstanding;
|
private final BigDecimal totalOutstanding;
|
||||||
private final BigDecimal totalPrincipalOutstanding;
|
private final BigDecimal totalPrincipalOutstanding;
|
||||||
private final BigDecimal totalInterestOutstanding;
|
private final BigDecimal totalInterestOutstanding;
|
||||||
|
|||||||
Reference in New Issue
Block a user