Skip to content

Commit 35ebfd6

Browse files
committed
FINERACT-2541: owner to owner transfer functionality
1 parent 2454f9d commit 35ebfd6

File tree

6 files changed

+357
-42
lines changed

6 files changed

+357
-42
lines changed

fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
4747
import org.springframework.context.annotation.Conditional;
4848
import org.springframework.data.domain.Sort;
49+
import org.springframework.lang.Nullable;
4950
import org.springframework.stereotype.Component;
5051

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

176-
ExternalAssetOwnerTransfer newTransfer = activatePendingEntry(settlementDate, externalAssetOwnerTransfer, activeStatus);
177+
ExternalAssetOwnerTransfer newTransfer = activatePendingEntry(settlementDate, externalAssetOwnerTransfer, activeStatus,
178+
previousOwner);
179+
177180
loanJournalEntryPoster.postJournalEntriesForExternalOwnerTransfer(loan, newTransfer, previousOwner);
178181
return newTransfer;
179182
}
180183

181184
private ExternalAssetOwner determinePreviousOwnerAndCleanupIfNeeded(final Loan loan, final LocalDate settlementDate,
182185
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
183-
if (!delayedSettlementAttributeService.isEnabled(loan.getLoanProduct().getId())) {
184-
// When delayed settlement is disabled, asset is directly sold to investor, and we are the previous owner.
185-
return null;
186-
}
187-
188-
if (ExternalTransferStatus.PENDING_INTERMEDIATE == externalAssetOwnerTransfer.getStatus()) {
189-
// When delayed settlement is enabled and asset is sold to intermediate, we are the previous owner.
190-
return null;
186+
if (!delayedSettlementAttributeService.isEnabled(loan.getLoanProduct().getId())
187+
|| ExternalTransferStatus.PENDING_INTERMEDIATE == externalAssetOwnerTransfer.getStatus()) {
188+
// Use the loan mapping as the source of truth for the current owner.
189+
// If a mapping exists, this is an owner-to-owner transfer — expire the current active and clean up.
190+
return expireCurrentOwnerIfPresent(loan, settlementDate);
191191
}
192192

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

202+
@Nullable
203+
private ExternalAssetOwner expireCurrentOwnerIfPresent(final Loan loan, final LocalDate settlementDate) {
204+
Optional<ExternalAssetOwnerTransfer> activeTransfer = externalAssetOwnerTransferRepository.findActiveByLoanId(loan.getId());
205+
if (activeTransfer.isPresent()) {
206+
ExternalAssetOwnerTransfer currentActiveTransfer = activeTransfer.get();
207+
expireTransfer(settlementDate, currentActiveTransfer);
208+
externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(), currentActiveTransfer);
209+
return currentActiveTransfer.getOwner();
210+
}
211+
// Internal-to-external transfer: no previous external owner
212+
return null;
213+
}
214+
202215
private ExternalTransferStatus determineActiveStatus(final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
203216
if (ExternalTransferStatus.PENDING_INTERMEDIATE == externalAssetOwnerTransfer.getStatus()) {
204217
return ExternalTransferStatus.ACTIVE_INTERMEDIATE;
@@ -208,13 +221,16 @@ private ExternalTransferStatus determineActiveStatus(final ExternalAssetOwnerTra
208221
}
209222

210223
private ExternalAssetOwnerTransfer getActiveIntermediateOrThrow(final Loan loan) {
211-
Optional<ExternalAssetOwnerTransfer> optionalActiveIntermediateTransfer = externalAssetOwnerTransferRepository
224+
Optional<ExternalAssetOwnerTransfer> optionalActiveIntermediateTransfer = findActiveIntermediateTransfer(loan);
225+
return optionalActiveIntermediateTransfer
226+
.orElseThrow(() -> new IllegalStateException("Expected a effective transfer of ACTIVE_INTERMEDIATE status to be present."));
227+
}
228+
229+
private Optional<ExternalAssetOwnerTransfer> findActiveIntermediateTransfer(final Loan loan) {
230+
return externalAssetOwnerTransferRepository
212231
.findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
213232
criteriaBuilder.equal(root.get("status"), ExternalTransferStatus.ACTIVE_INTERMEDIATE),
214233
criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)));
215-
216-
return optionalActiveIntermediateTransfer
217-
.orElseThrow(() -> new IllegalStateException("Expected a effective transfer of ACTIVE_INTERMEDIATE status to be present."));
218234
}
219235

