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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;

public interface ExternalAssetOwnerRepository
extends JpaRepository<ExternalAssetOwner, Long>, JpaSpecificationExecutor<ExternalAssetOwner> {

Optional<ExternalAssetOwner> findByExternalId(ExternalId externalId);

@Query("SELECT e.id FROM ExternalAssetOwner e WHERE e.externalId = :externalId")
Optional<Long> findIdByExternalId(ExternalId externalId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* 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.infrastructure.core.domain.ExternalId;
import org.apache.fineract.investor.domain.ExternalAssetOwner;
import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ExternalAssetOwnerTransactionalHelper {

private final ExternalAssetOwnerRepository repository;

// REQUIRES_NEW isolates the INSERT into a separate transaction and persistence context,
// so a constraint violation does not corrupt the caller's Hibernate Session or mark the
// outer transaction as rollback-only, allowing a safe retry.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Long findOrCreateId(final ExternalId externalId) {
return repository.findIdByExternalId(externalId).orElseGet(() -> {
final ExternalAssetOwner owner = new ExternalAssetOwner();
owner.setExternalId(externalId);
return repository.saveAndFlush(owner).getId();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -65,6 +66,9 @@
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.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -77,6 +81,8 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
ExternalTransferStatus.ACTIVE);
private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT = List
.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE, ExternalTransferStatus.ACTIVE);
private static final String SQL_STATE_INTEGRITY_CONSTRAINT_VIOLATION = "23";

private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
private final FromJsonHelper fromApiJsonHelper;
Expand All @@ -85,6 +91,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
private final ConfigurationDomainService configurationDomainService;
private final ExternalAssetOwnersReadService externalAssetOwnersReadService;
private final ExternalAssetOwnerValidator externalAssetOwnerValidator;
private final ExternalAssetOwnerTransactionalHelper externalAssetOwnerTransactionalHelper;

@Override
@Transactional
Expand Down Expand Up @@ -171,15 +178,16 @@ private void validateEffectiveTransferForSale(final List<ExternalAssetOwnerTrans
if (effectiveTransfers.size() == 2) {
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
} else if (effectiveTransfers.size() == 1) {
if (PENDING.equals(effectiveTransfers.get(0).getStatus())) {
if (PENDING.equals(effectiveTransfers.getFirst().getStatus())) {
throw new ExternalAssetOwnerInitiateTransferException(
"External asset owner transfer is already in PENDING state for this loan");
} else if (ExternalTransferStatus.ACTIVE.equals(effectiveTransfers.get(0).getStatus())) {
} else if (ExternalTransferStatus.ACTIVE.equals(effectiveTransfers.getFirst().getStatus())) {
throw new ExternalAssetOwnerInitiateTransferException(
"This loan cannot be sold, because it is owned by an external asset owner");
} else {
throw new ExternalAssetOwnerInitiateTransferException(String.format(
"This loan cannot be sold, because it is incorrect state! (transferId = %s)", effectiveTransfers.get(0).getId()));
throw new ExternalAssetOwnerInitiateTransferException(
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)",
effectiveTransfers.getFirst().getId()));
}
}
}
Expand All @@ -188,7 +196,7 @@ private void validateEffectiveTransferForDelayedSettlementSale(final List<Extern
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())) {
if (!ACTIVE_INTERMEDIATE.equals(effectiveTransfers.getFirst().getStatus())) {
throw new ExternalAssetOwnerInitiateTransferException(
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state.");
}
Expand All @@ -204,15 +212,16 @@ private void validateEffectiveTransferForIntermediarySale(final ExternalAssetOwn
if (effectiveTransfers.size() > 1) {
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
} else if (effectiveTransfers.size() == 1) {
if (PENDING_INTERMEDIATE.equals(effectiveTransfers.get(0).getStatus())) {
if (PENDING_INTERMEDIATE.equals(effectiveTransfers.getFirst().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())) {
} else if (ExternalTransferStatus.ACTIVE.equals(effectiveTransfers.getFirst().getStatus())) {
throw new ExternalAssetOwnerInitiateTransferException(
"This loan cannot be sold, because it is owned by an external asset owner");
} else {
throw new ExternalAssetOwnerInitiateTransferException(String.format(
"This loan cannot be sold, because it is incorrect state! (transferId = %s)", effectiveTransfers.get(0).getId()));
throw new ExternalAssetOwnerInitiateTransferException(
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)",
effectiveTransfers.getFirst().getId()));
}
}
}
Expand All @@ -232,17 +241,17 @@ private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuyback(
} else if (effectiveTransfers.size() == 2) {
throw new ExternalAssetOwnerInitiateTransferException(
"This loan cannot be bought back, external asset owner buyback transfer is already in progress");
} else if (!BUYBACK_READY_STATUSES.contains(effectiveTransfers.get(0).getStatus())) {
} else if (!BUYBACK_READY_STATUSES.contains(effectiveTransfers.getFirst().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())) {
effectiveTransfers.getFirst().getStatus()));
} else if (DateUtils.isBefore(settlementDate, effectiveTransfers.getFirst().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()));
effectiveTransfers.getFirst().getSettlementDate()));
}

