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
@@ -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>