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);
|
||||
}
|
||||
|
||||
public boolean isCreditEntry() {
|
||||
return JournalEntryType.CREDIT.getValue().equals(this.type);
|
||||
}
|
||||
|
||||
public void setReversalJournalEntry(final JournalEntry reversalJournalEntry) {
|
||||
this.reversalJournalEntry = reversalJournalEntry;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"default": null,
|
||||
"name": "transferExternalGroupId",
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"default": null,
|
||||
"name": "submittedDate",
|
||||
|
||||
@@ -31,8 +31,8 @@ compileJava.doLast {
|
||||
javaexec {
|
||||
description = 'Performs EclipseLink static weaving of entity classes'
|
||||
def target = source
|
||||
mainClass = 'org.eclipse.persistence.tools.weaving.jpa.StaticWeave'
|
||||
args '-persistenceinfo', source, '-classpath', sourceSets.main.runtimeClasspath, source, target
|
||||
main = 'org.eclipse.persistence.tools.weaving.jpa.StaticWeave'
|
||||
args '-persistenceinfo', source, source, target
|
||||
classpath sourceSets.main.runtimeClasspath
|
||||
}
|
||||
delete {
|
||||
|
||||
+8
@@ -3661,6 +3661,14 @@ public class CommandWrapperBuilder {
|
||||
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) {
|
||||
this.actionName = "SALE";
|
||||
this.entityName = "LOAN";
|
||||
|
||||
-2
@@ -43,9 +43,7 @@ public class CommandProcessingResultBuilder {
|
||||
private Long productId;
|
||||
private boolean rollbackTransaction = false;
|
||||
private ExternalId entityExternalId = ExternalId.empty();
|
||||
|
||||
private ExternalId subEntityExternalId = ExternalId.empty();
|
||||
|
||||
private ExternalId loanExternalId = ExternalId.empty();
|
||||
|
||||
public CommandProcessingResult build() {
|
||||
|
||||
+12
-7
@@ -20,13 +20,18 @@ package org.apache.fineract.test.data;
|
||||
|
||||
public enum AssetExternalizationErrorMessage {
|
||||
|
||||
LOAN_NOT_ACTIVE("Loan is not in active status"), ASSET_OWNED_CANNOT_BE_SOLD(
|
||||
"This loan cannot be sold, because it is owned by an external asset owner"), ASSET_NOT_OWNED_CANNOT_BE_BOUGHT(
|
||||
"This loan cannot be bought back, it is not owned by an external asset owner"), BUYBACK_ALREADY_IN_PROGRESS_CANNOT_BE_BOUGHT(
|
||||
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"), ALREADY_PENDING(
|
||||
"External asset owner transfer is already in PENDING state for this loan"), SETTLEMENT_DATE_IN_THE_PAST(
|
||||
"Settlement date cannot be in the past"), INVALID_REQUEST(
|
||||
"The request was invalid. This typically will happen due to validation errors which are provided.");
|
||||
LOAN_NOT_ACTIVE("Loan is not in active status"), //
|
||||
ASSET_OWNED_CANNOT_BE_SOLD("This loan cannot be sold, because it is owned by an external asset owner"), //
|
||||
ASSET_NOT_OWNED_CANNOT_BE_BOUGHT("This loan cannot be bought back, it is not owned by an external asset owner"), //
|
||||
BUYBACK_ALREADY_IN_PROGRESS_CANNOT_BE_BOUGHT(
|
||||
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"), //
|
||||
ALREADY_PENDING("External asset owner transfer is already in PENDING state for this loan"), //
|
||||
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;
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ Feature: Asset Externalization
|
||||
When Admin sets the business date to "10 May 2023"
|
||||
And Customer makes "AUTOPAY" repayment on "10 May 2023" with 1000 EUR transaction amount
|
||||
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 |
|
||||
| sale | 2023-05-21 | 1 |
|
||||
|
||||
@@ -276,7 +276,7 @@ Feature: Asset Externalization
|
||||
When Admin sets the business date to "10 May 2023"
|
||||
And Customer makes "AUTOPAY" repayment on "10 May 2023" with 1200 EUR transaction amount
|
||||
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 |
|
||||
| sale | 2023-05-21 | 1 |
|
||||
|
||||
@@ -396,7 +396,7 @@ Feature: Asset Externalization
|
||||
When Admin creates a client with random data
|
||||
When Admin creates a new default Loan with date: "1 May 2023"
|
||||
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 |
|
||||
| sale | 2023-05-30 | 1 |
|
||||
|
||||
@@ -407,7 +407,7 @@ Feature: Asset Externalization
|
||||
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"
|
||||
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 |
|
||||
| sale | 2023-05-30 | 1 |
|
||||
|
||||
|
||||
+3
-1
@@ -220,7 +220,9 @@ public class ExternalAssetOwnersApiResource {
|
||||
private String getResult(Long loanId, String apiRequestBodyAsJson, String commandParam) {
|
||||
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
|
||||
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();
|
||||
} else if (CommandParameterUtil.is(commandParam, "buyback")) {
|
||||
commandRequest = builder.buybackLoanToExternalAssetOwner(loanId).build();
|
||||
|
||||
+6
@@ -61,6 +61,9 @@ final class ExternalAssetOwnersApiResourceSwagger {
|
||||
@Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e")
|
||||
public String transferExternalId;
|
||||
|
||||
@Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e")
|
||||
public String transferExternalGroupId;
|
||||
|
||||
@Schema(example = "1")
|
||||
public String purchasePriceRatio;
|
||||
|
||||
@@ -101,6 +104,9 @@ final class ExternalAssetOwnersApiResourceSwagger {
|
||||
@Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e")
|
||||
public String transferExternalId;
|
||||
|
||||
@Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e")
|
||||
public String transferExternalGroupId;
|
||||
|
||||
@Schema(example = "1.2345678")
|
||||
public String purchasePriceRatio;
|
||||
|
||||
|
||||
+112
-36
@@ -23,16 +23,17 @@ import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.cob.loan.LoanCOBBusinessStep;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition;
|
||||
import org.apache.fineract.investor.data.ExternalTransferStatus;
|
||||
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.ExternalAssetOwnerTransferDetails;
|
||||
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.LoanOwnershipTransferBusinessEvent;
|
||||
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.springframework.context.annotation.Conditional;
|
||||
import org.springframework.data.domain.Sort;
|
||||
@@ -52,10 +55,16 @@ import org.springframework.stereotype.Component;
|
||||
public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep {
|
||||
|
||||
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 ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
|
||||
private final AccountingService accountingService;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanTransferabilityService loanTransferabilityService;
|
||||
private final DelayedSettlementAttributeService delayedSettlementAttributeService;
|
||||
|
||||
@Override
|
||||
public Loan execute(Loan loan) {
|
||||
@@ -66,7 +75,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
||||
List<ExternalAssetOwnerTransfer> transferDataList = externalAssetOwnerTransferRepository.findAll(
|
||||
(root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loanId),
|
||||
criteriaBuilder.equal(root.get("settlementDate"), settlementDate),
|
||||
root.get("status").in(List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK)),
|
||||
root.get("status").in(Stream.concat(PENDING_STATUSES.stream(), BUYBACK_STATUSES.stream()).toList()),
|
||||
criteriaBuilder.greaterThanOrEqualTo(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)),
|
||||
Sort.by(Sort.Direction.ASC, "id"));
|
||||
int size = transferDataList.size();
|
||||
@@ -75,6 +84,11 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
||||
ExternalTransferStatus firstTransferStatus = transferDataList.get(0).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)
|
||||
|| !ExternalTransferStatus.BUYBACK.equals(secondTransferStatus)) {
|
||||
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);
|
||||
} else if (size == 1) {
|
||||
ExternalAssetOwnerTransfer transfer = transferDataList.get(0);
|
||||
if (ExternalTransferStatus.PENDING.equals(transfer.getStatus())) {
|
||||
if (PENDING_STATUSES.contains(transfer.getStatus())) {
|
||||
handleSale(loan, settlementDate, transfer);
|
||||
} else if (ExternalTransferStatus.BUYBACK.equals(transfer.getStatus())) {
|
||||
} else if (BUYBACK_STATUSES.contains(transfer.getStatus())) {
|
||||
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) {
|
||||
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = sellAsset(loan, settlementDate, externalAssetOwnerTransfer);
|
||||
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = sellAssetOrDecline(loan, settlementDate, externalAssetOwnerTransfer);
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanOwnershipTransferBusinessEvent(newExternalAssetOwnerTransfer, loan));
|
||||
if (!ExternalTransferStatus.DECLINED.equals(newExternalAssetOwnerTransfer.getStatus())) {
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountSnapshotBusinessEvent(loan));
|
||||
@@ -104,14 +119,16 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
||||
|
||||
private void handleBuyback(final Loan loan, final LocalDate settlementDate,
|
||||
final ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer) {
|
||||
final ExternalTransferStatus expectedActiveStatus = determineExpectedActiveStatus(buybackExternalAssetOwnerTransfer);
|
||||
|
||||
Optional<ExternalAssetOwnerTransfer> optActiveExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository
|
||||
.findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
|
||||
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)));
|
||||
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer;
|
||||
if (!optActiveExternalAssetOwnerTransfer.isPresent()) {
|
||||
newExternalAssetOwnerTransfer = createNewEntry(settlementDate, buybackExternalAssetOwnerTransfer,
|
||||
newExternalAssetOwnerTransfer = createNewEntryAndExpireOldEntry(settlementDate, buybackExternalAssetOwnerTransfer,
|
||||
ExternalTransferStatus.CANCELLED, ExternalTransferSubStatus.UNSOLD, settlementDate, settlementDate);
|
||||
} else {
|
||||
newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, buybackExternalAssetOwnerTransfer,
|
||||
@@ -134,26 +151,71 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
||||
return buybackExternalAssetOwnerTransfer;
|
||||
}
|
||||
|
||||
private ExternalAssetOwnerTransfer sellAsset(final Loan loan, final LocalDate settlementDate,
|
||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer;
|
||||
if (isTransferable(loan)) {
|
||||
newExternalAssetOwnerTransfer = createActiveEntry(settlementDate, externalAssetOwnerTransfer);
|
||||
createActiveMapping(loan.getId(), newExternalAssetOwnerTransfer);
|
||||
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);
|
||||
private ExternalAssetOwnerTransfer sellAssetOrDecline(final Loan loan, final LocalDate settlementDate,
|
||||
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||
if (!loanTransferabilityService.isTransferable(loan, externalAssetOwnerTransfer)) {
|
||||
// Validation fails. Decline asset sell.
|
||||
ExternalTransferSubStatus declinedSubStatus = loanTransferabilityService.getDeclinedSubStatus(loan);
|
||||
return declinePendingEntry(loan, settlementDate, externalAssetOwnerTransfer, declinedSubStatus);
|
||||
}
|
||||
|
||||
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = sellAsset(loan, settlementDate, externalAssetOwnerTransfer);
|
||||
createActiveMapping(loan.getId(), newExternalAssetOwnerTransfer);
|
||||
newExternalAssetOwnerTransfer
|
||||
.setExternalAssetOwnerTransferDetails(createAssetOwnerTransferDetails(loan, 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,
|
||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||
ExternalAssetOwnerTransferDetails details = new ExternalAssetOwnerTransferDetails();
|
||||
@@ -176,10 +238,6 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
||||
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,
|
||||
Loan loan) {
|
||||
ExternalAssetOwnerTransfer cancelledPendingTransfer = cancelTransfer(settlementDate, transferDataList.get(0));
|
||||
@@ -190,11 +248,23 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
||||
|
||||
private ExternalAssetOwnerTransfer cancelTransfer(final LocalDate settlementDate,
|
||||
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||
return createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.CANCELLED,
|
||||
return createNewEntryAndExpireOldEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.CANCELLED,
|
||||
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 ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo) {
|
||||
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||
@@ -205,20 +275,26 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
|
||||
newExternalAssetOwnerTransfer.setSettlementDate(settlementDate);
|
||||
newExternalAssetOwnerTransfer.setLoanId(externalAssetOwnerTransfer.getLoanId());
|
||||
newExternalAssetOwnerTransfer.setExternalLoanId(externalAssetOwnerTransfer.getExternalLoanId());
|
||||
newExternalAssetOwnerTransfer.setExternalGroupId(externalAssetOwnerTransfer.getExternalGroupId());
|
||||
newExternalAssetOwnerTransfer.setPurchasePriceRatio(externalAssetOwnerTransfer.getPurchasePriceRatio());
|
||||
newExternalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
|
||||
newExternalAssetOwnerTransfer.setEffectiveDateTo(effectiveDateTo);
|
||||
|
||||
externalAssetOwnerTransfer.setEffectiveDateTo(settlementDate);
|
||||
externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
|
||||
expireTransfer(settlementDate, externalAssetOwnerTransfer);
|
||||
|
||||
return externalAssetOwnerTransferRepository.save(newExternalAssetOwnerTransfer);
|
||||
}
|
||||
|
||||
private ExternalAssetOwnerTransfer createActiveEntry(final LocalDate settlementDate,
|
||||
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||
LocalDate effectiveFrom = settlementDate.plusDays(1);
|
||||
return createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.ACTIVE, null, effectiveFrom,
|
||||
FUTURE_DATE_9999_12_31);
|
||||
private void expireTransfer(final LocalDate settlementDate, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||
externalAssetOwnerTransfer.setEffectiveDateTo(settlementDate);
|
||||
externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
|
||||
}
|
||||
|
||||
private ExternalTransferStatus determineExpectedActiveStatus(final ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer) {
|
||||
if (ExternalTransferStatus.BUYBACK_INTERMEDIATE == buybackExternalAssetOwnerTransfer.getStatus()) {
|
||||
return ExternalTransferStatus.ACTIVE_INTERMEDIATE;
|
||||
}
|
||||
return ExternalTransferStatus.ACTIVE;
|
||||
}
|
||||
|
||||
@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 ExternalTransferDataDetails details;
|
||||
private String transferExternalId;
|
||||
private String transferExternalGroupId;
|
||||
private String purchasePriceRatio;
|
||||
private LocalDate settlementDate;
|
||||
private ExternalTransferStatus status;
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ public final class ExternalTransferRequestParameters {
|
||||
public static final String SETTLEMENT_DATE = "settlementDate";
|
||||
public static final String OWNER_EXTERNAL_ID = "ownerExternalId";
|
||||
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 DATEFORMAT = "dateFormat";
|
||||
public static final String LOCALE = "locale";
|
||||
|
||||
+1
-1
@@ -19,5 +19,5 @@
|
||||
package org.apache.fineract.investor.data;
|
||||
|
||||
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)
|
||||
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 transferExternalId;
|
||||
private final ExternalId transferExternalGroupId;
|
||||
|
||||
private final ExternalTransferStatus status;
|
||||
private final ExternalTransferSubStatus subStatus;
|
||||
|
||||
+5
-4
@@ -94,10 +94,11 @@ public class SearchingExternalAssetOwnerRepositoryImpl implements SearchingExter
|
||||
query.orderBy(orders);
|
||||
|
||||
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"),
|
||||
root.get("settlementDate"), root.get("effectiveDateFrom"), root.get("effectiveDateTo"), details.get("id"),
|
||||
details.get("totalOutstanding"), details.get("totalPrincipalOutstanding"), details.get("totalInterestOutstanding"),
|
||||
details.get("totalFeeChargesOutstanding"), details.get("totalPenaltyChargesOutstanding"), details.get("totalOverpaid")));
|
||||
owner.get("externalId"), root.get("externalId"), root.get("externalGroupId"), root.get("status"), root.get("subStatus"),
|
||||
root.get("purchasePriceRatio"), root.get("settlementDate"), root.get("effectiveDateFrom"), root.get("effectiveDateTo"),
|
||||
details.get("id"), details.get("totalOutstanding"), details.get("totalPrincipalOutstanding"),
|
||||
details.get("totalInterestOutstanding"), details.get("totalFeeChargesOutstanding"),
|
||||
details.get("totalPenaltyChargesOutstanding"), details.get("totalOverpaid")));
|
||||
|
||||
TypedQuery<SearchedExternalAssetOwner> queryToExecute = entityManager.createQuery(query);
|
||||
return criteriaQueryFactory.readPage(queryToExecute, ExternalAssetOwnerTransfer.class, pageable, spec);
|
||||
|
||||
+2
-1
@@ -18,12 +18,13 @@
|
||||
*/
|
||||
package org.apache.fineract.investor.service;
|
||||
|
||||
import org.apache.fineract.investor.domain.ExternalAssetOwner;
|
||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
|
||||
public interface AccountingService {
|
||||
|
||||
void createJournalEntriesForSaleAssetTransfer(Loan loan, ExternalAssetOwnerTransfer transfer);
|
||||
void createJournalEntriesForSaleAssetTransfer(Loan loan, ExternalAssetOwnerTransfer transfer, ExternalAssetOwner previousOwner);
|
||||
|
||||
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.glaccount.domain.GLAccount;
|
||||
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.domain.ExternalAssetOwner;
|
||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping;
|
||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository;
|
||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
|
||||
@@ -53,25 +53,44 @@ public class AccountingServiceImpl implements AccountingService {
|
||||
private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository;
|
||||
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
|
||||
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);
|
||||
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
|
||||
public void createJournalEntriesForBuybackAssetTransfer(final Loan loan, final ExternalAssetOwnerTransfer transfer) {
|
||||
final ExternalAssetOwner previousOwner = transfer.getOwner();
|
||||
List<JournalEntry> journalEntryList = createJournalEntries(loan, transfer, false);
|
||||
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
|
||||
@@ -95,18 +114,52 @@ public class AccountingServiceImpl implements AccountingService {
|
||||
return journalEntryList;
|
||||
}
|
||||
|
||||
private void createMappingToOwner(ExternalAssetOwnerTransfer transfer, List<JournalEntry> journalEntryList,
|
||||
JournalEntryType filterType) {
|
||||
FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
|
||||
.findByFinancialActivityTypeWithNotFoundDetection(AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue());
|
||||
journalEntryList.forEach(journalEntry -> {
|
||||
if (participateInTransfer(financialActivityAccount, journalEntry, filterType)) {
|
||||
ExternalAssetOwnerJournalEntryMapping mapping = new ExternalAssetOwnerJournalEntryMapping();
|
||||
mapping.setJournalEntry(journalEntry);
|
||||
mapping.setOwner(transfer.getOwner());
|
||||
externalAssetOwnerJournalEntryMappingRepository.saveAndFlush(mapping);
|
||||
private void createMappingToOwner(final ExternalAssetOwner owner, final JournalEntry journalEntry) {
|
||||
if (owner == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExternalAssetOwnerJournalEntryMapping mapping = new ExternalAssetOwnerJournalEntryMapping();
|
||||
mapping.setJournalEntry(journalEntry);
|
||||
mapping.setOwner(owner);
|
||||
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) {
|
||||
@@ -211,4 +264,8 @@ public class AccountingServiceImpl implements AccountingService {
|
||||
}
|
||||
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.externalId", source = "externalLoanId")
|
||||
@Mapping(target = "transferExternalId", source = "externalId")
|
||||
@Mapping(target = "transferExternalGroupId", source = "externalGroupId")
|
||||
@Mapping(target = "effectiveFrom", source = "effectiveDateFrom")
|
||||
@Mapping(target = "effectiveTo", source = "effectiveDateTo")
|
||||
@Mapping(target = "purchasePriceRatio", source = "purchasePriceRatio")
|
||||
|
||||
+2
@@ -23,6 +23,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface ExternalAssetOwnersWriteService {
|
||||
|
||||
CommandProcessingResult intermediarySaleLoanByLoanId(JsonCommand jsonCommand);
|
||||
|
||||
CommandProcessingResult saleLoanByLoanId(JsonCommand command);
|
||||
|
||||
CommandProcessingResult buybackLoanByLoanId(JsonCommand command);
|
||||
|
||||
+238
-35
@@ -18,8 +18,12 @@
|
||||
*/
|
||||
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_INTERMEDIATE;
|
||||
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_ON_HOLD;
|
||||
|
||||
@@ -35,9 +39,10 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.data.ApiParameterError;
|
||||
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 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,
|
||||
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 ExternalAssetOwnerRepository externalAssetOwnerRepository;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
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
|
||||
@Transactional
|
||||
public CommandProcessingResult saleLoanByLoanId(JsonCommand command) {
|
||||
final JsonElement json = fromApiJsonHelper.parse(command.json());
|
||||
final LoanDataForExternalTransfer loanDataForExternalTransfer = fetchAndValidateLoanDataForExternalTransfer(command.getLoanId());
|
||||
final boolean isDelayedSettlementEnabled = delayedSettlementAttributeService
|
||||
.isEnabled(loanDataForExternalTransfer.getLoanProductId());
|
||||
validateSaleRequestBody(command.json());
|
||||
ExternalId externalId = getTransferExternalIdFromJson(json);
|
||||
validateExternalId(externalId);
|
||||
Long loanId = command.getLoanId();
|
||||
LoanIdAndExternalIdAndStatus loanIdAndExternalIdAndStatus = fetchLoanDetails(loanId);
|
||||
validateLoanStatus(loanIdAndExternalIdAndStatus);
|
||||
validateLoanStatus(loanDataForExternalTransfer, isDelayedSettlementEnabled);
|
||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = createSaleTransfer(loanId, json,
|
||||
loanIdAndExternalIdAndStatus.getExternalId());
|
||||
validateSale(externalAssetOwnerTransfer);
|
||||
loanDataForExternalTransfer.getExternalId());
|
||||
validateSale(externalAssetOwnerTransfer, isDelayedSettlementEnabled);
|
||||
externalAssetOwnerTransferRepository.saveAndFlush(externalAssetOwnerTransfer);
|
||||
return buildResponseData(externalAssetOwnerTransfer);
|
||||
}
|
||||
@@ -99,13 +133,13 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
public CommandProcessingResult buybackLoanByLoanId(JsonCommand command) {
|
||||
final JsonElement json = fromApiJsonHelper.parse(command.json());
|
||||
validateBuybackRequestBody(command.json());
|
||||
Long loanId = command.getLoanId();
|
||||
validateLoan(loanId);
|
||||
LoanDataForExternalTransfer loanDataForExternalTransfer = fetchAndValidateLoanDataForExternalTransfer(command.getLoanId());
|
||||
LocalDate settlementDate = getSettlementDateFromJson(json);
|
||||
ExternalId externalId = getTransferExternalIdFromJson(json);
|
||||
validateSettlementDate(settlementDate);
|
||||
validateExternalId(externalId);
|
||||
ExternalAssetOwnerTransfer effectiveTransfer = fetchAndValidateEffectiveTransferForBuyback(loanId, settlementDate);
|
||||
ExternalAssetOwnerTransfer effectiveTransfer = fetchAndValidateEffectiveTransferForBuyback(loanDataForExternalTransfer,
|
||||
settlementDate);
|
||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = createBuybackTransfer(effectiveTransfer, settlementDate, externalId);
|
||||
externalAssetOwnerTransferRepository.saveAndFlush(externalAssetOwnerTransfer);
|
||||
return buildResponseData(externalAssetOwnerTransfer);
|
||||
@@ -120,10 +154,8 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
}
|
||||
}
|
||||
|
||||
private void validateLoan(Long loanId) {
|
||||
if (!loanRepository.existsById(loanId)) {
|
||||
throw new LoanNotFoundException(loanId);
|
||||
}
|
||||
private LoanDataForExternalTransfer fetchAndValidateLoanDataForExternalTransfer(Long loanId) {
|
||||
return loanRepository.findLoanDataForExternalTransferByLoanId(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -136,10 +168,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
return buildResponseData(cancelTransfer);
|
||||
}
|
||||
|
||||
private void validateEffectiveTransferForSale(final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||
List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository
|
||||
.findEffectiveTransfersOrderByIdDesc(externalAssetOwnerTransfer.getLoanId(), DateUtils.getBusinessLocalDate());
|
||||
|
||||
private void validateEffectiveTransferForSale(final List<ExternalAssetOwnerTransfer> effectiveTransfers) {
|
||||
if (effectiveTransfers.size() == 2) {
|
||||
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
||||
} 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
|
||||
.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) {
|
||||
throw new ExternalAssetOwnerInitiateTransferException(
|
||||
@@ -179,6 +246,39 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
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) {
|
||||
ExternalAssetOwnerTransfer selectedTransfer = externalAssetOwnerTransferRepository.findById(transferId)
|
||||
.orElseThrow(() -> new ExternalAssetOwnerInitiateTransferException(
|
||||
@@ -206,10 +306,9 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||
externalAssetOwnerTransfer.setExternalId(externalId);
|
||||
externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner());
|
||||
externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.BUYBACK);
|
||||
externalAssetOwnerTransfer.setStatus(determineStatusAfterBuyback(effectiveTransfer));
|
||||
externalAssetOwnerTransfer.setLoanId(effectiveTransfer.getLoanId());
|
||||
externalAssetOwnerTransfer.setExternalLoanId(effectiveTransfer.getExternalLoanId());
|
||||
externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner());
|
||||
externalAssetOwnerTransfer.setSettlementDate(settlementDate);
|
||||
externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
|
||||
externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31);
|
||||
@@ -217,6 +316,16 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
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) {
|
||||
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
|
||||
externalAssetOwnerTransfer.setExternalId(effectiveTransfer.getExternalId());
|
||||
@@ -224,6 +333,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
externalAssetOwnerTransfer.setSubStatus(ExternalTransferSubStatus.USER_REQUESTED);
|
||||
externalAssetOwnerTransfer.setLoanId(effectiveTransfer.getLoanId());
|
||||
externalAssetOwnerTransfer.setExternalLoanId(effectiveTransfer.getExternalLoanId());
|
||||
externalAssetOwnerTransfer.setExternalGroupId(effectiveTransfer.getExternalGroupId());
|
||||
externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner());
|
||||
externalAssetOwnerTransfer.setSettlementDate(effectiveTransfer.getSettlementDate());
|
||||
externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveTransfer.getEffectiveDateFrom());
|
||||
@@ -241,9 +351,22 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
.build();
|
||||
}
|
||||
|
||||
private void validateSale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
|
||||
private void validateSale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer, boolean isDelayedSettlementEnabled) {
|
||||
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) {
|
||||
@@ -256,9 +379,25 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
}
|
||||
}
|
||||
|
||||
private void validateLoanStatus(LoanIdAndExternalIdAndStatus entity) {
|
||||
if (!ACTIVE_LOAN_STATUSES.contains(LoanStatus.fromInt(entity.getLoanStatus()))) {
|
||||
throw new ExternalAssetOwnerInitiateTransferException("Loan is not in active status");
|
||||
private void validateLoanStatus(LoanDataForExternalTransfer loanDataForExternalTransfer, boolean isDelayedSettlementEnabled) {
|
||||
LoanStatus loanStatus = LoanStatus.fromInt(loanDataForExternalTransfer.getLoanStatus());
|
||||
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.setLoanId(loanId);
|
||||
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;
|
||||
}
|
||||
|
||||
private void validateSaleRequestBody(String apiRequestBodyAsJson) {
|
||||
final Set<String> requestParameters = new HashSet<>(
|
||||
Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE, ExternalTransferRequestParameters.OWNER_EXTERNAL_ID,
|
||||
ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO,
|
||||
ExternalTransferRequestParameters.DATEFORMAT, ExternalTransferRequestParameters.LOCALE));
|
||||
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();
|
||||
@@ -308,6 +466,51 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
LocalDate settlementDate = fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, json);
|
||||
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()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
@@ -352,6 +555,12 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
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) {
|
||||
return fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, json);
|
||||
}
|
||||
@@ -368,10 +577,4 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
||||
externalAssetOwner.setExternalId(ExternalIdFactory.produce(externalId));
|
||||
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;
|
||||
|
||||
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_INTERMEDIATE;
|
||||
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.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_ZERO;
|
||||
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.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
|
||||
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.domain.ExternalAssetOwnerTransfer;
|
||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferDetails;
|
||||
@@ -64,25 +67,21 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
||||
@Override
|
||||
public void handleLoanClosedOrOverpaid(Loan loan) {
|
||||
Long loanId = loan.getId();
|
||||
List<ExternalAssetOwnerTransfer> transferDataList = findAllPendingOrBuybackTransfers(loanId);
|
||||
List<ExternalAssetOwnerTransfer> transferDataList = findAllPendingOrBuybackOrIntermediateTransfers(loanId);
|
||||
|
||||
if (transferDataList.size() == 2) {
|
||||
ExternalTransferSubStatus subStatus;
|
||||
ExternalAssetOwnerTransfer pendingSaleTransfer = transferDataList.get(0);
|
||||
ExternalAssetOwnerTransfer pendingBuybackTransfer = transferDataList.get(1);
|
||||
if (transferDataList.size() > 1) {
|
||||
if (isSameDayTransfers(transferDataList)) {
|
||||
subStatus = SAMEDAY_TRANSFERS;
|
||||
cancelTransfer(loan, pendingSaleTransfer, subStatus);
|
||||
cancelTransfer(loan, pendingBuybackTransfer, subStatus);
|
||||
transferDataList.forEach(externalAssetOwnerTransfer -> cancelTransfer(loan, externalAssetOwnerTransfer, SAMEDAY_TRANSFERS));
|
||||
} else {
|
||||
declineTransfer(loan, pendingSaleTransfer);
|
||||
cancelTransfer(loan, pendingBuybackTransfer, UNSOLD);
|
||||
// decline first and cancel the rest
|
||||
declineTransfer(loan, transferDataList.get(0));
|
||||
transferDataList.stream().skip(1).forEach(assetOwnerTransfer -> cancelTransfer(loan, assetOwnerTransfer, UNSOLD));
|
||||
}
|
||||
} else if (transferDataList.size() == 1) {
|
||||
ExternalAssetOwnerTransfer transfer = transferDataList.get(0);
|
||||
if (PENDING.equals(transfer.getStatus())) {
|
||||
if (PENDING.equals(transfer.getStatus()) || PENDING_INTERMEDIATE.equals(transfer.getStatus())) {
|
||||
declineTransfer(loan, transfer);
|
||||
} else if (BUYBACK.equals(transfer.getStatus())) {
|
||||
} else if (BUYBACK.equals(transfer.getStatus()) || BUYBACK_INTERMEDIATE.equals(transfer.getStatus())) {
|
||||
executePendingBuybackTransfer(loan, transfer);
|
||||
}
|
||||
}
|
||||
@@ -103,7 +102,7 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
||||
}
|
||||
|
||||
private void executePendingBuybackTransfer(final Loan loan, ExternalAssetOwnerTransfer buybackTransfer) {
|
||||
ExternalAssetOwnerTransfer activeTransfer = findActiveTransfer(loan, buybackTransfer);
|
||||
ExternalAssetOwnerTransfer activeTransfer = findActiveOrActiveIntermediateTransfer(loan, buybackTransfer);
|
||||
updateActiveTransfer(activeTransfer);
|
||||
buybackTransfer = updatePendingBuybackTransfer(loan, buybackTransfer);
|
||||
|
||||
@@ -119,6 +118,7 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
||||
ExternalAssetOwnerTransfer cancelledTransfer = new ExternalAssetOwnerTransfer();
|
||||
cancelledTransfer.setOwner(pendingTransfer.getOwner());
|
||||
cancelledTransfer.setExternalId(pendingTransfer.getExternalId());
|
||||
cancelledTransfer.setExternalGroupId(pendingTransfer.getExternalGroupId());
|
||||
cancelledTransfer.setStatus(CANCELLED);
|
||||
cancelledTransfer.setSubStatus(subStatus);
|
||||
cancelledTransfer.setSettlementDate(pendingTransfer.getSettlementDate());
|
||||
@@ -134,6 +134,7 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
||||
ExternalAssetOwnerTransfer declinedTransfer = new ExternalAssetOwnerTransfer();
|
||||
declinedTransfer.setOwner(pendingSaleTransfer.getOwner());
|
||||
declinedTransfer.setExternalId(pendingSaleTransfer.getExternalId());
|
||||
declinedTransfer.setExternalGroupId(pendingSaleTransfer.getExternalGroupId());
|
||||
declinedTransfer.setStatus(DECLINED);
|
||||
declinedTransfer.setSubStatus(isBiggerThanZero(loan.getTotalOverpaid()) ? BALANCE_NEGATIVE : BALANCE_ZERO);
|
||||
declinedTransfer.setSettlementDate(pendingSaleTransfer.getSettlementDate());
|
||||
@@ -176,20 +177,20 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
||||
return details;
|
||||
}
|
||||
|
||||
private ExternalAssetOwnerTransfer findActiveTransfer(Loan loan, ExternalAssetOwnerTransfer buybackTransfer) {
|
||||
private ExternalAssetOwnerTransfer findActiveOrActiveIntermediateTransfer(Loan loan, ExternalAssetOwnerTransfer buybackTransfer) {
|
||||
return externalAssetOwnerTransferRepository
|
||||
.findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
|
||||
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)))
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
private List<ExternalAssetOwnerTransfer> findAllPendingOrBuybackTransfers(Long loanId) {
|
||||
private List<ExternalAssetOwnerTransfer> findAllPendingOrBuybackOrIntermediateTransfers(Long loanId) {
|
||||
return externalAssetOwnerTransferRepository
|
||||
.findAll(
|
||||
(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)),
|
||||
Sort.by(Sort.Direction.ASC, "id"));
|
||||
}
|
||||
@@ -199,6 +200,6 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
|
||||
}
|
||||
|
||||
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.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_INTERMEDIATE;
|
||||
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.PENDING_INTERMEDIATE;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.avro.generic.GenericContainer;
|
||||
import org.apache.fineract.avro.generator.ByteBufferSerializable;
|
||||
@@ -50,6 +54,9 @@ import org.springframework.stereotype.Component;
|
||||
@RequiredArgsConstructor
|
||||
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 static CurrencyDataV1 getCurrencyFromEvent(InvestorBusinessEvent event) {
|
||||
@@ -83,6 +90,7 @@ public class InvestorBusinessEventSerializer implements BusinessEventSerializer
|
||||
LoanOwnershipTransferDataV1.Builder builder = LoanOwnershipTransferDataV1.newBuilder().setLoanId(transferData.getLoan().getLoanId())
|
||||
.setLoanExternalId(transferData.getLoan().getExternalId()).setTransferExternalId(transferData.getTransferExternalId())
|
||||
.setAssetOwnerExternalId(transferData.getOwner().getExternalId())
|
||||
.setTransferExternalGroupId(transferData.getTransferExternalGroupId())
|
||||
.setPurchasePriceRatio(transferData.getPurchasePriceRatio()).setCurrency(getCurrencyFromEvent(event))
|
||||
.setSettlementDate(transferData.getSettlementDate().format(DEFAULT_DATE_FORMATTER))
|
||||
.setSubmittedDate(transferData.getSettlementDate().format(DEFAULT_DATE_FORMATTER)).setType(transferType)
|
||||
@@ -103,7 +111,15 @@ public class InvestorBusinessEventSerializer implements BusinessEventSerializer
|
||||
|
||||
@NotNull
|
||||
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) {
|
||||
@@ -126,7 +142,7 @@ public class InvestorBusinessEventSerializer implements BusinessEventSerializer
|
||||
}
|
||||
|
||||
private String getStatus(ExternalTransferStatus status) {
|
||||
if (ACTIVE.equals(status) || BUYBACK.equals(status)) {
|
||||
if (EXECUTED_TRANSFER_STATUSES.contains(status)) {
|
||||
return "EXECUTED";
|
||||
} else if (DECLINED.equals(status) || CANCELLED.equals(status)) {
|
||||
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/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/0015_add_intermediary_sale_command.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/0016_add_external_reference_id.xml"/>
|
||||
</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.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
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 java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
||||
import org.apache.fineract.infrastructure.core.domain.ActionContext;
|
||||
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.investor.data.ExternalTransferStatus;
|
||||
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.ExternalAssetOwnerTransferLoanMapping;
|
||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
|
||||
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
|
||||
import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
|
||||
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.LoanSummary;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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.Mock;
|
||||
import org.mockito.Mockito;
|
||||
@@ -69,17 +78,27 @@ import org.springframework.data.jpa.domain.Specification;
|
||||
public class LoanAccountOwnerTransferBusinessStepTest {
|
||||
|
||||
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());
|
||||
|
||||
@Mock
|
||||
private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
||||
|
||||
@Mock
|
||||
private ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
|
||||
|
||||
@Mock
|
||||
private AccountingService accountingService;
|
||||
|
||||
@Mock
|
||||
private BusinessEventNotifierService businessEventNotifierService;
|
||||
|
||||
@Mock
|
||||
private AccountingService accountingService;
|
||||
private LoanTransferabilityService loanTransferabilityService;
|
||||
|
||||
@Mock
|
||||
private DelayedSettlementAttributeService delayedSettlementAttributeService;
|
||||
|
||||
private LoanAccountOwnerTransferBusinessStep underTest;
|
||||
|
||||
@BeforeEach
|
||||
@@ -88,7 +107,8 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
||||
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
|
||||
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate)));
|
||||
underTest = new LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository,
|
||||
externalAssetOwnerTransferLoanMappingRepository, accountingService, businessEventNotifierService);
|
||||
externalAssetOwnerTransferLoanMappingRepository, accountingService, businessEventNotifierService,
|
||||
loanTransferabilityService, delayedSettlementAttributeService);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -106,15 +126,21 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
||||
final Loan processedLoan = underTest.execute(loanForProcessing);
|
||||
// then
|
||||
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||
verifyNoInteractions(businessEventNotifierService);
|
||||
verifyNoInteractions(businessEventNotifierService, loanTransferabilityService, accountingService);
|
||||
assertEquals(processedLoan, loanForProcessing);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenLoanTwoTransferButInvalidTransfers() {
|
||||
// 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(false);
|
||||
|
||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
|
||||
@@ -127,14 +153,46 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
||||
// then
|
||||
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")));
|
||||
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
|
||||
public void givenLoanTwoTransferSameDay() {
|
||||
// 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(false);
|
||||
|
||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||
|
||||
@@ -192,13 +250,20 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
||||
|
||||
assertEquals(processedLoan, loanForProcessing);
|
||||
|
||||
verifyNoInteractions(loanTransferabilityService, accountingService);
|
||||
|
||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, secondSaveResult);
|
||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, fourthSaveResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenLoanBuyback() {
|
||||
private static Stream<Arguments> buybackStatusDataProvider() {
|
||||
return Stream.of(Arguments.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE), Arguments.of(ExternalTransferStatus.BUYBACK));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("buybackStatusDataProvider")
|
||||
public void givenLoanBuyback(final ExternalTransferStatus buybackStatus) {
|
||||
// given
|
||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||
when(loanForProcessing.getId()).thenReturn(1L);
|
||||
@@ -206,7 +271,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
||||
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
||||
ExternalAssetOwnerTransfer firstResponseItem = 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);
|
||||
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
|
||||
.thenReturn(response);
|
||||
@@ -218,6 +283,8 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
||||
// when
|
||||
final Loan processedLoan = underTest.execute(loanForProcessing);
|
||||
// then
|
||||
verifyNoInteractions(loanTransferabilityService);
|
||||
|
||||
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());
|
||||
@@ -226,147 +293,248 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
||||
|
||||
assertEquals(processedLoan, loanForProcessing);
|
||||
|
||||
verify(accountingService).createJournalEntriesForBuybackAssetTransfer(loanForProcessing, firstResponseItem);
|
||||
verifyNoMoreInteractions(accountingService);
|
||||
|
||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, firstResponseItem);
|
||||
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenLoanSale() {
|
||||
private static Stream<Arguments> loanSaleTransferableDataProvider() {
|
||||
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
|
||||
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);
|
||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
|
||||
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
|
||||
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
|
||||
.thenReturn(response);
|
||||
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
|
||||
.forClass(ExternalAssetOwnerTransfer.class);
|
||||
ArgumentCaptor<ExternalAssetOwnerTransferLoanMapping> externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor
|
||||
.forClass(ExternalAssetOwnerTransferLoanMapping.class);
|
||||
ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
|
||||
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(isDelayedSettlementEnabled);
|
||||
|
||||
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
|
||||
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
|
||||
final Loan processedLoan = underTest.execute(loanForProcessing);
|
||||
// then
|
||||
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||
verify(firstResponseItem).setEffectiveDateTo(actualDate);
|
||||
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||
verify(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer);
|
||||
verifyNoMoreInteractions(loanTransferabilityService);
|
||||
verify(externalAssetOwnerTransferRepository).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||
|
||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
|
||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
|
||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
|
||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
|
||||
assertEquals(ExternalTransferStatus.ACTIVE, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
|
||||
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
|
||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(),
|
||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId());
|
||||
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(),
|
||||
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio());
|
||||
assertEquals(actualDate.plusDays(1), externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
|
||||
assertEquals(FUTURE_DATE_9999_12_31, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
|
||||
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(expectedActiveStatus, 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).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))
|
||||
.save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture());
|
||||
assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId());
|
||||
assertEquals(newTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
|
||||
assertEquals(savedNewTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
|
||||
assertEquals(processedLoan, loanForProcessing);
|
||||
|
||||
verify(accountingService).createJournalEntriesForSaleAssetTransfer(loanForProcessing, savedNewTransfer, previousOwner);
|
||||
verifyNoMoreInteractions(accountingService);
|
||||
|
||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer);
|
||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, savedNewTransfer);
|
||||
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenLoanSaleButBalanceIsZero() {
|
||||
public void testSaleLoanWithDelayedSettlementFromIntermediateToInvestorActiveIntermediateTransferNotFound() {
|
||||
// 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);
|
||||
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
|
||||
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
|
||||
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
|
||||
|
||||
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"))))
|
||||
.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.ZERO);
|
||||
when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ZERO);
|
||||
when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED);
|
||||
when(loanTransferabilityService.isTransferable(loanForProcessing, pendingTransfer)).thenReturn(true);
|
||||
|
||||
// when
|
||||
final Loan processedLoan = underTest.execute(loanForProcessing);
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> 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("Expected a effective transfer of ACTIVE_INTERMEDIATE status to be present.", exception.getMessage());
|
||||
|
||||
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
|
||||
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);
|
||||
verify(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer);
|
||||
verifyNoMoreInteractions(loanTransferabilityService);
|
||||
verifyNoInteractions(accountingService);
|
||||
verify(externalAssetOwnerTransferRepository).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
|
||||
verify(externalAssetOwnerTransferRepository).findOne(any(Specification.class));
|
||||
verify(externalAssetOwnerTransferRepository, never()).save(any(ExternalAssetOwnerTransfer.class));
|
||||
verifyNoInteractions(externalAssetOwnerTransferLoanMappingRepository);
|
||||
verifyBusinessEvents(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -383,55 +551,6 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
||||
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
|
||||
private ArgumentCaptor<BusinessEvent<?>> verifyBusinessEvents(int expectedBusinessEvents) {
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -453,4 +572,12 @@ public class LoanAccountOwnerTransferBusinessStepTest {
|
||||
assertTrue(businessEventArgumentCaptor.getAllValues().get(index) instanceof LoanAccountSnapshotBusinessEvent);
|
||||
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;
|
||||
|
||||
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.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -36,10 +34,12 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
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.loan.LoanAccountSnapshotBusinessEvent;
|
||||
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.ExternalAssetOwnerTransferLoanMappingRepository;
|
||||
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.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.Mock;
|
||||
import org.mockito.Mockito;
|
||||
@@ -80,38 +83,88 @@ public class LoanAccountOwnerTransferServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWhenCancelPendingSaleAndBuybackTransferThenBusinessEventsAreSent() {
|
||||
public void verifyWhenCancelPendingAndIntermediateSaleAndBuybackTransferThenBusinessEventsAreSent() {
|
||||
// given
|
||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||
when(loanForProcessing.getId()).thenReturn(1L);
|
||||
|
||||
ExternalAssetOwnerTransfer pendingSaleTransfer = 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 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.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
|
||||
underTest.handleLoanClosedOrOverpaid(loanForProcessing);
|
||||
|
||||
// then
|
||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
|
||||
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(4);
|
||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, cancelledSaleTransfer);
|
||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, cancelledBuybackTransfer);
|
||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 2, loanForProcessing, cancelledIntermediateTransfer);
|
||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 3, loanForProcessing, cancelledBuyBackIntermediateSaleTransfer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWhenDeclinePendingSaleTransferThenBusinessEventIsSent() {
|
||||
public void verifyWhenDeclineCancelPendingAndIntermediateSaleAndBuybackTransferThenBusinessEventsAreSent() {
|
||||
// given
|
||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||
when(loanForProcessing.getId()).thenReturn(1L);
|
||||
|
||||
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);
|
||||
List<ExternalAssetOwnerTransfer> response = List.of(pendingSaleTransfer);
|
||||
|
||||
@@ -127,8 +180,9 @@ public class LoanAccountOwnerTransferServiceTest {
|
||||
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, declineTransfer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyWhenExecutePendingBuybackTransferThenBusinessEventIsSent() {
|
||||
@ParameterizedTest
|
||||
@MethodSource("buybackStatusDataProvider")
|
||||
public void verifyWhenExecutePendingBuybackTransferThenBusinessEventIsSent(final ExternalTransferStatus buybackStatus) {
|
||||
// given
|
||||
final Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||
when(loanForProcessing.getId()).thenReturn(1L);
|
||||
@@ -136,7 +190,7 @@ public class LoanAccountOwnerTransferServiceTest {
|
||||
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
|
||||
|
||||
ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||
when(pendingBuybackTransfer.getStatus()).thenReturn(BUYBACK);
|
||||
when(pendingBuybackTransfer.getStatus()).thenReturn(buybackStatus);
|
||||
ExternalAssetOwnerTransfer activeTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
|
||||
List<ExternalAssetOwnerTransfer> response = List.of(pendingBuybackTransfer);
|
||||
|
||||
@@ -154,6 +208,14 @@ public class LoanAccountOwnerTransferServiceTest {
|
||||
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
|
||||
private ArgumentCaptor<BusinessEvent<?>> verifyBusinessEvents(int expectedBusinessEvents) {
|
||||
@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;
|
||||
|
||||
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_INTERMEDIATE;
|
||||
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.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_ZERO;
|
||||
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.SAMEDAY_TRANSFERS;
|
||||
@@ -67,35 +71,61 @@ public class InvestorBusinessEventSerializerTest {
|
||||
|
||||
@Test
|
||||
public void testSerializationSellOK() {
|
||||
doTest(ACTIVE, null, "SALE", "EXECUTED", null);
|
||||
doTest(ACTIVE, null, null, "SALE", "EXECUTED", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
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
|
||||
public void testSerializationDeclinedNegativeBalance() {
|
||||
doTest(DECLINED, BALANCE_NEGATIVE, "SALE", "DECLINED", "BALANCE_NEGATIVE");
|
||||
doTest(DECLINED, BALANCE_NEGATIVE, PENDING, "SALE", "DECLINED", "BALANCE_NEGATIVE");
|
||||
}
|
||||
|
||||
@Test
|
||||
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
|
||||
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,
|
||||
String expectedReason) {
|
||||
@Test
|
||||
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
|
||||
ExternalAssetOwnersReadService mockReadService = Mockito.mock(ExternalAssetOwnersReadService.class);
|
||||
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);
|
||||
when(loan.getCurrency()).thenReturn(new MonetaryCurrency("EUR", 2, 1));
|
||||
List<LoanCharge> loanCharges = createMockCharges();
|
||||
|
||||
+3
-1
@@ -24,9 +24,11 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class LoanIdAndExternalIdAndStatus {
|
||||
public class LoanDataForExternalTransfer {
|
||||
|
||||
Long id;
|
||||
ExternalId externalId;
|
||||
Integer loanStatus;
|
||||
Long loanProductId;
|
||||
String loanProductShortName;
|
||||
}
|
||||
+4
-4
@@ -22,8 +22,8 @@ import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.cob.data.LoanDataForExternalTransfer;
|
||||
import org.apache.fineract.cob.data.LoanIdAndExternalIdAndAccountNo;
|
||||
import org.apache.fineract.cob.data.LoanIdAndExternalIdAndStatus;
|
||||
import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
@@ -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_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 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)
|
||||
Loan findLoanAccountByAccountNumber(@Param("accountNumber") String accountNumber);
|
||||
|
||||
@Query(FIND_LOAN_ID_AND_EXTERNAL_ID_AND_STATUS)
|
||||
Optional<LoanIdAndExternalIdAndStatus> findLoanIdAndExternalIdAndStatusByLoanId(@Param("loanId") Long loanId);
|
||||
@Query(FIND_LOAN_DATA_FOR_EXTERNAL_TRANSFER)
|
||||
Optional<LoanDataForExternalTransfer> findLoanDataForExternalTransferByLoanId(@Param("loanId") Long loanId);
|
||||
|
||||
@Query(EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID)
|
||||
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/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/0168_transaction_summary_with_asset_owner_report_add_active_intermediate_filtering.xml" relativeToChangelogFile="true" />
|
||||
</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) {
|
||||
String transferExternalId = UUID.randomUUID().toString();
|
||||
String transferExternalGroupId = 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,
|
||||
String ownerExternalId, String purchasePriceRatio) {
|
||||
String transferExternalGroupId, String ownerExternalId, String purchasePriceRatio) {
|
||||
PostInitiateTransferResponse saleResponse = EXTERNAL_ASSET_OWNER_HELPER.initiateTransferByLoanId(loanID.longValue(), "sale",
|
||||
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());
|
||||
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.LoanTransactionHelper;
|
||||
import org.apache.fineract.integrationtests.common.report.ReportHelper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
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);
|
||||
|
||||
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(REQUEST_SPEC, RESPONSE_SPEC, loanID);
|
||||
LoanStatus loanStatus = LoanStatus.fromInt((Integer) loanStatusHashMap.get("id"));
|
||||
|
||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||
() -> 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 {
|
||||
cleanUpAndRestoreBusinessDate();
|
||||
}
|
||||
@@ -868,10 +872,12 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
||||
assertTrue(exception5.getMessage().contains("Loan with identifier -1 does not exist"));
|
||||
|
||||
String externalId = UUID.randomUUID().toString();
|
||||
String transferExternalGroupId = UUID.randomUUID().toString();
|
||||
|
||||
CallFailedRuntimeException exception6 = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
Integer clientID = createClient();
|
||||
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);
|
||||
});
|
||||
assertTrue(exception6.getMessage()
|
||||
@@ -892,12 +898,12 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, () -> createSaleTransfer(loanID, null));
|
||||
assertTrue(exception.getMessage().contains("The parameter `settlementDate` is mandatory."));
|
||||
|
||||
CallFailedRuntimeException exception2 = assertThrows(CallFailedRuntimeException.class,
|
||||
() -> createSaleTransfer(loanID, "2020-03-02", UUID.randomUUID().toString(), null, "1.0"));
|
||||
CallFailedRuntimeException exception2 = assertThrows(CallFailedRuntimeException.class, () -> createSaleTransfer(loanID,
|
||||
"2020-03-02", UUID.randomUUID().toString(), UUID.randomUUID().toString(), null, "1.0"));
|
||||
assertTrue(exception2.getMessage().contains("The parameter `ownerExternalId` is mandatory."));
|
||||
|
||||
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."));
|
||||
|
||||
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"));
|
||||
String externalId = UUID.randomUUID().toString();
|
||||
String transferExternalGroupId = UUID.randomUUID().toString();
|
||||
CallFailedRuntimeException exception7 = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||
Integer loanID2 = createLoanForClient(clientID);
|
||||
createSaleTransfer(loanID2, "2020-03-05", externalId, "1", "1.0");
|
||||
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, transferExternalGroupId, "1", "1.0");
|
||||
});
|
||||
assertTrue(exception7.getMessage()
|
||||
.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) {
|
||||
String transferExternalId = UUID.randomUUID().toString();
|
||||
String transferExternalGroupId = 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,
|
||||
String ownerExternalId, String purchasePriceRatio) {
|
||||
String transferExternalGroupId, String ownerExternalId, String purchasePriceRatio) {
|
||||
PostInitiateTransferResponse saleResponse = EXTERNAL_ASSET_OWNER_HELPER.initiateTransferByLoanId(loanID.longValue(), "sale",
|
||||
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());
|
||||
return saleResponse;
|
||||
}
|
||||
@@ -1315,6 +1324,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
||||
assertTrue(first.isPresent());
|
||||
ExternalTransferData etd = first.get();
|
||||
assertEquals(expected.transferExternalId, etd.getTransferExternalId());
|
||||
|
||||
assertEquals(expected.status, etd.getStatus());
|
||||
assertEquals(LocalDate.parse(expected.settlementDate), etd.getSettlementDate());
|
||||
assertEquals(LocalDate.parse(expected.effectiveFrom), etd.getEffectiveFrom());
|
||||
@@ -1415,6 +1425,7 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
||||
private final String effectiveTo;
|
||||
private final ExternalTransferData.SubStatusEnum subStatus;
|
||||
private final boolean detailsExpected;
|
||||
|
||||
private final BigDecimal totalOutstanding;
|
||||
private final BigDecimal totalPrincipalOutstanding;
|
||||
private final BigDecimal totalInterestOutstanding;
|
||||
|
||||
Reference in New Issue
Block a user