220236
private ExternalAssetOwnerTransferDetails createAssetOwnerTransferDetails(Loan loan,
@@ -253,9 +269,11 @@ private ExternalAssetOwnerTransfer cancelTransfer(final LocalDate settlementDate
253269
}
254270

255271
private ExternalAssetOwnerTransfer activatePendingEntry(final LocalDate settlementDate,
256-
final ExternalAssetOwnerTransfer pendingTransfer, final ExternalTransferStatus activeStatus) {
272+
final ExternalAssetOwnerTransfer pendingTransfer, final ExternalTransferStatus activeStatus,
273+
final ExternalAssetOwner previousOwner) {
257274
LocalDate effectiveFrom = settlementDate.plusDays(1);
258-
return createNewEntryAndExpireOldEntry(settlementDate, pendingTransfer, activeStatus, null, effectiveFrom, FUTURE_DATE_9999_12_31);
275+
return createNewEntryAndExpireOldEntry(settlementDate, pendingTransfer, activeStatus, null, effectiveFrom, FUTURE_DATE_9999_12_31,
276+
previousOwner);
259277
}
260278

261279
private ExternalAssetOwnerTransfer declinePendingEntry(final Loan loan, final LocalDate settlementDate,
@@ -267,6 +285,14 @@ private ExternalAssetOwnerTransfer declinePendingEntry(final Loan loan, final Lo
267285
private ExternalAssetOwnerTransfer createNewEntryAndExpireOldEntry(final LocalDate settlementDate,
268286
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer, final ExternalTransferStatus status,
269287
final ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo) {
288+
return createNewEntryAndExpireOldEntry(settlementDate, externalAssetOwnerTransfer, status, subStatus, effectiveDateFrom,
289+
effectiveDateTo, null);
290+
}
291+
292+
private ExternalAssetOwnerTransfer createNewEntryAndExpireOldEntry(final LocalDate settlementDate,
293+
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer, final ExternalTransferStatus status,
294+
final ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo,
295+
final ExternalAssetOwner previousOwner) {
270296
ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
271297
newExternalAssetOwnerTransfer.setOwner(externalAssetOwnerTransfer.getOwner());
272298
newExternalAssetOwnerTransfer.setExternalId(externalAssetOwnerTransfer.getExternalId());
@@ -279,7 +305,8 @@ private ExternalAssetOwnerTransfer createNewEntryAndExpireOldEntry(final LocalDa
279305
newExternalAssetOwnerTransfer.setPurchasePriceRatio(externalAssetOwnerTransfer.getPurchasePriceRatio());
280306
newExternalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
281307
newExternalAssetOwnerTransfer.setEffectiveDateTo(effectiveDateTo);
282-
newExternalAssetOwnerTransfer.setPreviousOwner(externalAssetOwnerTransfer.getPreviousOwner());
308+
newExternalAssetOwnerTransfer
309+
.setPreviousOwner(previousOwner != null ? previousOwner : externalAssetOwnerTransfer.getPreviousOwner());
283310

284311
expireTransfer(settlementDate, externalAssetOwnerTransfer);
285312

fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -178,16 +178,15 @@ private void validateEffectiveTransferForSale(final List<ExternalAssetOwnerTrans
178178
if (effectiveTransfers.size() == 2) {
179179
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
180180
} else if (effectiveTransfers.size() == 1) {
181-
if (PENDING.equals(effectiveTransfers.getFirst().getStatus())) {
181+
ExternalAssetOwnerTransfer transfer = effectiveTransfers.getFirst();
182+
ExternalTransferStatus transferStatus = transfer.getStatus();
183+
if (PENDING.equals(transferStatus)) {
182184
throw new ExternalAssetOwnerInitiateTransferException(
183185
"External asset owner transfer is already in PENDING state for this loan");
184-
} else if (ExternalTransferStatus.ACTIVE.equals(effectiveTransfers.getFirst().getStatus())) {
185-
throw new ExternalAssetOwnerInitiateTransferException(
186-
"This loan cannot be sold, because it is owned by an external asset owner");
187-
} else {
186+
}
187+
if (!ExternalTransferStatus.ACTIVE.equals(transferStatus)) {
188188
throw new ExternalAssetOwnerInitiateTransferException(
189-
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)",
190-
effectiveTransfers.getFirst().getId()));
189+
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)", transfer.getId()));
191190
}
192191
}
193192
}
@@ -212,17 +211,18 @@ private void validateEffectiveTransferForIntermediarySale(final ExternalAssetOwn
212211
if (effectiveTransfers.size() > 1) {
213212
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
214213
} else if (effectiveTransfers.size() == 1) {
215-
if (PENDING_INTERMEDIATE.equals(effectiveTransfers.getFirst().getStatus())) {
214+
ExternalAssetOwnerTransfer transfer = effectiveTransfers.getFirst();
215+
ExternalTransferStatus transferStatus = transfer.getStatus();
216+
if (PENDING_INTERMEDIATE.equals(transferStatus)) {
216217
throw new ExternalAssetOwnerInitiateTransferException(
217218
"External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan");
218-
} else if (ExternalTransferStatus.ACTIVE.equals(effectiveTransfers.getFirst().getStatus())) {
219-
throw new ExternalAssetOwnerInitiateTransferException(
220-
"This loan cannot be sold, because it is owned by an external asset owner");
221-
} else {
219+
}
220+
if (!ExternalTransferStatus.ACTIVE.equals(transferStatus)) {
222221
throw new ExternalAssetOwnerInitiateTransferException(
223-
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)",
224-
effectiveTransfers.getFirst().getId()));
222+
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)", transfer.getId()));
225223
}
224+
// Owner-to-owner transfer with delayed settlement: allow intermediarySale when loan is currently
225+
// owned. The actual ownership switch happens atomically in the COB step.
226226
}
227227
}
228228

fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,8 @@ public void testSaleLoanWithDelayedSettlementFromIntermediateToInvestor() {
494494

495495
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
496496
.forClass(ExternalAssetOwnerTransfer.class);
497+
// 3 saves: activeIntermediateTransfer (expire), pendingTransfer (expire), activeTransfer (new, with previous
498+
// owner set)
497499
verify(externalAssetOwnerTransferRepository, times(3)).save(externalAssetOwnerTransferArgumentCaptor.capture());
498500
ExternalAssetOwnerTransfer capturedActiveIntermediateTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0);
499501
ExternalAssetOwnerTransfer capturedPendingTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1);

fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceTest.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -392,9 +392,7 @@ private static Stream<Arguments> effectiveTransferDataProviderIntermediarySaleTe
392392
Arguments.of("Already In Progress", List.of(activeIntermediate, active),
393393
"This loan cannot be sold, there is already an in progress transfer"),
394394
Arguments.of("Already Pending Intermediary", List.of(pendingIntermediate),
395-
"External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan"),
396-
Arguments.of("Already Owned by External Asset Owner", List.of(active),
397-
"This loan cannot be sold, because it is owned by an external asset owner"));
395+
"External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan"));
398396
}
399397

400398
private static Stream<Arguments> loanStatusValidationDataProviderValidActive() {
@@ -744,8 +742,6 @@ private static Stream<Arguments> invalidTransferStatusDataProvider() {
744742
return Stream.of(
745743
Arguments.of(ExternalTransferStatus.PENDING, false,
746744
"External asset owner transfer is already in PENDING state for this loan"),
747-
Arguments.of(ExternalTransferStatus.ACTIVE, false,
748-
"This loan cannot be sold, because it is owned by an external asset owner"),
749745
Arguments.of(ExternalTransferStatus.PENDING_INTERMEDIATE, true,
750746
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
751747
Arguments.of(ExternalTransferStatus.ACTIVE, true,

0 commit comments

Comments
 (0)