Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ Feature: Asset Externalization - Part1
| sale | 2023-05-21 | 1 |

@TestRailId:C2735
Scenario: Verify that SALES request on a loan with ACTIVE ownership results an error
Scenario: Verify that SALES request on a loan with ACTIVE ownership succeeds (owner-to-owner transfer)
When Admin sets the business date to "1 May 2023"
When Admin creates a client with random data
When Admin creates a new default Loan with date: "1 May 2023"
Expand All @@ -304,9 +304,10 @@ Feature: Asset Externalization - Part1
| 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE |
| 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 9999-12-31 | SALE |
When Admin sets the business date to "25 May 2023"
Then Asset externalization transaction with the following data results a 403 error and "ASSET_OWNED_CANNOT_BE_SOLD" error message
When Admin makes asset externalization request by Loan ID with unique ownerExternalId, system-generated transferExternalId and the following data:
| Transaction type | settlementDate | purchasePriceRatio |
| sale | 2023-05-30 | 1 |
Then Asset externalization response has the correct Loan ID, transferExternalId

@TestRailId:C2736
Scenario: Verify that BUYBACK request on a fully paid loan can be done successfully
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
import org.springframework.context.annotation.Conditional;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

@Component
Expand Down Expand Up @@ -173,21 +174,20 @@ private ExternalAssetOwnerTransfer sellAsset(final Loan loan, final LocalDate se
ExternalAssetOwner previousOwner = determinePreviousOwnerAndCleanupIfNeeded(loan, settlementDate, externalAssetOwnerTransfer);
ExternalTransferStatus activeStatus = determineActiveStatus(externalAssetOwnerTransfer);

ExternalAssetOwnerTransfer newTransfer = activatePendingEntry(settlementDate, externalAssetOwnerTransfer, activeStatus);
ExternalAssetOwnerTransfer newTransfer = activatePendingEntry(settlementDate, externalAssetOwnerTransfer, activeStatus,
previousOwner);

loanJournalEntryPoster.postJournalEntriesForExternalOwnerTransfer(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;
if (!delayedSettlementAttributeService.isEnabled(loan.getLoanProduct().getId())
|| ExternalTransferStatus.PENDING_INTERMEDIATE == externalAssetOwnerTransfer.getStatus()) {
// Use the loan mapping as the source of truth for the current owner.
// If a mapping exists, this is an owner-to-owner transfer — expire the current active and clean up.
return expireCurrentOwnerIfPresent(loan, settlementDate);
}

// When delayed settlement is enabled and asset is sold from intermediate to investor, the intermediate is the
Expand All @@ -199,6 +199,19 @@ private ExternalAssetOwner determinePreviousOwnerAndCleanupIfNeeded(final Loan l
return activeIntermediateTransfer.getOwner();
}

@Nullable
private ExternalAssetOwner expireCurrentOwnerIfPresent(final Loan loan, final LocalDate settlementDate) {
Optional<ExternalAssetOwnerTransfer> activeTransfer = externalAssetOwnerTransferRepository.findActiveByLoanId(loan.getId());
if (activeTransfer.isPresent()) {
ExternalAssetOwnerTransfer currentActiveTransfer = activeTransfer.get();
expireTransfer(settlementDate, currentActiveTransfer);
externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(), currentActiveTransfer);
return currentActiveTransfer.getOwner();
}
// Internal-to-external transfer: no previous external owner
return null;
}

private ExternalTransferStatus determineActiveStatus(final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
if (ExternalTransferStatus.PENDING_INTERMEDIATE == externalAssetOwnerTransfer.getStatus()) {
return ExternalTransferStatus.ACTIVE_INTERMEDIATE;
Expand All @@ -208,13 +221,16 @@ private ExternalTransferStatus determineActiveStatus(final ExternalAssetOwnerTra
}

private ExternalAssetOwnerTransfer getActiveIntermediateOrThrow(final Loan loan) {
Optional<ExternalAssetOwnerTransfer> optionalActiveIntermediateTransfer = externalAssetOwnerTransferRepository
Optional<ExternalAssetOwnerTransfer> optionalActiveIntermediateTransfer = findActiveIntermediateTransfer(loan);
return optionalActiveIntermediateTransfer
.orElseThrow(() -> new IllegalStateException("Expected a effective transfer of ACTIVE_INTERMEDIATE status to be present."));
}

private Optional<ExternalAssetOwnerTransfer> findActiveIntermediateTransfer(final Loan loan) {
return 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,
Expand Down Expand Up @@ -253,9 +269,11 @@ private ExternalAssetOwnerTransfer cancelTransfer(final LocalDate settlementDate
}

private ExternalAssetOwnerTransfer activatePendingEntry(final LocalDate settlementDate,
final ExternalAssetOwnerTransfer pendingTransfer, final ExternalTransferStatus activeStatus) {
final ExternalAssetOwnerTransfer pendingTransfer, final ExternalTransferStatus activeStatus,
final ExternalAssetOwner previousOwner) {
LocalDate effectiveFrom = settlementDate.plusDays(1);
return createNewEntryAndExpireOldEntry(settlementDate, pendingTransfer, activeStatus, null, effectiveFrom, FUTURE_DATE_9999_12_31);
return createNewEntryAndExpireOldEntry(settlementDate, pendingTransfer, activeStatus, null, effectiveFrom, FUTURE_DATE_9999_12_31,
previousOwner);
}

private ExternalAssetOwnerTransfer declinePendingEntry(final Loan loan, final LocalDate settlementDate,
Expand All @@ -267,6 +285,14 @@ private ExternalAssetOwnerTransfer declinePendingEntry(final Loan loan, final Lo
private ExternalAssetOwnerTransfer createNewEntryAndExpireOldEntry(final LocalDate settlementDate,
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer, final ExternalTransferStatus status,
final ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo) {
return createNewEntryAndExpireOldEntry(settlementDate, externalAssetOwnerTransfer, status, subStatus, effectiveDateFrom,
effectiveDateTo, null);
}

private ExternalAssetOwnerTransfer createNewEntryAndExpireOldEntry(final LocalDate settlementDate,
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer, final ExternalTransferStatus status,
final ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo,
final ExternalAssetOwner previousOwner) {
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
newExternalAssetOwnerTransfer.setOwner(externalAssetOwnerTransfer.getOwner());
newExternalAssetOwnerTransfer.setExternalId(externalAssetOwnerTransfer.getExternalId());
Expand All @@ -279,7 +305,8 @@ private ExternalAssetOwnerTransfer createNewEntryAndExpireOldEntry(final LocalDa
newExternalAssetOwnerTransfer.setPurchasePriceRatio(externalAssetOwnerTransfer.getPurchasePriceRatio());
newExternalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
newExternalAssetOwnerTransfer.setEffectiveDateTo(effectiveDateTo);
newExternalAssetOwnerTransfer.setPreviousOwner(externalAssetOwnerTransfer.getPreviousOwner());
newExternalAssetOwnerTransfer
.setPreviousOwner(previousOwner != null ? previousOwner : externalAssetOwnerTransfer.getPreviousOwner());

expireTransfer(settlementDate, externalAssetOwnerTransfer);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,15 @@ private void validateEffectiveTransferForSale(final List<ExternalAssetOwnerTrans
if (effectiveTransfers.size() == 2) {
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
} else if (effectiveTransfers.size() == 1) {
if (PENDING.equals(effectiveTransfers.getFirst().getStatus())) {
ExternalAssetOwnerTransfer transfer = effectiveTransfers.getFirst();
ExternalTransferStatus transferStatus = transfer.getStatus();
if (PENDING.equals(transferStatus)) {
throw new ExternalAssetOwnerInitiateTransferException(
"External asset owner transfer is already in PENDING state for this loan");
} else if (ExternalTransferStatus.ACTIVE.equals(effectiveTransfers.getFirst().getStatus())) {
throw new ExternalAssetOwnerInitiateTransferException(
"This loan cannot be sold, because it is owned by an external asset owner");
} else {
}
if (!ExternalTransferStatus.ACTIVE.equals(transferStatus)) {
throw new ExternalAssetOwnerInitiateTransferException(
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)",
effectiveTransfers.getFirst().getId()));
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)", transfer.getId()));
}
}
}
Expand All @@ -212,17 +211,18 @@ private void validateEffectiveTransferForIntermediarySale(final ExternalAssetOwn
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.getFirst().getStatus())) {
ExternalAssetOwnerTransfer transfer = effectiveTransfers.getFirst();
ExternalTransferStatus transferStatus = transfer.getStatus();
if (PENDING_INTERMEDIATE.equals(transferStatus)) {
throw new ExternalAssetOwnerInitiateTransferException(
"External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan");
} else if (ExternalTransferStatus.ACTIVE.equals(effectiveTransfers.getFirst().getStatus())) {
throw new ExternalAssetOwnerInitiateTransferException(
"This loan cannot be sold, because it is owned by an external asset owner");
} else {
}
if (!ExternalTransferStatus.ACTIVE.equals(transferStatus)) {
throw new ExternalAssetOwnerInitiateTransferException(
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)",
effectiveTransfers.getFirst().getId()));
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)", transfer.getId()));
}
// Owner-to-owner transfer with delayed settlement: allow intermediarySale when loan is currently
// owned. The actual ownership switch happens atomically in the COB step.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,8 @@ public void testSaleLoanWithDelayedSettlementFromIntermediateToInvestor() {

ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
// 3 saves: activeIntermediateTransfer (expire), pendingTransfer (expire), activeTransfer (new, with previous
// owner set)
verify(externalAssetOwnerTransferRepository, times(3)).save(externalAssetOwnerTransferArgumentCaptor.capture());
ExternalAssetOwnerTransfer capturedActiveIntermediateTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0);
ExternalAssetOwnerTransfer capturedPendingTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,9 +392,7 @@ private static Stream<Arguments> effectiveTransferDataProviderIntermediarySaleTe
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"));
"External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan"));
}

private static Stream<Arguments> loanStatusValidationDataProviderValidActive() {
Expand Down Expand Up @@ -744,8 +742,6 @@ 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,
Expand Down
Loading
Loading