diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java index 1c2c1871591..ec86e22b53c 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java @@ -147,6 +147,10 @@ public boolean isDebitEntry() { 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; } diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanOwnershipTransferDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanOwnershipTransferDataV1.avsc index 8251980f271..7d50360142a 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanOwnershipTransferDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanOwnershipTransferDataV1.avsc @@ -31,6 +31,14 @@ "string" ] }, + { + "default": null, + "name": "transferExternalReferenceId", + "type": [ + "null", + "string" + ] + }, { "default": null, "name": "submittedDate", diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 8b3ec4ad039..3823aff7990 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -3661,6 +3661,14 @@ public CommandWrapperBuilder updateExternalAssetOwnerLoanProductAttribute(final 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"; diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java index 5b07f76bf2a..a55ae628dff 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java @@ -48,6 +48,7 @@ public class CommandProcessingResult implements Serializable { private final Long glimId; private Boolean rollbackTransaction; private final ExternalId resourceExternalId; + private final ExternalId resourceExternalReferenceId; private final ExternalId subResourceExternalId; private final ExternalId loanExternalId; @@ -55,7 +56,8 @@ private CommandProcessingResult(final Long commandId, final Long officeId, final final Long savingsId, final String resourceIdentifier, final Long resourceId, final String transactionId, final Map changes, final Long productId, final Long gsimId, final Long glimId, final Map creditBureauReportData, Boolean rollbackTransaction, final Long subResourceId, - final ExternalId resourceExternalId, final ExternalId subResourceExternalId, final ExternalId loanExternalId) { + final ExternalId resourceExternalId, final ExternalId resourceExternalReferenceId, final ExternalId subResourceExternalId, + final ExternalId loanExternalId) { this.commandId = commandId; this.officeId = officeId; this.groupId = groupId; @@ -73,6 +75,7 @@ private CommandProcessingResult(final Long commandId, final Long officeId, final this.rollbackTransaction = rollbackTransaction; this.subResourceId = subResourceId; this.resourceExternalId = resourceExternalId; + this.resourceExternalReferenceId = resourceExternalReferenceId; this.subResourceExternalId = subResourceExternalId; this.loanExternalId = loanExternalId; } @@ -80,7 +83,7 @@ private CommandProcessingResult(final Long commandId, final Long officeId, final protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, final Map changes, Long clientId) { this(commandId, officeId, null, clientId, null, null, resourceId == null ? null : resourceId.toString(), resourceId, null, changes, - null, null, null, null, null, null, ExternalId.empty(), ExternalId.empty(), ExternalId.empty()); + null, null, null, null, null, null, ExternalId.empty(), ExternalId.empty(), ExternalId.empty(), ExternalId.empty()); } protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, final Map changes) { @@ -96,7 +99,8 @@ public static CommandProcessingResult fromCommandProcessingResult(CommandProcess commandResult.loanId, commandResult.savingsId, commandResult.resourceIdentifier, resourceId, commandResult.transactionId, commandResult.changes, commandResult.productId, commandResult.gsimId, commandResult.glimId, commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId, - commandResult.resourceExternalId, commandResult.subResourceExternalId, commandResult.loanExternalId); + commandResult.resourceExternalId, commandResult.resourceExternalReferenceId, commandResult.subResourceExternalId, + commandResult.loanExternalId); } public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult) { @@ -107,10 +111,11 @@ public static CommandProcessingResult fromDetails(final Long commandId, final Lo final Long loanId, final Long savingsId, final String resourceIdentifier, final Long entityId, final Long gsimId, final Long glimId, final Map creditBureauReportData, final String transactionId, final Map changes, final Long productId, final Boolean rollbackTransaction, final Long subResourceId, - final ExternalId resourceExternalId, final ExternalId subResourceExternalId, final ExternalId loanExternalId) { + final ExternalId resourceExternalId, final ExternalId resourceExternalReferenceId, final ExternalId subResourceExternalId, + final ExternalId loanExternalId) { return new CommandProcessingResult(commandId, officeId, groupId, clientId, loanId, savingsId, resourceIdentifier, entityId, transactionId, changes, productId, gsimId, glimId, creditBureauReportData, rollbackTransaction, subResourceId, - resourceExternalId, subResourceExternalId, loanExternalId); + resourceExternalId, resourceExternalReferenceId, subResourceExternalId, loanExternalId); } public static CommandProcessingResult commandOnlyResult(final Long commandId) { diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java index e630ba7076a..743bd6d020b 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java @@ -43,16 +43,15 @@ public class CommandProcessingResultBuilder { private Long productId; private boolean rollbackTransaction = false; private ExternalId entityExternalId = ExternalId.empty(); - + private ExternalId entityExternalReferenceId = ExternalId.empty(); private ExternalId subEntityExternalId = ExternalId.empty(); - private ExternalId loanExternalId = ExternalId.empty(); public CommandProcessingResult build() { return CommandProcessingResult.fromDetails(this.commandId, this.officeId, this.groupId, this.clientId, this.loanId, this.savingsId, this.resourceIdentifier, this.entityId, this.gsimId, this.glimId, this.creditBureauReportData, this.transactionId, - this.changes, this.productId, this.rollbackTransaction, this.subEntityId, this.entityExternalId, this.subEntityExternalId, - this.loanExternalId); + this.changes, this.productId, this.rollbackTransaction, this.subEntityId, this.entityExternalId, + this.entityExternalReferenceId, this.subEntityExternalId, this.loanExternalId); } public CommandProcessingResultBuilder withCommandId(final Long withCommandId) { @@ -140,6 +139,11 @@ public CommandProcessingResultBuilder withEntityExternalId(final ExternalId enti return this; } + public CommandProcessingResultBuilder withEntityExternalReferenceId(final ExternalId entityExternalReferenceId) { + this.entityExternalReferenceId = entityExternalReferenceId; + return this; + } + public CommandProcessingResultBuilder withSubEntityExternalId(final ExternalId subEntityExternalId) { this.subEntityExternalId = subEntityExternalId; return this; diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java index 3f92519b09d..e1bfb974729 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResource.java @@ -220,7 +220,9 @@ private String getResultByTransferId(Long id, String command) { 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(); diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResourceSwagger.java b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResourceSwagger.java index e8c032d1cd2..776d7760519 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResourceSwagger.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/api/ExternalAssetOwnersApiResourceSwagger.java @@ -61,6 +61,9 @@ private GetExternalTransferLoan() {} @Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e") public String transferExternalId; + @Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e") + public String transferExternalReferenceId; + @Schema(example = "1") public String purchasePriceRatio; @@ -101,6 +104,9 @@ private PostInitiateTransferRequest() {} @Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e") public String transferExternalId; + @Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e") + public String transferExternalReferenceId; + @Schema(example = "1.2345678") public String purchasePriceRatio; @@ -122,6 +128,9 @@ private PostInitiateTransferResponse() {} @Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e", description = "transfer external ID") public String resourceExternalId; + @Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e", description = "transfer external reference ID") + public String resourceExternalReferenceId; + @Schema(example = "2", description = "loan ID") public Long subResourceId; @@ -148,6 +157,9 @@ static final class ExternalAssetOwnerTransferChangesData { @Schema(example = "36efeb06-d835-48a1-99eb-09bd1d348c1e") public String transferExternalId; + @Schema(example = "e1156fbe-38bb-42f8-b491-fca02075f40e") + public String transferExternalReferenceId; + @Schema(example = "1.23456789") public String purchasePriceRatio; } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java index f58aff5812b..df0e86dd23d 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java @@ -23,16 +23,17 @@ 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.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 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep { public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31); + public static final List PENDING_STATUSES = List.of(ExternalTransferStatus.PENDING_INTERMEDIATE, + ExternalTransferStatus.PENDING); + public static final List 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 Loan execute(Loan loan) { List 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 Loan execute(Loan loan) { 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 Loan execute(Loan loan) { 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 Loan execute(Loan loan) { } 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 @@ private void handleSale(final Loan loan, final LocalDate settlementDate, final E private void handleBuyback(final Loan loan, final LocalDate settlementDate, final ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer) { + final ExternalTransferStatus expectedActiveStatus = determineExpectedActiveStatus(buybackExternalAssetOwnerTransfer); + Optional 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 @@ private ExternalAssetOwnerTransfer buybackAsset(final Loan loan, final LocalDate 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 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 @@ private void createActiveMapping(Long loanId, ExternalAssetOwnerTransfer externa 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 transferDataList, Loan loan) { ExternalAssetOwnerTransfer cancelledPendingTransfer = cancelTransfer(settlementDate, transferDataList.get(0)); @@ -190,11 +248,23 @@ private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final L 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 @@ private ExternalAssetOwnerTransfer createNewEntry(final LocalDate settlementDate newExternalAssetOwnerTransfer.setSettlementDate(settlementDate); newExternalAssetOwnerTransfer.setLoanId(externalAssetOwnerTransfer.getLoanId()); newExternalAssetOwnerTransfer.setExternalLoanId(externalAssetOwnerTransfer.getExternalLoanId()); + newExternalAssetOwnerTransfer.setExternalReferenceId(externalAssetOwnerTransfer.getExternalReferenceId()); newExternalAssetOwnerTransfer.setPurchasePriceRatio(externalAssetOwnerTransfer.getPurchasePriceRatio()); newExternalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom); newExternalAssetOwnerTransfer.setEffectiveDateTo(effectiveDateTo); + expireTransfer(settlementDate, externalAssetOwnerTransfer); + + return externalAssetOwnerTransferRepository.save(newExternalAssetOwnerTransfer); + } + + private void expireTransfer(final LocalDate settlementDate, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { externalAssetOwnerTransfer.setEffectiveDateTo(settlementDate); externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer); - return externalAssetOwnerTransferRepository.save(newExternalAssetOwnerTransfer); } - private ExternalAssetOwnerTransfer createActiveEntry(final LocalDate settlementDate, - final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { - LocalDate effectiveFrom = settlementDate.plusDays(1); - return createNewEntry(settlementDate, externalAssetOwnerTransfer, ExternalTransferStatus.ACTIVE, null, effectiveFrom, - FUTURE_DATE_9999_12_31); + private ExternalTransferStatus determineExpectedActiveStatus(final ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer) { + if (ExternalTransferStatus.BUYBACK_INTERMEDIATE == buybackExternalAssetOwnerTransfer.getStatus()) { + return ExternalTransferStatus.ACTIVE_INTERMEDIATE; + } + return ExternalTransferStatus.ACTIVE; } @Override diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/config/LoanAccountOwnerTransferConfiguration.java b/fineract-investor/src/main/java/org/apache/fineract/investor/config/LoanAccountOwnerTransferConfiguration.java new file mode 100644 index 00000000000..7d1844492cd --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/config/LoanAccountOwnerTransferConfiguration.java @@ -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); + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferData.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferData.java index 67027356d2c..19346c4fade 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferData.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferData.java @@ -29,6 +29,7 @@ public class ExternalTransferData { private ExternalTransferLoanData loan; private ExternalTransferDataDetails details; private String transferExternalId; + private String transferExternalReferenceId; private String purchasePriceRatio; private LocalDate settlementDate; private ExternalTransferStatus status; diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferRequestParameters.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferRequestParameters.java index 49338e22464..b15cdc1c7f6 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferRequestParameters.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferRequestParameters.java @@ -25,6 +25,7 @@ private 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_REFERENCE_ID = "transferExternalReferenceId"; public static final String PURCHASE_PRICE_RATIO = "purchasePriceRatio"; public static final String DATEFORMAT = "dateFormat"; public static final String LOCALE = "locale"; diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferResponseData.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferResponseData.java index b6cc3bbd53b..19735ae740a 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferResponseData.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferResponseData.java @@ -25,6 +25,7 @@ public class ExternalTransferResponseData { private Long resourceId; private String resourceExternalId; + private String resourceExternalReferenceId; private Long subResourceId; private String subResourceExternalId; private ExternalTransferChangedData changes; diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferStatus.java b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferStatus.java index 74a410cdfe1..37da59898d1 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferStatus.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferStatus.java @@ -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 } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java index 9a63df58753..1ef94d6e7cd 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java @@ -79,4 +79,6 @@ public class ExternalAssetOwnerTransfer extends AbstractAuditableWithUTCDateTime @Column(name = "external_loan_id", length = 100) private ExternalId externalLoanId; + @Column(name = "external_reference_id", length = 100) + private ExternalId externalReferenceId; } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/search/SearchedExternalAssetOwner.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/search/SearchedExternalAssetOwner.java index 47ac2732184..7e90fc8a1d1 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/search/SearchedExternalAssetOwner.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/search/SearchedExternalAssetOwner.java @@ -36,6 +36,7 @@ public class SearchedExternalAssetOwner { private final ExternalId owner; private final ExternalId transferExternalId; + private final ExternalId transferExternalReferenceId; private final ExternalTransferStatus status; private final ExternalTransferSubStatus subStatus; diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/search/SearchingExternalAssetOwnerRepositoryImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/search/SearchingExternalAssetOwnerRepositoryImpl.java index 25566052fb0..ccab0afe193 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/search/SearchingExternalAssetOwnerRepositoryImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/search/SearchingExternalAssetOwnerRepositoryImpl.java @@ -94,10 +94,11 @@ public Page searchInvestorData(PagedRequest queryToExecute = entityManager.createQuery(query); return criteriaQueryFactory.readPage(queryToExecute, ExternalAssetOwnerTransfer.class, pageable, spec); diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java index 437211da008..08f5615fae1 100755 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingService.java @@ -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); } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java index 394c53111f7..adac64ea55f 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java @@ -31,8 +31,8 @@ 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 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(previousOwner, 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 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(previousOwner, journalEntry); + return; + } + + ExternalAssetOwner owner = determineOwnerForBuyback(journalEntry, loan, previousOwner); + createMappingToOwner(owner, journalEntry); + }); } @NotNull @@ -95,18 +114,52 @@ private List createJournalEntries(Loan loan, ExternalAssetOwnerTra return journalEntryList; } - private void createMappingToOwner(ExternalAssetOwnerTransfer transfer, List 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 journalEntryList) { @@ -211,4 +264,8 @@ private List createJournalEntries(Loan loan, Long transactionId, L } return journalEntryList; } + + private boolean isOwnedByFinancialActivityAccount(JournalEntry journalEntry, FinancialActivityAccount financialActivityAccount) { + return Objects.equals(financialActivityAccount.getGlAccount().getId(), journalEntry.getGlAccount().getId()); + } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/DelayedSettlementAttributeService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/DelayedSettlementAttributeService.java new file mode 100644 index 00000000000..6d8f2a5b51d --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/DelayedSettlementAttributeService.java @@ -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); +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/DelayedSettlementAttributeServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/DelayedSettlementAttributeServiceImpl.java new file mode 100644 index 00000000000..c558f62010c --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/DelayedSettlementAttributeServiceImpl.java @@ -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 attributesDataPage = externalAssetOwnerLoanProductAttributesReadService + .retrieveAllLoanProductAttributesByLoanProductId(loanProductId, DELAYED_SETTLEMENT.getAttributeKey()); + String attributeValue = attributesDataPage.getPageItems().stream().findFirst() + .map(ExternalTransferLoanProductAttributesData::getAttributeValue).orElse(null); + return DELAYED_SETTLEMENT.getAttributeValue().equals(attributeValue); + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersTransferMapper.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersTransferMapper.java index 61491ac6566..4a554e6194d 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersTransferMapper.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersTransferMapper.java @@ -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 = "transferExternalReferenceId", source = "externalReferenceId") @Mapping(target = "effectiveFrom", source = "effectiveDateFrom") @Mapping(target = "effectiveTo", source = "effectiveDateTo") @Mapping(target = "purchasePriceRatio", source = "purchasePriceRatio") diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java index 8e5f7bdf0fb..146a26421cf 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java @@ -23,6 +23,8 @@ public interface ExternalAssetOwnersWriteService { + CommandProcessingResult intermediarySaleLoanByLoanId(JsonCommand jsonCommand); + CommandProcessingResult saleLoanByLoanId(JsonCommand command); CommandProcessingResult buybackLoanByLoanId(JsonCommand command); diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java index c4790b920e5..7fb3016e107 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java @@ -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.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 ACTIVE_LOAN_STATUSES = List.of(ACTIVE, TRANSFER_IN_PROGRESS, TRANSFER_ON_HOLD); + private static final List 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 BUYBACK_READY_STATUSES = List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.ACTIVE); + private static final List 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 CommandProcessingResult saleLoanByLoanId(JsonCommand command) { 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 @@ private void validateExternalId(ExternalId externalId) { } } - 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 CommandProcessingResult cancelTransactionById(JsonCommand command) { return buildResponseData(cancelTransfer); } - private void validateEffectiveTransferForSale(final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { - List effectiveTransfers = externalAssetOwnerTransferRepository - .findEffectiveTransfersOrderByIdDesc(externalAssetOwnerTransfer.getLoanId(), DateUtils.getBusinessLocalDate()); - + private void validateEffectiveTransferForSale(final List 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 @@ private void validateEffectiveTransferForSale(final ExternalAssetOwnerTransfer e } } - private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuyback(final Long loanId, final LocalDate settlementDate) { + private void validateEffectiveTransferForDelayedSettlementSale(final List 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 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 effectiveTransfers = externalAssetOwnerTransferRepository + .findEffectiveTransfersOrderByIdDesc(loanDataForExternalTransfer.getId(), DateUtils.getBusinessLocalDate()); if (effectiveTransfers.size() == 0) { throw new ExternalAssetOwnerInitiateTransferException( @@ -179,6 +246,39 @@ private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuyback(f return effectiveTransfers.get(0); } + private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuybackWithDelayedSettlement( + final LoanDataForExternalTransfer loanDataForExternalTransfer, final LocalDate settlementDate) { + List 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 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 @@ private ExternalAssetOwnerTransfer createBuybackTransfer(ExternalAssetOwnerTrans 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 @@ private ExternalAssetOwnerTransfer createBuybackTransfer(ExternalAssetOwnerTrans 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 @@ private ExternalAssetOwnerTransfer createCancelTransfer(ExternalAssetOwnerTransf externalAssetOwnerTransfer.setSubStatus(ExternalTransferSubStatus.USER_REQUESTED); externalAssetOwnerTransfer.setLoanId(effectiveTransfer.getLoanId()); externalAssetOwnerTransfer.setExternalLoanId(effectiveTransfer.getExternalLoanId()); + externalAssetOwnerTransfer.setExternalReferenceId(effectiveTransfer.getExternalReferenceId()); externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner()); externalAssetOwnerTransfer.setSettlementDate(effectiveTransfer.getSettlementDate()); externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveTransfer.getEffectiveDateFrom()); @@ -235,15 +345,29 @@ private ExternalAssetOwnerTransfer createCancelTransfer(ExternalAssetOwnerTransf private CommandProcessingResult buildResponseData(ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer) { return new CommandProcessingResultBuilder().withEntityId(savedExternalAssetOwnerTransfer.getId()) .withEntityExternalId(savedExternalAssetOwnerTransfer.getExternalId()) + .withEntityExternalReferenceId(savedExternalAssetOwnerTransfer.getExternalReferenceId()) .withSubEntityId(savedExternalAssetOwnerTransfer.getLoanId()) .withSubEntityExternalId(Objects.isNull(savedExternalAssetOwnerTransfer.getExternalLoanId()) ? null : savedExternalAssetOwnerTransfer.getExternalLoanId()) .build(); } - private void validateSale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { + private void validateSale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer, boolean isDelayedSettlementEnabled) { + validateSettlementDate(externalAssetOwnerTransfer); + + final List effectiveTransfers = externalAssetOwnerTransferRepository + .findEffectiveTransfersOrderByIdDesc(externalAssetOwnerTransfer.getLoanId(), DateUtils.getBusinessLocalDate()); + + if (isDelayedSettlementEnabled) { + validateEffectiveTransferForDelayedSettlementSale(effectiveTransfers); + } else { + validateEffectiveTransferForSale(effectiveTransfers); + } + } + + private void validateIntermediarySale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { validateSettlementDate(externalAssetOwnerTransfer); - validateEffectiveTransferForSale(externalAssetOwnerTransfer); + validateEffectiveTransferForIntermediarySale(externalAssetOwnerTransfer); } private void validateSettlementDate(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { @@ -256,9 +380,25 @@ private void validateSettlementDate(LocalDate settlementDate) { } } - 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 getValidLoanStatusList(boolean isDelayedSettlementEnabled) { + if (isDelayedSettlementEnabled) { + return VALID_DELAYED_SETTLEMENT_LOAN_STATUSES_BUYBACK_AND_SALE; + } else { + return ACTIVE_LOAN_STATUSES; } } @@ -276,14 +416,33 @@ private ExternalAssetOwnerTransfer createSaleTransfer(Long loanId, JsonElement j externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31); externalAssetOwnerTransfer.setLoanId(loanId); externalAssetOwnerTransfer.setExternalLoanId(externalLoanId); + externalAssetOwnerTransfer.setExternalReferenceId(getTransferExternalReferenceIdFromJson(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.setExternalReferenceId(getTransferExternalReferenceIdFromJson(json)); return externalAssetOwnerTransfer; } private void validateSaleRequestBody(String apiRequestBodyAsJson) { - final Set 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 requestParameters = new HashSet<>(Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE, + ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, + ExternalTransferRequestParameters.TRANSFER_EXTERNAL_REFERENCE_ID, ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, + ExternalTransferRequestParameters.DATEFORMAT, ExternalTransferRequestParameters.LOCALE)); final Type typeOfMap = new TypeToken>() { }.getType(); @@ -308,6 +467,51 @@ private void validateSaleRequestBody(String apiRequestBodyAsJson) { LocalDate settlementDate = fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, json); baseDataValidator.reset().parameter(ExternalTransferRequestParameters.SETTLEMENT_DATE).value(settlementDate).notNull(); + final String transferExternalReferenceId = fromApiJsonHelper + .extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_REFERENCE_ID, json); + baseDataValidator.reset().parameter(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_REFERENCE_ID) + .value(transferExternalReferenceId).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 requestParameters = new HashSet<>(Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE, + ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, + ExternalTransferRequestParameters.TRANSFER_EXTERNAL_REFERENCE_ID, ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, + ExternalTransferRequestParameters.DATEFORMAT, ExternalTransferRequestParameters.LOCALE)); + final Type typeOfMap = new TypeToken>() { + + }.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, apiRequestBodyAsJson, requestParameters); + + final List 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 transferExternalReferenceId = fromApiJsonHelper + .extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_REFERENCE_ID, json); + baseDataValidator.reset().parameter(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_REFERENCE_ID) + .value(transferExternalReferenceId).ignoreIfNull().notExceedingLengthOf(100); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); @@ -352,6 +556,12 @@ private ExternalId getTransferExternalIdFromJson(JsonElement json) { return StringUtils.isEmpty(transferExternalId) ? ExternalId.generate() : ExternalIdFactory.produce(transferExternalId); } + private ExternalId getTransferExternalReferenceIdFromJson(JsonElement json) { + String transferExternalReferenceId = fromApiJsonHelper + .extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_REFERENCE_ID, json); + return StringUtils.isEmpty(transferExternalReferenceId) ? null : ExternalIdFactory.produce(transferExternalReferenceId); + } + private String getPurchasePriceRatioFromJson(JsonElement json) { return fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, json); } @@ -368,10 +578,4 @@ private ExternalAssetOwner createAndGetAssetOwner(String externalId) { externalAssetOwner.setExternalId(ExternalIdFactory.produce(externalId)); return externalAssetOwnerRepository.saveAndFlush(externalAssetOwner); } - - private LoanIdAndExternalIdAndStatus fetchLoanDetails(Long loanId) { - Optional loanIdAndExternalIdAndStatusResult = loanRepository - .findLoanIdAndExternalIdAndStatusByLoanId(loanId); - return loanIdAndExternalIdAndStatusResult.orElseThrow(() -> new LoanNotFoundException(loanId)); - } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/IntermediarySaleToExternalAssetOwnerHandler.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/IntermediarySaleToExternalAssetOwnerHandler.java new file mode 100644 index 00000000000..9c41b5bd3b5 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/IntermediarySaleToExternalAssetOwnerHandler.java @@ -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); + } + +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java index 9d5517cc7da..3f08fa4accb 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java @@ -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 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 transferDataList = findAllPendingOrBuybackTransfers(loanId); + List 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 @@ private void declineTransfer(Loan loan, ExternalAssetOwnerTransfer pendingTransf } 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 @@ private ExternalAssetOwnerTransfer createCancelledTransfer(ExternalAssetOwnerTra ExternalAssetOwnerTransfer cancelledTransfer = new ExternalAssetOwnerTransfer(); cancelledTransfer.setOwner(pendingTransfer.getOwner()); cancelledTransfer.setExternalId(pendingTransfer.getExternalId()); + cancelledTransfer.setExternalReferenceId(pendingTransfer.getExternalReferenceId()); cancelledTransfer.setStatus(CANCELLED); cancelledTransfer.setSubStatus(subStatus); cancelledTransfer.setSettlementDate(pendingTransfer.getSettlementDate()); @@ -134,6 +134,7 @@ private ExternalAssetOwnerTransfer createDeclinedTransfer(ExternalAssetOwnerTran ExternalAssetOwnerTransfer declinedTransfer = new ExternalAssetOwnerTransfer(); declinedTransfer.setOwner(pendingSaleTransfer.getOwner()); declinedTransfer.setExternalId(pendingSaleTransfer.getExternalId()); + declinedTransfer.setExternalReferenceId(pendingSaleTransfer.getExternalReferenceId()); declinedTransfer.setStatus(DECLINED); declinedTransfer.setSubStatus(isBiggerThanZero(loan.getTotalOverpaid()) ? BALANCE_NEGATIVE : BALANCE_ZERO); declinedTransfer.setSettlementDate(pendingSaleTransfer.getSettlementDate()); @@ -176,20 +177,20 @@ private ExternalAssetOwnerTransferDetails createAssetOwnerTransferDetails(Loan l 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 findAllPendingOrBuybackTransfers(Long loanId) { + private List 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 @@ private boolean isBiggerThanZero(BigDecimal loanTotalOverpaid) { } private static boolean isSameDayTransfers(List transferDataList) { - return Objects.equals(transferDataList.get(0).getSettlementDate(), transferDataList.get(1).getSettlementDate()); + return (transferDataList.stream().map(ExternalAssetOwnerTransfer::getSettlementDate).distinct().count() == 1); } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanTransferabilityService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanTransferabilityService.java new file mode 100644 index 00000000000..0863436ab9a --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanTransferabilityService.java @@ -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); +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanTransferabilityServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanTransferabilityServiceImpl.java new file mode 100644 index 00000000000..0f73c404560 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanTransferabilityServiceImpl.java @@ -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; + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java index a056a30ea57..8aa4585c061 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializer.java @@ -20,14 +20,18 @@ 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 @@ @RequiredArgsConstructor public class InvestorBusinessEventSerializer implements BusinessEventSerializer { + private static final Set 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 ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { LoanOwnershipTransferDataV1.Builder builder = LoanOwnershipTransferDataV1.newBuilder().setLoanId(transferData.getLoan().getLoanId()) .setLoanExternalId(transferData.getLoan().getExternalId()).setTransferExternalId(transferData.getTransferExternalId()) .setAssetOwnerExternalId(transferData.getOwner().getExternalId()) + .setTransferExternalReferenceId(transferData.getTransferExternalReferenceId()) .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 ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { @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 getUnpaidChargeData(InvestorBusinessEvent event) { @@ -126,7 +142,7 @@ private void addToMap(Map map, LoanCharge loanCharge) } 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(); diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml index e2d1717dcdd..6023efbdf0e 100644 --- a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml @@ -36,4 +36,6 @@ + + diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0015_add_intermediary_sale_command.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0015_add_intermediary_sale_command.xml new file mode 100644 index 00000000000..acf74a75e4d --- /dev/null +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0015_add_intermediary_sale_command.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0016_add_external_reference_id.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0016_add_external_reference_id.xml new file mode 100644 index 00000000000..65dad18f9d8 --- /dev/null +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0016_add_external_reference_id.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java index 81500a08f49..ed5fa3934c4 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java @@ -24,18 +24,20 @@ 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.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 @@ 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 void setUp() { 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 void givenLoanNoTransfer() { 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 void givenLoanTwoTransferButInvalidTransfers() { // 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 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 void givenLoanTwoTransferSameDay() { assertEquals(processedLoan, loanForProcessing); + verifyNoInteractions(loanTransferabilityService, accountingService); + ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(2); verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, secondSaveResult); verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, fourthSaveResult); } - @Test - public void givenLoanBuyback() { + private static Stream 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 void givenLoanBuyback() { 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 response = List.of(firstResponseItem); when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) .thenReturn(response); @@ -218,6 +283,8 @@ public void givenLoanBuyback() { // 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,210 +293,262 @@ public void givenLoanBuyback() { assertEquals(processedLoan, loanForProcessing); + verify(accountingService).createJournalEntriesForBuybackAssetTransfer(loanForProcessing, firstResponseItem); + verifyNoMoreInteractions(accountingService); + ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(2); verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, firstResponseItem); verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing); } - @Test - public void givenLoanSale() { + private static Stream 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 response = List.of(firstResponseItem); - when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) - .thenReturn(response); - ArgumentCaptor externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor - .forClass(ExternalAssetOwnerTransfer.class); - ArgumentCaptor 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 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(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer); + verifyNoMoreInteractions(loanTransferabilityService); + verify(externalAssetOwnerTransferRepository).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))); + + ArgumentCaptor 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(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(), - externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner()); - assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(), - externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId()); - assertEquals(ExternalTransferStatus.ACTIVE, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus()); - assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate()); - assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(), - externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId()); - assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(), - externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio()); - assertEquals(actualDate.plusDays(1), externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom()); - assertEquals(FUTURE_DATE_9999_12_31, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo()); - verify(externalAssetOwnerTransferLoanMappingRepository, times(1)) - .save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture()); + assertEquals(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 externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransferLoanMapping.class); + verify(externalAssetOwnerTransferLoanMappingRepository).save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture()); assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId()); - assertEquals(newTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer()); + assertEquals(savedNewTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer()); assertEquals(processedLoan, loanForProcessing); + verify(externalAssetOwnerTransferLoanMappingRepository).save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture()); + + verify(accountingService).createJournalEntriesForSaleAssetTransfer(loanForProcessing, savedNewTransfer, null); + verifyNoMoreInteractions(accountingService); + ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(2); - verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer); + verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, savedNewTransfer); verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing); } - @Test - public void givenLoanSaleButBalanceIsZero() { + private static Stream 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 firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); - when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING); - List response = List.of(firstResponseItem); + + ExternalAssetOwnerTransfer pendingTransfer = new ExternalAssetOwnerTransfer(); + pendingTransfer.setStatus(pendingStatus); + List response = List.of(pendingTransfer); when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) .thenReturn(response); - ArgumentCaptor 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(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"))); - verify(firstResponseItem).setEffectiveDateTo(actualDate); + + ArgumentCaptor 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(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(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> businessEventArgumentCaptor = verifyBusinessEvents(1); - verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer); + verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, savedNewTransfer); } @Test - public void givenLoanSaleButBalanceIsNegative() { + 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); - ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); - when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING); - List response = List.of(firstResponseItem); - when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) - .thenReturn(response); - ArgumentCaptor externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor - .forClass(ExternalAssetOwnerTransfer.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(true); + 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); + + 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 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(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"))); + verify(externalAssetOwnerTransferRepository).findOne(any(Specification.class)); - 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 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); - ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(1); - verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer); - } + assertEquals(actualDate, capturedActiveIntermediateTransfer.getEffectiveDateTo()); + assertEquals(actualDate, capturedPendingTransfer.getEffectiveDateTo()); - @Test - public void testGetEnumStyledNameSuccessScenario() { - final String actualEnumName = underTest.getEnumStyledName(); - assertNotNull(actualEnumName); - assertEquals("EXTERNAL_ASSET_OWNER_TRANSFER", actualEnumName); - } + 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()); - @Test - public void testGetHumanReadableNameSuccessScenario() { - final String actualEnumName = underTest.getHumanReadableName(); - assertNotNull(actualEnumName); - assertEquals("Execute external asset owner transfer", actualEnumName); + ArgumentCaptor externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransferLoanMapping.class); + verify(externalAssetOwnerTransferLoanMappingRepository, times(1)) + .save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture()); + assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId()); + assertEquals(savedNewTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer()); + assertEquals(processedLoan, loanForProcessing); + + verify(accountingService).createJournalEntriesForSaleAssetTransfer(loanForProcessing, savedNewTransfer, previousOwner); + verifyNoMoreInteractions(accountingService); + + ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(2); + verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, savedNewTransfer); + verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing); } @Test - public void givenLoanSaleAnsBuyBackButBalanceIsNegative() { + 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); - 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)); + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true); - 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(externalAssetOwnerTransferRepository.findOne(any(Specification.class))).thenReturn(Optional.empty()); - when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING); - when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(firstSaveResult) - .thenReturn(secondSaveResult).thenReturn(thirdSaveResult).thenReturn(fourthSaveResult); - List response = List.of(firstResponseItem); + ExternalAssetOwnerTransfer pendingTransfer = new ExternalAssetOwnerTransfer(); + pendingTransfer.setStatus(ExternalTransferStatus.PENDING); + List response = List.of(pendingTransfer); when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")))) .thenReturn(response); - ArgumentCaptor externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor - .forClass(ExternalAssetOwnerTransfer.class); + when(loanTransferabilityService.isTransferable(loanForProcessing, pendingTransfer)).thenReturn(true); + // 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()); + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing)); - 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()); + // then + assertEquals("Expected a effective transfer of ACTIVE_INTERMEDIATE status to be present.", exception.getMessage()); + + 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); + } - assertEquals(processedLoan, loanForProcessing); + @Test + public void testGetEnumStyledNameSuccessScenario() { + final String actualEnumName = underTest.getEnumStyledName(); + assertNotNull(actualEnumName); + assertEquals("EXTERNAL_ASSET_OWNER_TRANSFER", actualEnumName); + } - ArgumentCaptor> businessEventArgumentCaptor = verifyBusinessEvents(2); - verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, secondSaveResult); + @Test + public void testGetHumanReadableNameSuccessScenario() { + final String actualEnumName = underTest.getHumanReadableName(); + assertNotNull(actualEnumName); + assertEquals("Execute external asset owner transfer", actualEnumName); } @NotNull @@ -453,4 +572,12 @@ private void verifyLoanAccountSnapshotBusinessEvent(ArgumentCaptor(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 ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerJournalEntryMapping.class); + verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(6)) + .saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture()); + List capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor + .getAllValues(); + + List expectedOwnerJournalEntryMappings = List.of( + ownerJournalEntryMapping(principleAndInterestCreditJournalEntry, previousOwner), + ownerJournalEntryMapping(feeAndPenaltyCreditJournalEntry, previousOwner), + ownerJournalEntryMapping(transferDebitJournalEntry, previousOwner), + ownerJournalEntryMapping(principleAndInterestDebitJournalEntry, newOwner), + ownerJournalEntryMapping(feeAndPenaltyDebitJournalEntry, newOwner), + ownerJournalEntryMapping(transferCreditJournalEntry, previousOwner)); + assertNotNull(capturedOwnerJournalEntryMappings); + assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings); + + ArgumentCaptor transferJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransferJournalEntryMapping.class); + verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(6)) + .saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture()); + List capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor + .getAllValues(); + + List 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 ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerJournalEntryMapping.class); + verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(4)) + .saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture()); + List capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor + .getAllValues(); + + List expectedOwnerJournalEntryMappings = List.of( + ownerJournalEntryMapping(overpaidCreditJournalEntry, newOwner), + ownerJournalEntryMapping(transferDebitJournalEntry, previousOwner), + ownerJournalEntryMapping(overpaidDebitJournalEntry, previousOwner), + ownerJournalEntryMapping(transferCreditJournalEntry, previousOwner)); + assertNotNull(capturedOwnerJournalEntryMappings); + assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings); + + ArgumentCaptor transferJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransferJournalEntryMapping.class); + verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(4)) + .saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture()); + List capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor + .getAllValues(); + + List 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 ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerJournalEntryMapping.class); + verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(2)) + .saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture()); + List capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor + .getAllValues(); + + List expectedOwnerJournalEntryMappings = List.of( + ownerJournalEntryMapping(principleAndInterestDebitJournalEntry, newOwner), + ownerJournalEntryMapping(feeAndPenaltyDebitJournalEntry, newOwner)); + assertNotNull(capturedOwnerJournalEntryMappings); + assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings); + + ArgumentCaptor transferJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransferJournalEntryMapping.class); + verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(6)) + .saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture()); + List capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor + .getAllValues(); + + List 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 ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerJournalEntryMapping.class); + verify(testContext.externalAssetOwnerJournalEntryMappingRepository).saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture()); + List capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor + .getAllValues(); + + List expectedOwnerJournalEntryMappings = List + .of(ownerJournalEntryMapping(overpaidCreditJournalEntry, newOwner)); + assertNotNull(capturedOwnerJournalEntryMappings); + assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings); + + ArgumentCaptor transferJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransferJournalEntryMapping.class); + verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(4)) + .saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture()); + List capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor + .getAllValues(); + + List 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 ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerJournalEntryMapping.class); + verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(4)) + .saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture()); + List capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor + .getAllValues(); + + List expectedOwnerJournalEntryMappings = List.of( + ownerJournalEntryMapping(transferCreditJournalEntry, previousOwner), + ownerJournalEntryMapping(principleAndInterestCreditJournalEntry, previousOwner), + ownerJournalEntryMapping(feeAndPenaltyCreditJournalEntry, previousOwner), + ownerJournalEntryMapping(transferDebitJournalEntry, previousOwner)); + assertNotNull(capturedOwnerJournalEntryMappings); + assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings); + + ArgumentCaptor transferJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransferJournalEntryMapping.class); + verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(6)) + .saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture()); + List capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor + .getAllValues(); + + List 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 ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerJournalEntryMapping.class); + verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(3)) + .saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture()); + List capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor + .getAllValues(); + + List expectedOwnerJournalEntryMappings = List.of( + ownerJournalEntryMapping(overpaidDebitJournalEntry, previousOwner), + ownerJournalEntryMapping(transferCreditJournalEntry, previousOwner), + ownerJournalEntryMapping(transferDebitJournalEntry, previousOwner)); + assertNotNull(capturedOwnerJournalEntryMappings); + assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings); + + ArgumentCaptor transferJournalEntryMappingArgumentCaptor = ArgumentCaptor + .forClass(ExternalAssetOwnerTransferJournalEntryMapping.class); + verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(4)) + .saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture()); + List capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor + .getAllValues(); + + List 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 expectedOwnerJournalEntryMappings, + List 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 expectedTransferJournalEntryMappings, + List 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); + } + } +} diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/DelayedSettlementAttributeServiceImplTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/DelayedSettlementAttributeServiceImplTest.java new file mode 100644 index 00000000000..5f9108c8e67 --- /dev/null +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/DelayedSettlementAttributeServiceImplTest.java @@ -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 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 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); + } + } +} diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceTest.java new file mode 100644 index 00000000000..99811d278a3 --- /dev/null +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceTest.java @@ -0,0 +1,929 @@ +/** + * 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 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 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 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 assetOwnerTransfers = new ArrayList<>(); + assetOwnerTransfers.add(effectiveTransferForDelayedSettlement); + final TestContext testContext = new TestContext(); + final ArgumentCaptor 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 externalAssetOwnerTransferList, final boolean isDelayedSettlementEnabled) { + final TestContext testContext = new TestContext(); + final ArgumentCaptor 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 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 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 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 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 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 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 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 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 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().plusDays(1)), + // purchaseRatio length cannot be > 50 + Arguments.of("value", "value", RandomStringUtils.randomAlphanumeric(51), LocalDate.now().plusDays(1)), + // transferExternalId length cannot be > 100 + Arguments.of("value", RandomStringUtils.randomAlphanumeric(101), "value", LocalDate.now().plusDays(1)), + // ownerExternalId cannot be null + Arguments.of(null, "value", "value", LocalDate.now().plusDays(1)), + // ownerExternalId length cannot be > 100 + Arguments.of(RandomStringUtils.randomAlphanumeric(101), "value", "value", LocalDate.now().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 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 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 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 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 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 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 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 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 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.setExternalReferenceId(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.transferExternalReferenceId, externalAssetOwnerTransfer.getExternalReferenceId().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 transferExternalReferenceId = RandomStringUtils.randomAlphanumeric(10); + private final LocalDate settlementDate = LocalDate.parse("9999-08-22"); + private final String jsonCommand = String.format(""" + { + "settlementDate": "%s", + "ownerExternalId": "%s", + "transferExternalId": "%s", + "transferExternalReferenceId": "%s", + "purchasePriceRatio": "%s", + "dateFormat": "%s", + "locale": "%s" + } + """, settlementDate, ownerExternalId, transferExternalId, transferExternalReferenceId, 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_REFERENCE_ID, + jsonCommandElement)).thenReturn(transferExternalReferenceId); + 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); + } + } +} diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java index 74b1991a239..430ff33a9b8 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java @@ -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.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.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 void setUp() { } @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 response = List.of(pendingSaleTransfer, pendingBuybackTransfer); + List 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> businessEventArgumentCaptor = verifyBusinessEvents(2); + ArgumentCaptor> 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.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 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> 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(PENDING); + when(pendingSaleTransfer.getStatus()).thenReturn(pendingStatus); ExternalAssetOwnerTransfer declineTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class); List response = List.of(pendingSaleTransfer); @@ -127,8 +180,9 @@ public void verifyWhenDeclinePendingSaleTransferThenBusinessEventIsSent() { 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 void verifyWhenExecutePendingBuybackTransferThenBusinessEventIsSent() { 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 response = List.of(pendingBuybackTransfer); @@ -154,6 +208,14 @@ public void verifyWhenExecutePendingBuybackTransferThenBusinessEventIsSent() { verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing); } + private static Stream pendingStatusDataProvider() { + return Stream.of(Arguments.of(ExternalTransferStatus.PENDING_INTERMEDIATE), Arguments.of(ExternalTransferStatus.PENDING)); + } + + private static Stream buybackStatusDataProvider() { + return Stream.of(Arguments.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE), Arguments.of(ExternalTransferStatus.BUYBACK)); + } + @NotNull private ArgumentCaptor> verifyBusinessEvents(int expectedBusinessEvents) { @SuppressWarnings("unchecked") diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanTransferabilityServiceImplTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanTransferabilityServiceImplTest.java new file mode 100644 index 00000000000..c34ca123bbf --- /dev/null +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanTransferabilityServiceImplTest.java @@ -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 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 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); + } + } +} diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java index 423e33ae10e..19e5bdfee40 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java @@ -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"); + } + + @Test + public void testSerializationCancelledPendingIntermediateSameDayTransfer() { + doTest(CANCELLED, SAMEDAY_TRANSFERS, PENDING_INTERMEDIATE, "INTERMEDIARYSALE", "CANCELLED", "SAMEDAY_TRANSFERS"); } - private void doTest(ExternalTransferStatus status, ExternalTransferSubStatus subStatus, String expectedType, String expectedStatus, - String expectedReason) { + 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 loanCharges = createMockCharges(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndStatus.java b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanDataForExternalTransfer.java similarity index 91% rename from fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndStatus.java rename to fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanDataForExternalTransfer.java index 92c27293972..e443d406557 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndStatus.java +++ b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanDataForExternalTransfer.java @@ -24,9 +24,11 @@ @AllArgsConstructor @Getter -public class LoanIdAndExternalIdAndStatus { +public class LoanDataForExternalTransfer { Long id; ExternalId externalId; Integer loanStatus; + Long loanProductId; + String loanProductShortName; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index b022596b88d..128ffe153b0 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -22,8 +22,8 @@ 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, 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 @@ List findByGroupOfficeIdsAndLoanStatus(@Param("officeIds") Collection findLoanIdAndExternalIdAndStatusByLoanId(@Param("loanId") Long loanId); + @Query(FIND_LOAN_DATA_FOR_EXTERNAL_TRANSFER) + Optional findLoanDataForExternalTransferByLoanId(@Param("loanId") Long loanId); @Query(EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID) boolean existsNonClosedLoanByExternalLoanId(@Param("externalLoanId") ExternalId externalLoanId); diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 8dba3f1e112..439d1d6fc46 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -186,4 +186,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0168_transaction_summary_with_asset_owner_report_add_active_intermediate_filtering.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0168_transaction_summary_with_asset_owner_report_add_active_intermediate_filtering.xml new file mode 100644 index 00000000000..e5387c2aca4 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0168_transaction_summary_with_asset_owner_report_add_active_intermediate_filtering.xml @@ -0,0 +1,1120 @@ + + + + + + + report_name='Transaction Summary Report with Asset Owner' + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferTest.java index ba1a9bea47a..767b322368b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferTest.java @@ -123,15 +123,17 @@ protected void updateBusinessDateAndExecuteCOBJob(String date) { protected PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate) { String transferExternalId = UUID.randomUUID().toString(); + String transferExternalReferenceId = UUID.randomUUID().toString(); ownerExternalId = UUID.randomUUID().toString(); - return createSaleTransfer(loanID, settlementDate, transferExternalId, ownerExternalId, "1.0"); + return createSaleTransfer(loanID, settlementDate, transferExternalId, transferExternalReferenceId, ownerExternalId, "1.0"); } protected PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate, String transferExternalId, - String ownerExternalId, String purchasePriceRatio) { + String transferExternalReferenceId, 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).transferExternalReferenceId(transferExternalReferenceId) + .ownerExternalId(ownerExternalId).purchasePriceRatio(purchasePriceRatio)); assertEquals(transferExternalId, saleResponse.getResourceExternalId()); return saleResponse; } @@ -263,6 +265,7 @@ protected void validateExternalAssetOwnerTransfer(PageExternalTransferData respo assertTrue(first.isPresent()); ExternalTransferData etd = first.get(); assertEquals(expected.transferExternalId, etd.getTransferExternalId()); + assertEquals(expected.transferExternalReferenceId, etd.getTransferExternalReferenceId()); assertEquals(expected.status, etd.getStatus()); assertEquals(LocalDate.parse(expected.settlementDate), etd.getSettlementDate()); assertEquals(LocalDate.parse(expected.effectiveFrom), etd.getEffectiveFrom()); @@ -306,6 +309,7 @@ protected void validateResponse(PostInitiateTransferResponse transferResponse, I assertNotNull(transferResponse); assertNotNull(transferResponse.getResourceId()); assertNotNull(transferResponse.getResourceExternalId()); + assertNotNull(transferResponse.getResourceExternalReferenceId()); assertNotNull(transferResponse.getSubResourceId()); assertEquals((long) loanID, transferResponse.getSubResourceId()); assertNotNull(transferResponse.getSubResourceExternalId()); @@ -356,6 +360,7 @@ public static class ExpectedExternalTransferData { private final ExternalTransferData.StatusEnum status; private final String transferExternalId; + private final String transferExternalReferenceId; private final String settlementDate; @@ -371,24 +376,26 @@ public static class ExpectedExternalTransferData { private final BigDecimal totalOverpaid; static ExpectedExternalTransferData expected(ExternalTransferData.StatusEnum status, String transferExternalId, - String settlementDate, String effectiveFrom, String effectiveTo, boolean detailsExpected, BigDecimal totalOutstanding, - BigDecimal totalPrincipalOutstanding, BigDecimal totalInterestOutstanding, BigDecimal totalPenaltyOutstanding, - BigDecimal totalFeeOutstanding, BigDecimal totalOverpaid) { - return new ExpectedExternalTransferData(status, transferExternalId, settlementDate, effectiveFrom, effectiveTo, null, - detailsExpected, totalOutstanding, totalPrincipalOutstanding, totalInterestOutstanding, totalPenaltyOutstanding, - totalFeeOutstanding, totalOverpaid); + String transferExternalReferenceId, String settlementDate, String effectiveFrom, String effectiveTo, + boolean detailsExpected, BigDecimal totalOutstanding, BigDecimal totalPrincipalOutstanding, + BigDecimal totalInterestOutstanding, BigDecimal totalPenaltyOutstanding, BigDecimal totalFeeOutstanding, + BigDecimal totalOverpaid) { + return new ExpectedExternalTransferData(status, transferExternalId, transferExternalReferenceId, settlementDate, effectiveFrom, + effectiveTo, null, detailsExpected, totalOutstanding, totalPrincipalOutstanding, totalInterestOutstanding, + totalPenaltyOutstanding, totalFeeOutstanding, totalOverpaid); } static ExpectedExternalTransferData expected(ExternalTransferData.StatusEnum status, String transferExternalId, - String settlementDate, String effectiveFrom, String effectiveTo) { - return new ExpectedExternalTransferData(status, transferExternalId, settlementDate, effectiveFrom, effectiveTo, null, false, - null, null, null, null, null, null); + String transferExternalReferenceId, String settlementDate, String effectiveFrom, String effectiveTo) { + return new ExpectedExternalTransferData(status, transferExternalId, transferExternalReferenceId, settlementDate, effectiveFrom, + effectiveTo, null, false, null, null, null, null, null, null); } static ExpectedExternalTransferData expected(ExternalTransferData.StatusEnum status, String transferExternalId, - String settlementDate, String effectiveFrom, String effectiveTo, ExternalTransferData.SubStatusEnum subStatus) { - return new ExpectedExternalTransferData(status, transferExternalId, settlementDate, effectiveFrom, effectiveTo, subStatus, - false, null, null, null, null, null, null); + String transferExternalReferenceId, String settlementDate, String effectiveFrom, String effectiveTo, + ExternalTransferData.SubStatusEnum subStatus) { + return new ExpectedExternalTransferData(status, transferExternalId, transferExternalReferenceId, settlementDate, effectiveFrom, + effectiveTo, subStatus, false, null, null, null, null, null, null); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java index 82d84e15d4d..ffe4f88fa01 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java @@ -88,6 +88,7 @@ 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; @@ -168,12 +169,12 @@ public void saleActiveLoanToExternalAssetOwnerWithCancelAndBuybackADayLater() { addPenaltyForLoan(loanID, "10"); PostInitiateTransferResponse saleTransferResponse = createSaleTransfer(loanID, "2020-03-02"); - validateResponse(saleTransferResponse, loanID); + validateResponse(saleTransferResponse, loanID, false); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "9999-12-31", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); getAndValidateThereIsNoActiveMapping(saleTransferResponse.getResourceExternalId()); PageExternalTransferData retrieveResponse = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue()); retrieveResponse.getContent().forEach(transfer -> getAndValidateThereIsNoJournalEntriesForTransfer(transfer.getTransferId())); @@ -181,50 +182,50 @@ public void saleActiveLoanToExternalAssetOwnerWithCancelAndBuybackADayLater() { EXTERNAL_ASSET_OWNER_HELPER.cancelTransferByTransferExternalId(saleTransferResponse.getResourceExternalId()); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), "2020-03-02", - "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); PostInitiateTransferResponse oldSaleTransferResponse = saleTransferResponse; saleTransferResponse = createSaleTransfer(loanID, "2020-03-02"); - validateResponse(saleTransferResponse, loanID); + validateResponse(saleTransferResponse, loanID, false); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, oldSaleTransferResponse.getResourceExternalId(), "2020-03-02", - "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(CANCELLED, oldSaleTransferResponse.getResourceExternalId(), "2020-03-02", - "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, oldSaleTransferResponse.getResourceExternalId(), + oldSaleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(CANCELLED, oldSaleTransferResponse.getResourceExternalId(), + oldSaleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "9999-12-31", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); updateBusinessDateAndExecuteCOBJob("2020-03-03"); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, oldSaleTransferResponse.getResourceExternalId(), "2020-03-02", - "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(CANCELLED, oldSaleTransferResponse.getResourceExternalId(), "2020-03-02", - "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "9999-12-31", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, oldSaleTransferResponse.getResourceExternalId(), + oldSaleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(CANCELLED, oldSaleTransferResponse.getResourceExternalId(), + oldSaleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "9999-12-31", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); List allExternalEvents = ExternalEventHelper.getAllExternalEvents(REQUEST_SPEC, RESPONSE_SPEC); Assertions.assertEquals(1, allExternalEvents.size()); @@ -253,25 +254,25 @@ public void saleActiveLoanToExternalAssetOwnerWithCancelAndBuybackADayLater() { BigDecimal.valueOf(15767.420000), expectedDate, expectedDate)); PostInitiateTransferResponse buybackTransferResponse = createBuybackTransfer(loanID, "2020-03-03"); - validateResponse(buybackTransferResponse, loanID); + validateResponse(buybackTransferResponse, loanID, true); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, oldSaleTransferResponse.getResourceExternalId(), "2020-03-02", - "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(CANCELLED, oldSaleTransferResponse.getResourceExternalId(), "2020-03-02", - "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "9999-12-31", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-03", + ExpectedExternalTransferData.expected(PENDING, oldSaleTransferResponse.getResourceExternalId(), + oldSaleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(CANCELLED, oldSaleTransferResponse.getResourceExternalId(), + oldSaleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "9999-12-31", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-03", "2020-03-03", "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); @@ -294,23 +295,23 @@ public void saleActiveLoanToExternalAssetOwnerWithCancelAndBuybackADayLater() { updateBusinessDateAndExecuteCOBJob("2020-03-04"); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, oldSaleTransferResponse.getResourceExternalId(), "2020-03-02", - "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(CANCELLED, oldSaleTransferResponse.getResourceExternalId(), "2020-03-02", - "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "2020-03-03", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-03", + ExpectedExternalTransferData.expected(PENDING, oldSaleTransferResponse.getResourceExternalId(), + oldSaleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(CANCELLED, oldSaleTransferResponse.getResourceExternalId(), + oldSaleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "2020-03-03", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-03", "2020-03-03", "2020-03-03", true, new BigDecimal("15762.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("5.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); @@ -344,10 +345,14 @@ public void saleActiveLoanToExternalAssetOwnerWithCancelAndBuybackADayLater() { BigDecimal.valueOf(9.680000), expectedDate, expectedDate), ExpectedJournalEntryData.expected((long) INCOME_ACCOUNT.getAccountID(), (long) JournalEntryType.CREDIT.getValue(), BigDecimal.valueOf(9.680000), expectedDate, expectedDate), + ExpectedJournalEntryData.expected((long) TRANSFER_ACCOUNT.getAccountID(), (long) JournalEntryType.CREDIT.getValue(), + BigDecimal.valueOf(15762.420000), expectedDate, expectedDate), ExpectedJournalEntryData.expected((long) ASSET_ACCOUNT.getAccountID(), (long) JournalEntryType.CREDIT.getValue(), BigDecimal.valueOf(15757.420000), expectedDate, expectedDate), ExpectedJournalEntryData.expected((long) FEE_PENALTY_ACCOUNT.getAccountID(), (long) JournalEntryType.CREDIT.getValue(), - BigDecimal.valueOf(5.000000), expectedDate, expectedDate)); + BigDecimal.valueOf(5.000000), expectedDate, expectedDate), + ExpectedJournalEntryData.expected((long) TRANSFER_ACCOUNT.getAccountID(), (long) JournalEntryType.DEBIT.getValue(), + BigDecimal.valueOf(15762.420000), expectedDate, expectedDate)); } finally { cleanUpAndRestoreBusinessDate(); } @@ -363,26 +368,26 @@ public void saleActiveLoanToExternalAssetOwnerAndBuybackADayLater() { addPenaltyForLoan(loanID, "10"); PostInitiateTransferResponse saleTransferResponse = createSaleTransfer(loanID, "2020-03-02"); - validateResponse(saleTransferResponse, loanID); + validateResponse(saleTransferResponse, loanID, false); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "9999-12-31", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); getAndValidateThereIsNoActiveMapping(saleTransferResponse.getResourceExternalId()); PageExternalTransferData retrieveResponse = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue()); retrieveResponse.getContent().forEach(transfer -> getAndValidateThereIsNoJournalEntriesForTransfer(transfer.getTransferId())); updateBusinessDateAndExecuteCOBJob("2020-03-03"); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "9999-12-31", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "9999-12-31", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); getAndValidateThereIsActiveMapping(loanID); retrieveResponse = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue()); LocalDate expectedDate = LocalDate.of(2020, 3, 2); @@ -401,17 +406,17 @@ public void saleActiveLoanToExternalAssetOwnerAndBuybackADayLater() { BigDecimal.valueOf(15767.420000), expectedDate, expectedDate)); PostInitiateTransferResponse buybackTransferResponse = createBuybackTransfer(loanID, "2020-03-03"); - validateResponse(buybackTransferResponse, loanID); + validateResponse(buybackTransferResponse, loanID, true); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "9999-12-31", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-03", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "9999-12-31", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-03", "2020-03-03", "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); @@ -434,15 +439,15 @@ public void saleActiveLoanToExternalAssetOwnerAndBuybackADayLater() { updateBusinessDateAndExecuteCOBJob("2020-03-04"); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "2020-03-03", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-03", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "2020-03-03", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-03", "2020-03-03", "2020-03-03", true, new BigDecimal("15762.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("5.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); @@ -476,10 +481,14 @@ public void saleActiveLoanToExternalAssetOwnerAndBuybackADayLater() { BigDecimal.valueOf(9.680000), expectedDate, expectedDate), ExpectedJournalEntryData.expected((long) INCOME_ACCOUNT.getAccountID(), (long) JournalEntryType.CREDIT.getValue(), BigDecimal.valueOf(9.680000), expectedDate, expectedDate), + ExpectedJournalEntryData.expected((long) TRANSFER_ACCOUNT.getAccountID(), (long) JournalEntryType.CREDIT.getValue(), + BigDecimal.valueOf(15762.420000), expectedDate, expectedDate), ExpectedJournalEntryData.expected((long) ASSET_ACCOUNT.getAccountID(), (long) JournalEntryType.CREDIT.getValue(), BigDecimal.valueOf(15757.420000), expectedDate, expectedDate), ExpectedJournalEntryData.expected((long) FEE_PENALTY_ACCOUNT.getAccountID(), (long) JournalEntryType.CREDIT.getValue(), - BigDecimal.valueOf(5.000000), expectedDate, expectedDate)); + BigDecimal.valueOf(5.000000), expectedDate, expectedDate), + ExpectedJournalEntryData.expected((long) TRANSFER_ACCOUNT.getAccountID(), (long) JournalEntryType.DEBIT.getValue(), + BigDecimal.valueOf(15762.420000), expectedDate, expectedDate)); } finally { cleanUpAndRestoreBusinessDate(); } @@ -495,26 +504,26 @@ public void saleOverpaidLoanToExternalAssetOwnerAndBuybackADayLater() { addPenaltyForLoan(loanID, "10"); PostInitiateTransferResponse saleTransferResponse = createSaleTransfer(loanID, "2020-03-02"); - validateResponse(saleTransferResponse, loanID); + validateResponse(saleTransferResponse, loanID, false); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "9999-12-31", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); getAndValidateThereIsNoActiveMapping(saleTransferResponse.getResourceExternalId()); PageExternalTransferData retrieveResponse = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue()); retrieveResponse.getContent().forEach(transfer -> getAndValidateThereIsNoJournalEntriesForTransfer(transfer.getTransferId())); updateBusinessDateAndExecuteCOBJob("2020-03-03"); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "9999-12-31", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "9999-12-31", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); getAndValidateThereIsActiveMapping(loanID); retrieveResponse = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue()); LocalDate expectedDate = LocalDate.of(2020, 3, 2); @@ -533,17 +542,17 @@ public void saleOverpaidLoanToExternalAssetOwnerAndBuybackADayLater() { BigDecimal.valueOf(15767.420000), expectedDate, expectedDate)); PostInitiateTransferResponse buybackTransferResponse = createBuybackTransfer(loanID, "2020-03-03"); - validateResponse(buybackTransferResponse, loanID); + validateResponse(buybackTransferResponse, loanID, true); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "9999-12-31", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-03", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "9999-12-31", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-03", "2020-03-03", "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); @@ -560,19 +569,23 @@ public void saleOverpaidLoanToExternalAssetOwnerAndBuybackADayLater() { ExpectedJournalEntryData.expected((long) FEE_PENALTY_ACCOUNT.getAccountID(), (long) JournalEntryType.DEBIT.getValue(), BigDecimal.valueOf(10.000000), expectedDate, expectedDate), ExpectedJournalEntryData.expected((long) OVERPAYMENT_ACCOUNT.getAccountID(), (long) JournalEntryType.DEBIT.getValue(), + BigDecimal.valueOf(10.000000), repaymentSubmittedOnDate, repaymentSubmittedOnDate), + ExpectedJournalEntryData.expected((long) TRANSFER_ACCOUNT.getAccountID(), (long) JournalEntryType.CREDIT.getValue(), + BigDecimal.valueOf(10.000000), repaymentSubmittedOnDate, repaymentSubmittedOnDate), + ExpectedJournalEntryData.expected((long) TRANSFER_ACCOUNT.getAccountID(), (long) JournalEntryType.DEBIT.getValue(), BigDecimal.valueOf(10.000000), repaymentSubmittedOnDate, repaymentSubmittedOnDate)); updateBusinessDateAndExecuteCOBJob("2020-03-04"); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "2020-03-03", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-03", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "2020-03-03", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-03", "2020-03-03", "2020-03-03", true, new BigDecimal("0.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"), new BigDecimal("10.000000"))); @@ -595,7 +608,11 @@ public void saleOverpaidLoanToExternalAssetOwnerAndBuybackADayLater() { ExpectedJournalEntryData.expected((long) FEE_PENALTY_ACCOUNT.getAccountID(), (long) JournalEntryType.DEBIT.getValue(), BigDecimal.valueOf(10.000000), previousDayDate, previousDayDate), ExpectedJournalEntryData.expected((long) OVERPAYMENT_ACCOUNT.getAccountID(), (long) JournalEntryType.DEBIT.getValue(), - BigDecimal.valueOf(10.000000), expectedDate, expectedDate)); + BigDecimal.valueOf(10.000000), expectedDate, expectedDate), + ExpectedJournalEntryData.expected((long) TRANSFER_ACCOUNT.getAccountID(), (long) JournalEntryType.CREDIT.getValue(), + BigDecimal.valueOf(10.000000), repaymentSubmittedOnDate, repaymentSubmittedOnDate), + ExpectedJournalEntryData.expected((long) TRANSFER_ACCOUNT.getAccountID(), (long) JournalEntryType.DEBIT.getValue(), + BigDecimal.valueOf(10.000000), repaymentSubmittedOnDate, repaymentSubmittedOnDate)); } finally { cleanUpAndRestoreBusinessDate(); } @@ -631,9 +648,12 @@ public void saleIsNotAllowedWhenLoanIsNotActive() { 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(); } @@ -653,10 +673,10 @@ public void saleIsDeclinedWhenLoanIsCancelled() { LOAN_TRANSACTION_HELPER.writeOffLoan("04 March 2020", loanID); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-06", "2020-03-02", - "2020-03-04"), - ExpectedExternalTransferData.expected(DECLINED, saleTransferResponse.getResourceExternalId(), "2020-03-06", - "2020-03-04", "2020-03-04", BALANCE_ZERO)); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-06", "2020-03-02", "2020-03-04"), + ExpectedExternalTransferData.expected(DECLINED, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-06", "2020-03-04", "2020-03-04", BALANCE_ZERO)); } finally { cleanUpAndRestoreBusinessDate(); } @@ -677,13 +697,13 @@ public void buybackIsExecutedWhenLoanIsCancelled() { LOAN_TRANSACTION_HELPER.writeOffLoan("04 March 2020", loanID); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-04", "2020-03-02", - "2020-03-04"), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-04", "2020-03-05", - "2020-03-05", true, new BigDecimal("15757.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-06", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-04", "2020-03-02", "2020-03-04"), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-04", "2020-03-05", "2020-03-05", true, + new BigDecimal("15757.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("0.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-06", "2020-03-05", "2020-03-05", true, new BigDecimal("15757.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); @@ -707,14 +727,14 @@ public void buybackAndSaleIsCancelledWhenLoanIsCancelled() { LOAN_TRANSACTION_HELPER.writeOffLoan("02 March 2020", loanID); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-04", "2020-03-02", - "2020-03-02"), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-06", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-04", "2020-03-02", "2020-03-02"), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-06", "2020-03-02", "2020-03-02"), - ExpectedExternalTransferData.expected(CANCELLED, buybackTransferResponse.getResourceExternalId(), "2020-03-06", + ExpectedExternalTransferData.expected(CANCELLED, buybackTransferResponse.getResourceExternalId(), null, "2020-03-06", "2020-03-02", "2020-03-02", UNSOLD), - ExpectedExternalTransferData.expected(DECLINED, saleTransferResponse.getResourceExternalId(), "2020-03-04", - "2020-03-02", "2020-03-02", BALANCE_ZERO)); + ExpectedExternalTransferData.expected(DECLINED, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-04", "2020-03-02", "2020-03-02", BALANCE_ZERO)); } finally { cleanUpAndRestoreBusinessDate(); } @@ -734,14 +754,15 @@ public void sameDayBuybackAndSaleIsCancelledWhenLoanIsCancelled() { LOAN_TRANSACTION_HELPER.writeOffLoan("02 March 2020", loanID); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-03", "2020-03-02", - "2020-03-02"), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-03", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-03", "2020-03-02", "2020-03-02"), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-03", "2020-03-02", "2020-03-02"), - ExpectedExternalTransferData.expected(CANCELLED, buybackTransferResponse.getResourceExternalId(), "2020-03-03", + ExpectedExternalTransferData.expected(CANCELLED, buybackTransferResponse.getResourceExternalId(), null, "2020-03-03", "2020-03-02", "2020-03-02", SAMEDAY_TRANSFERS), - ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), "2020-03-03", - "2020-03-02", "2020-03-02", SAMEDAY_TRANSFERS)); + ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-03", "2020-03-02", "2020-03-02", + SAMEDAY_TRANSFERS)); } finally { cleanUpAndRestoreBusinessDate(); } @@ -756,16 +777,16 @@ public void saleAndBuybackOnTheSameDay() { Integer loanID = createLoanForClient(clientID); PostInitiateTransferResponse saleTransferResponse = createSaleTransfer(loanID, "2020-03-02"); - validateResponse(saleTransferResponse, loanID); + validateResponse(saleTransferResponse, loanID, false); PostInitiateTransferResponse buybackTransferResponse = createBuybackTransfer(loanID, "2020-03-02"); - validateResponse(buybackTransferResponse, loanID); + validateResponse(buybackTransferResponse, loanID, true); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-02", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "9999-12-31", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-02", "2020-03-02", "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); @@ -775,22 +796,22 @@ public void saleAndBuybackOnTheSameDay() { updateBusinessDateAndExecuteCOBJob("2020-03-03"); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-02", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-02", "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(CANCELLED, buybackTransferResponse.getResourceExternalId(), "2020-03-02", + ExpectedExternalTransferData.expected(CANCELLED, buybackTransferResponse.getResourceExternalId(), null, "2020-03-02", "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), "2020-03-02", - "2020-03-02", "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); getAndValidateThereIsNoActiveMapping((long) loanID); } finally { cleanUpAndRestoreBusinessDate(); @@ -809,11 +830,11 @@ public void saleAndBuybackMultipleTimes() { PostInitiateTransferResponse buybackTransferResponse = createBuybackTransfer(loanID, "2020-03-04"); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-04", "2020-03-02", - "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-04", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-04", "2020-03-02", "9999-12-31", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-04", "2020-03-02", "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); @@ -868,10 +889,12 @@ public void buybackExceptionHandling() { assertTrue(exception5.getMessage().contains("Loan with identifier -1 does not exist")); String externalId = UUID.randomUUID().toString(); + String transferExternalReferenceId = 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, transferExternalReferenceId, "1", "1.0"); createBuybackTransfer(loanID, "2020-03-02", externalId); }); assertTrue(exception6.getMessage() @@ -892,12 +915,12 @@ public void saleExceptionHandling() { 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 +941,11 @@ public void saleExceptionHandling() { }); assertTrue(exception6.getMessage().contains("This loan cannot be sold, because it is owned by an external asset owner")); String externalId = UUID.randomUUID().toString(); + String transferExternalReferenceId = 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, transferExternalReferenceId, "1", "1.0"); + createSaleTransfer(loanID2, "2020-03-05", externalId, transferExternalReferenceId, "1", "1.0"); }); assertTrue(exception7.getMessage() .contains(String.format("Already existing an asset transfer with the provided transfer external id: %s", externalId))); @@ -945,26 +969,26 @@ public void transactionSummaryReportWithAssetOwner() throws IOException { addPenaltyForLoan(loanID, "10"); final var saleTransferResponse = createSaleTransfer(loanID, "2020-03-02"); - validateResponse(saleTransferResponse, loanID); + validateResponse(saleTransferResponse, loanID, false); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "9999-12-31", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); getAndValidateThereIsNoActiveMapping(saleTransferResponse.getResourceExternalId()); var retrieveResponse = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue()); retrieveResponse.getContent().forEach(transfer -> getAndValidateThereIsNoJournalEntriesForTransfer(transfer.getTransferId())); updateBusinessDateAndExecuteCOBJob("2020-03-03"); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "9999-12-31", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "9999-12-31", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); final var allExternalEvents = ExternalEventHelper.getAllExternalEvents(REQUEST_SPEC, RESPONSE_SPEC); Assertions.assertEquals(1, allExternalEvents.size()); @@ -980,17 +1004,17 @@ public void transactionSummaryReportWithAssetOwner() throws IOException { final var initial = 0; final var buybackTransferResponse = createBuybackTransfer(loanID, "2020-03-03"); - validateResponse(buybackTransferResponse, loanID); + validateResponse(buybackTransferResponse, loanID, true); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "9999-12-31", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-03", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "9999-12-31", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-03", "2020-03-03", "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); @@ -1003,15 +1027,15 @@ public void transactionSummaryReportWithAssetOwner() throws IOException { updateBusinessDateAndExecuteCOBJob("2020-03-04"); getAndValidateExternalAssetOwnerTransferByLoan(loanID, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-02", - "2020-03-02", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), "2020-03-02", "2020-03-03", - "2020-03-03", true, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), "2020-03-03", + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-02", "2020-03-02", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(ACTIVE, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), "2020-03-02", "2020-03-03", "2020-03-03", true, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), + ExpectedExternalTransferData.expected(BUYBACK, buybackTransferResponse.getResourceExternalId(), null, "2020-03-03", "2020-03-03", "2020-03-03", true, new BigDecimal("15762.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("5.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); @@ -1178,15 +1202,17 @@ private void updateBusinessDateAndExecuteCOBJob(String date) { private PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate) { String transferExternalId = UUID.randomUUID().toString(); + String transferExternalReferenceId = UUID.randomUUID().toString(); ownerExternalId = UUID.randomUUID().toString(); - return createSaleTransfer(loanID, settlementDate, transferExternalId, ownerExternalId, "1.0"); + return createSaleTransfer(loanID, settlementDate, transferExternalId, transferExternalReferenceId, ownerExternalId, "1.0"); } private PostInitiateTransferResponse createSaleTransfer(Integer loanID, String settlementDate, String transferExternalId, - String ownerExternalId, String purchasePriceRatio) { + String transferExternalReferenceId, 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).transferExternalReferenceId(transferExternalReferenceId) + .ownerExternalId(ownerExternalId).purchasePriceRatio(purchasePriceRatio)); assertEquals(transferExternalId, saleResponse.getResourceExternalId()); return saleResponse; } @@ -1315,6 +1341,9 @@ private void getAndValidateExternalAssetOwnerTransferByLoan(Integer loanID, Expe assertTrue(first.isPresent()); ExternalTransferData etd = first.get(); assertEquals(expected.transferExternalId, etd.getTransferExternalId()); + + assertEquals(expected.transferExternalReferenceId, etd.getTransferExternalReferenceId()); + assertEquals(expected.status, etd.getStatus()); assertEquals(LocalDate.parse(expected.settlementDate), etd.getSettlementDate()); assertEquals(LocalDate.parse(expected.effectiveFrom), etd.getEffectiveFrom()); @@ -1354,10 +1383,13 @@ private void getAndValidateThereIsNoActiveMapping(String transferExternalId) { assertNull(activeTransfer); } - private void validateResponse(PostInitiateTransferResponse transferResponse, Integer loanID) { + private void validateResponse(PostInitiateTransferResponse transferResponse, Integer loanID, boolean buyback) { assertNotNull(transferResponse); assertNotNull(transferResponse.getResourceId()); assertNotNull(transferResponse.getResourceExternalId()); + if (!buyback) { + assertNotNull(transferResponse.getResourceExternalReferenceId()); + } assertNotNull(transferResponse.getSubResourceId()); assertEquals((long) loanID, transferResponse.getSubResourceId()); assertNotNull(transferResponse.getSubResourceExternalId()); @@ -1408,6 +1440,7 @@ public static class ExpectedExternalTransferData { private final ExternalTransferData.StatusEnum status; private final String transferExternalId; + private final String transferExternalReferenceId; private final String settlementDate; @@ -1415,6 +1448,7 @@ public static class ExpectedExternalTransferData { 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; @@ -1423,24 +1457,26 @@ public static class ExpectedExternalTransferData { private final BigDecimal totalOverpaid; static ExpectedExternalTransferData expected(ExternalTransferData.StatusEnum status, String transferExternalId, - String settlementDate, String effectiveFrom, String effectiveTo, boolean detailsExpected, BigDecimal totalOutstanding, - BigDecimal totalPrincipalOutstanding, BigDecimal totalInterestOutstanding, BigDecimal totalPenaltyOutstanding, - BigDecimal totalFeeOutstanding, BigDecimal totalOverpaid) { - return new ExpectedExternalTransferData(status, transferExternalId, settlementDate, effectiveFrom, effectiveTo, null, - detailsExpected, totalOutstanding, totalPrincipalOutstanding, totalInterestOutstanding, totalPenaltyOutstanding, - totalFeeOutstanding, totalOverpaid); + String transferExternalReferenceId, String settlementDate, String effectiveFrom, String effectiveTo, + boolean detailsExpected, BigDecimal totalOutstanding, BigDecimal totalPrincipalOutstanding, + BigDecimal totalInterestOutstanding, BigDecimal totalPenaltyOutstanding, BigDecimal totalFeeOutstanding, + BigDecimal totalOverpaid) { + return new ExpectedExternalTransferData(status, transferExternalId, transferExternalReferenceId, settlementDate, effectiveFrom, + effectiveTo, null, detailsExpected, totalOutstanding, totalPrincipalOutstanding, totalInterestOutstanding, + totalPenaltyOutstanding, totalFeeOutstanding, totalOverpaid); } static ExpectedExternalTransferData expected(ExternalTransferData.StatusEnum status, String transferExternalId, - String settlementDate, String effectiveFrom, String effectiveTo) { - return new ExpectedExternalTransferData(status, transferExternalId, settlementDate, effectiveFrom, effectiveTo, null, false, - null, null, null, null, null, null); + String transferExternalReferenceId, String settlementDate, String effectiveFrom, String effectiveTo) { + return new ExpectedExternalTransferData(status, transferExternalId, transferExternalReferenceId, settlementDate, effectiveFrom, + effectiveTo, null, false, null, null, null, null, null, null); } static ExpectedExternalTransferData expected(ExternalTransferData.StatusEnum status, String transferExternalId, - String settlementDate, String effectiveFrom, String effectiveTo, ExternalTransferData.SubStatusEnum subStatus) { - return new ExpectedExternalTransferData(status, transferExternalId, settlementDate, effectiveFrom, effectiveTo, subStatus, - false, null, null, null, null, null, null); + String transferExternalReferenceId, String settlementDate, String effectiveFrom, String effectiveTo, + ExternalTransferData.SubStatusEnum subStatus) { + return new ExpectedExternalTransferData(status, transferExternalId, transferExternalReferenceId, settlementDate, effectiveFrom, + effectiveTo, subStatus, false, null, null, null, null, null, null); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java index de779040432..453fdc6414a 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java @@ -60,20 +60,20 @@ public void saleActiveLoanToExternalAssetOwnerWithSearching() { PageExternalTransferData response = EXTERNAL_ASSET_OWNER_HELPER.searchExternalAssetOwnerTransfer(searchRequest); validateExternalAssetOwnerTransfer(response, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), baseDate, baseDate, - "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), baseDate, baseDate, "9999-12-31", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); // LookUp by Effective Date searchRequest = EXTERNAL_ASSET_OWNER_HELPER.buildExternalAssetOwnerSearchRequest("", "settlement", baseLocalDate, null, null, null); response = EXTERNAL_ASSET_OWNER_HELPER.searchExternalAssetOwnerTransfer(searchRequest); validateExternalAssetOwnerTransfer(response, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), baseDate, baseDate, - "9999-12-31", false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), - new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), - new BigDecimal("0.000000"))); + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), baseDate, baseDate, "9999-12-31", false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); // Cancel the External Asset Transfer EXTERNAL_ASSET_OWNER_HELPER.cancelTransferByTransferExternalId(saleTransferResponse.getResourceExternalId()); @@ -81,11 +81,13 @@ public void saleActiveLoanToExternalAssetOwnerWithSearching() { response = EXTERNAL_ASSET_OWNER_HELPER.searchExternalAssetOwnerTransfer(searchRequest); validateExternalAssetOwnerTransfer(response, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), baseDate, baseDate, - baseDate, false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), baseDate, baseDate, baseDate, false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), baseDate, baseDate, - baseDate, false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), baseDate, baseDate, baseDate, false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); // LookUp by Effective Date @@ -95,11 +97,13 @@ baseDate, false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), response = EXTERNAL_ASSET_OWNER_HELPER.searchExternalAssetOwnerTransfer(searchRequest); validateExternalAssetOwnerTransfer(response, - ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), baseDate, baseDate, - baseDate, false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + ExpectedExternalTransferData.expected(PENDING, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), baseDate, baseDate, baseDate, false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000")), - ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), baseDate, baseDate, - baseDate, false, new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), + ExpectedExternalTransferData.expected(CANCELLED, saleTransferResponse.getResourceExternalId(), + saleTransferResponse.getResourceExternalReferenceId(), baseDate, baseDate, baseDate, false, + new BigDecimal("15767.420000"), new BigDecimal("15000.000000"), new BigDecimal("757.420000"), new BigDecimal("10.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); } finally {