FINERACT-2141: Delayed settlement implementation for fineract loan investor module

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