return effectiveTransfers.get(0);
return effectiveTransfers.getFirst();
}

private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuybackWithDelayedSettlement(
Expand All @@ -265,17 +274,17 @@ private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuybackWi
|| 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())) {
} else if (!BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT.contains(effectiveTransfers.getFirst().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())) {
effectiveTransfers.getFirst().getStatus()));
} else if (DateUtils.isBefore(settlementDate, effectiveTransfers.getFirst().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()));
effectiveTransfers.getFirst().getSettlementDate()));
}

return effectiveTransfers.get(0);
return effectiveTransfers.getFirst();
}

private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForCancel(final Long transferId) {
Expand All @@ -287,10 +296,9 @@ private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForCancel(fi
.findEffectiveTransfersOrderByIdDesc(selectedTransfer.getLoanId(), DateUtils.getBusinessLocalDate());
if (effective.isEmpty()) {
throw new ExternalAssetOwnerInitiateTransferException(
String.format("This loan cannot be cancelled, there is no effective transfer for this loan"));
} else if (!Objects.equals(effective.get(0).getId(), selectedTransfer.getId())) {
throw new ExternalAssetOwnerInitiateTransferException(
String.format("This loan cannot be cancelled, selected transfer is not the latest"));
"This loan cannot be cancelled, there is no effective transfer for this loan");
} else if (!Objects.equals(effective.getFirst().getId(), selectedTransfer.getId())) {
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be cancelled, selected transfer is not the latest");
} else if (selectedTransfer.getStatus() != PENDING && selectedTransfer.getStatus() != ExternalTransferStatus.BUYBACK) {
throw new ExternalAssetOwnerInitiateTransferException(
"This loan cannot be cancelled, the selected transfer status is not pending or buyback");
Expand Down Expand Up @@ -318,8 +326,7 @@ private ExternalAssetOwnerTransfer createBuybackTransfer(ExternalAssetOwnerTrans

private ExternalTransferStatus determineStatusAfterBuyback(ExternalAssetOwnerTransfer effectiveTransfer) {
return switch (effectiveTransfer.getStatus()) {
case PENDING -> ExternalTransferStatus.BUYBACK;
case ACTIVE -> ExternalTransferStatus.BUYBACK;
case PENDING, 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()));
Expand Down Expand Up @@ -582,17 +589,33 @@ private String getPurchasePriceRatioFromJson(JsonElement json) {
return fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, json);
}

private ExternalAssetOwner getOwner(JsonElement json) {
String ownerExternalId = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, json);
Optional<ExternalAssetOwner> byExternalId = externalAssetOwnerRepository
.findByExternalId(ExternalIdFactory.produce(ownerExternalId));
return byExternalId.orElseGet(() -> createAndGetAssetOwner(ownerExternalId));
private ExternalAssetOwner getOwner(final JsonElement json) {
final String ownerExternalId = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, json);
final ExternalId externalId = ExternalIdFactory.produce(ownerExternalId);
return externalAssetOwnerRepository.findByExternalId(externalId).orElseGet(() -> {
final Long ownerId = findOrCreateOwnerId(externalId);
// getReferenceById returns a lazy proxy without hitting the DB. findById would fail
// here because the outer transaction's persistence context does not contain the entity
// committed by the inner REQUIRES_NEW transaction.
return externalAssetOwnerRepository.getReferenceById(ownerId);
});
}

private Long findOrCreateOwnerId(final ExternalId externalId) {
try {
return externalAssetOwnerTransactionalHelper.findOrCreateId(externalId);
} catch (JpaSystemException | DataIntegrityViolationException e) {
if (!isConstraintViolation(e)) {
throw e;
}
// Another thread created the owner concurrently - retry
return externalAssetOwnerTransactionalHelper.findOrCreateId(externalId);
}
}

private ExternalAssetOwner createAndGetAssetOwner(String externalId) {
ExternalAssetOwner externalAssetOwner = new ExternalAssetOwner();
externalAssetOwner.setExternalId(ExternalIdFactory.produce(externalId));
return externalAssetOwnerRepository.saveAndFlush(externalAssetOwner);
private boolean isConstraintViolation(final DataAccessException e) {
return e.getMostSpecificCause() instanceof SQLException sqlEx && sqlEx.getSQLState() != null
&& sqlEx.getSQLState().startsWith(SQL_STATE_INTEGRITY_CONSTRAINT_VIOLATION);
}

private List<LoanStatus> getAllowedLoanStatuses() {
Expand Down
Loading