FINERACT-2421: Retry external asset owner creation in case of unique constraint violation
This commit is contained in:
committed by
Adam Saghy
parent
da85c069b7
commit
020754bad4
+4
@@ -22,10 +22,14 @@ import java.util.Optional;
|
|||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
public interface ExternalAssetOwnerRepository
|
public interface ExternalAssetOwnerRepository
|
||||||
extends JpaRepository<ExternalAssetOwner, Long>, JpaSpecificationExecutor<ExternalAssetOwner> {
|
extends JpaRepository<ExternalAssetOwner, Long>, JpaSpecificationExecutor<ExternalAssetOwner> {
|
||||||
|
|
||||||
Optional<ExternalAssetOwner> findByExternalId(ExternalId externalId);
|
Optional<ExternalAssetOwner> findByExternalId(ExternalId externalId);
|
||||||
|
|
||||||
|
@Query("SELECT e.id FROM ExternalAssetOwner e WHERE e.externalId = :externalId")
|
||||||
|
Optional<Long> findIdByExternalId(ExternalId externalId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+46
@@ -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 ExternalAssetOwnerHelper {
|
||||||
|
|
||||||
|
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 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
-31
@@ -25,6 +25,7 @@ import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING_I
|
|||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -65,6 +66,9 @@ import org.apache.fineract.investor.serialization.ExternalAssetOwnerValidator;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
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.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -77,6 +81,8 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
ExternalTransferStatus.ACTIVE);
|
ExternalTransferStatus.ACTIVE);
|
||||||
private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT = List
|
private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT = List
|
||||||
.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE, ExternalTransferStatus.ACTIVE);
|
.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE, ExternalTransferStatus.ACTIVE);
|
||||||
|
private static final String SQL_STATE_INTEGRITY_CONSTRAINT_VIOLATION = "23";
|
||||||
|
|
||||||
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
||||||
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
|
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
@@ -85,6 +91,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
private final ConfigurationDomainService configurationDomainService;
|
private final ConfigurationDomainService configurationDomainService;
|
||||||
private final ExternalAssetOwnersReadService externalAssetOwnersReadService;
|
private final ExternalAssetOwnersReadService externalAssetOwnersReadService;
|
||||||
private final ExternalAssetOwnerValidator externalAssetOwnerValidator;
|
private final ExternalAssetOwnerValidator externalAssetOwnerValidator;
|
||||||
|
private final ExternalAssetOwnerHelper externalAssetOwnerHelper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -171,15 +178,16 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
if (effectiveTransfers.size() == 2) {
|
if (effectiveTransfers.size() == 2) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
||||||
} else if (effectiveTransfers.size() == 1) {
|
} else if (effectiveTransfers.size() == 1) {
|
||||||
if (PENDING.equals(effectiveTransfers.get(0).getStatus())) {
|
if (PENDING.equals(effectiveTransfers.getFirst().getStatus())) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"External asset owner transfer is already in PENDING state for this loan");
|
"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(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"This loan cannot be sold, because it is owned by an external asset owner");
|
"This loan cannot be sold, because it is owned by an external asset owner");
|
||||||
} else {
|
} else {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(String.format(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"This loan cannot be sold, because it is incorrect state! (transferId = %s)", effectiveTransfers.get(0).getId()));
|
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)",
|
||||||
|
effectiveTransfers.getFirst().getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +196,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
if (effectiveTransfers.size() > 1) {
|
if (effectiveTransfers.size() > 1) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
||||||
} else if (effectiveTransfers.size() == 1) {
|
} else if (effectiveTransfers.size() == 1) {
|
||||||
if (!ACTIVE_INTERMEDIATE.equals(effectiveTransfers.get(0).getStatus())) {
|
if (!ACTIVE_INTERMEDIATE.equals(effectiveTransfers.getFirst().getStatus())) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state.");
|
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state.");
|
||||||
}
|
}
|
||||||
@@ -204,15 +212,16 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
if (effectiveTransfers.size() > 1) {
|
if (effectiveTransfers.size() > 1) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
||||||
} else if (effectiveTransfers.size() == 1) {
|
} else if (effectiveTransfers.size() == 1) {
|
||||||
if (PENDING_INTERMEDIATE.equals(effectiveTransfers.get(0).getStatus())) {
|
if (PENDING_INTERMEDIATE.equals(effectiveTransfers.getFirst().getStatus())) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan");
|
"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(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"This loan cannot be sold, because it is owned by an external asset owner");
|
"This loan cannot be sold, because it is owned by an external asset owner");
|
||||||
} else {
|
} else {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(String.format(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"This loan cannot be sold, because it is incorrect state! (transferId = %s)", effectiveTransfers.get(0).getId()));
|
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)",
|
||||||
|
effectiveTransfers.getFirst().getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,17 +241,17 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
} else if (effectiveTransfers.size() == 2) {
|
} else if (effectiveTransfers.size() == 2) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"This loan cannot be bought back, external asset owner buyback transfer is already in progress");
|
"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(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
String.format("This loan cannot be bought back, effective transfer is not in right state: %s",
|
String.format("This loan cannot be bought back, effective transfer is not in right state: %s",
|
||||||
effectiveTransfers.get(0).getStatus()));
|
effectiveTransfers.getFirst().getStatus()));
|
||||||
} else if (DateUtils.isBefore(settlementDate, effectiveTransfers.get(0).getSettlementDate())) {
|
} else if (DateUtils.isBefore(settlementDate, effectiveTransfers.getFirst().getSettlementDate())) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
String.format("This loan cannot be bought back, settlement date is earlier than effective transfer settlement date: %s",
|
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(
|
private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuybackWithDelayedSettlement(
|
||||||
@@ -265,17 +274,17 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
|| Set.of(ExternalTransferStatus.ACTIVE, ExternalTransferStatus.BUYBACK).equals(effectiveTransferStatuses)) {
|
|| Set.of(ExternalTransferStatus.ACTIVE, ExternalTransferStatus.BUYBACK).equals(effectiveTransferStatuses)) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"This loan cannot be bought back, external asset owner buyback transfer is already in progress");
|
"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(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
String.format("This loan cannot be bought back, effective transfer is not in right state: %s",
|
String.format("This loan cannot be bought back, effective transfer is not in right state: %s",
|
||||||
effectiveTransfers.get(0).getStatus()));
|
effectiveTransfers.getFirst().getStatus()));
|
||||||
} else if (DateUtils.isBefore(settlementDate, effectiveTransfers.get(0).getSettlementDate())) {
|
} else if (DateUtils.isBefore(settlementDate, effectiveTransfers.getFirst().getSettlementDate())) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
String.format("This loan cannot be bought back, settlement date is earlier than effective transfer settlement date: %s",
|
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) {
|
private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForCancel(final Long transferId) {
|
||||||
@@ -287,10 +296,9 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
.findEffectiveTransfersOrderByIdDesc(selectedTransfer.getLoanId(), DateUtils.getBusinessLocalDate());
|
.findEffectiveTransfersOrderByIdDesc(selectedTransfer.getLoanId(), DateUtils.getBusinessLocalDate());
|
||||||
if (effective.isEmpty()) {
|
if (effective.isEmpty()) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
String.format("This loan cannot be cancelled, there is no effective transfer for this loan"));
|
"This loan cannot be cancelled, there is no effective transfer for this loan");
|
||||||
} else if (!Objects.equals(effective.get(0).getId(), selectedTransfer.getId())) {
|
} else if (!Objects.equals(effective.getFirst().getId(), selectedTransfer.getId())) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be cancelled, selected transfer is not the latest");
|
||||||
String.format("This loan cannot be cancelled, selected transfer is not the latest"));
|
|
||||||
} else if (selectedTransfer.getStatus() != PENDING && selectedTransfer.getStatus() != ExternalTransferStatus.BUYBACK) {
|
} else if (selectedTransfer.getStatus() != PENDING && selectedTransfer.getStatus() != ExternalTransferStatus.BUYBACK) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"This loan cannot be cancelled, the selected transfer status is not pending or buyback");
|
"This loan cannot be cancelled, the selected transfer status is not pending or buyback");
|
||||||
@@ -318,8 +326,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
|
|
||||||
private ExternalTransferStatus determineStatusAfterBuyback(ExternalAssetOwnerTransfer effectiveTransfer) {
|
private ExternalTransferStatus determineStatusAfterBuyback(ExternalAssetOwnerTransfer effectiveTransfer) {
|
||||||
return switch (effectiveTransfer.getStatus()) {
|
return switch (effectiveTransfer.getStatus()) {
|
||||||
case PENDING -> ExternalTransferStatus.BUYBACK;
|
case PENDING, ACTIVE -> ExternalTransferStatus.BUYBACK;
|
||||||
case ACTIVE -> ExternalTransferStatus.BUYBACK;
|
|
||||||
case ACTIVE_INTERMEDIATE -> ExternalTransferStatus.BUYBACK_INTERMEDIATE;
|
case ACTIVE_INTERMEDIATE -> ExternalTransferStatus.BUYBACK_INTERMEDIATE;
|
||||||
default -> throw new ExternalAssetOwnerInitiateTransferException(String.format(
|
default -> throw new ExternalAssetOwnerInitiateTransferException(String.format(
|
||||||
"This loan cannot be bought back, effective transfer is not in right state: %s", effectiveTransfer.getStatus()));
|
"This loan cannot be bought back, effective transfer is not in right state: %s", effectiveTransfer.getStatus()));
|
||||||
@@ -582,11 +589,33 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
return fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, json);
|
return fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExternalAssetOwner getOwner(JsonElement json) {
|
private ExternalAssetOwner getOwner(final JsonElement json) {
|
||||||
String ownerExternalId = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, json);
|
final String ownerExternalId = fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, json);
|
||||||
Optional<ExternalAssetOwner> byExternalId = externalAssetOwnerRepository
|
final ExternalId externalId = ExternalIdFactory.produce(ownerExternalId);
|
||||||
.findByExternalId(ExternalIdFactory.produce(ownerExternalId));
|
return externalAssetOwnerRepository.findByExternalId(externalId).orElseGet(() -> {
|
||||||
return byExternalId.orElseGet(() -> createAndGetAssetOwner(ownerExternalId));
|
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 externalAssetOwnerHelper.findOrCreateId(externalId);
|
||||||
|
} catch (JpaSystemException | DataIntegrityViolationException e) {
|
||||||
|
if (!isConstraintViolation(e)) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
// Another thread created the owner concurrently - retry
|
||||||
|
return externalAssetOwnerHelper.findOrCreateId(externalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isConstraintViolation(final DataAccessException e) {
|
||||||
|
return e.getMostSpecificCause() instanceof SQLException sqlEx && sqlEx.getSQLState() != null
|
||||||
|
&& sqlEx.getSQLState().startsWith(SQL_STATE_INTEGRITY_CONSTRAINT_VIOLATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExternalAssetOwner createAndGetAssetOwner(String externalId) {
|
private ExternalAssetOwner createAndGetAssetOwner(String externalId) {
|
||||||
@@ -611,7 +640,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
String ownerExternalId = command.stringValueOfParameterNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID);
|
String ownerExternalId = command.stringValueOfParameterNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID);
|
||||||
Optional<ExternalAssetOwner> optExternalId = externalAssetOwnerRepository
|
Optional<ExternalAssetOwner> optExternalId = externalAssetOwnerRepository
|
||||||
.findByExternalId(ExternalIdFactory.produce(ownerExternalId));
|
.findByExternalId(ExternalIdFactory.produce(ownerExternalId));
|
||||||
if (!optExternalId.isEmpty()) {
|
if (optExternalId.isPresent()) {
|
||||||
throw new ExternalAssetOwnerDuplicateException(ownerExternalId);
|
throw new ExternalAssetOwnerDuplicateException(ownerExternalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+98
-7
@@ -38,6 +38,7 @@ import com.google.gson.JsonElement;
|
|||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -79,7 +80,9 @@ import org.mockito.InjectMocks;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
public class ExternalAssetOwnersWriteServiceTest {
|
public class ExternalAssetOwnersWriteServiceTest {
|
||||||
@@ -346,7 +349,7 @@ public class ExternalAssetOwnersWriteServiceTest {
|
|||||||
testContext.fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, jsonCommandElement))
|
testContext.fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, jsonCommandElement))
|
||||||
.thenReturn(LocalDate.EPOCH);
|
.thenReturn(LocalDate.EPOCH);
|
||||||
lenient().when(testContext.fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE,
|
lenient().when(testContext.fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE,
|
||||||
jsonCommandElement, testContext.DATE_FORMAT, Locale.GERMANY)).thenReturn(LocalDate.EPOCH);
|
jsonCommandElement, TestContext.DATE_FORMAT, Locale.GERMANY)).thenReturn(LocalDate.EPOCH);
|
||||||
|
|
||||||
// given
|
// given
|
||||||
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
@@ -364,7 +367,7 @@ public class ExternalAssetOwnersWriteServiceTest {
|
|||||||
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
|
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
|
||||||
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
|
||||||
Assertions.assertEquals(thrownException.getMessage(), "Settlement date cannot be in the past");
|
Assertions.assertEquals("Settlement date cannot be in the past", thrownException.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Stream<Arguments> effectiveTransferDataProvider() {
|
private static Stream<Arguments> effectiveTransferDataProvider() {
|
||||||
@@ -440,14 +443,17 @@ public class ExternalAssetOwnersWriteServiceTest {
|
|||||||
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class))).thenReturn(Optional.empty());
|
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class))).thenReturn(Optional.empty());
|
||||||
|
when(testContext.externalAssetOwnerHelper.findOrCreateId(any(ExternalId.class))).thenReturn(42L);
|
||||||
|
when(testContext.externalAssetOwnerRepository.getReferenceById(42L)).thenReturn(testContext.externalAssetOwner);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command);
|
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
|
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
|
||||||
|
verify(testContext.externalAssetOwnerHelper).findOrCreateId(any(ExternalId.class));
|
||||||
|
verify(testContext.externalAssetOwnerRepository).getReferenceById(42L);
|
||||||
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
|
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
|
||||||
verify(testContext.externalAssetOwnerRepository).saveAndFlush(any(ExternalAssetOwner.class));
|
|
||||||
verify(testContext.externalAssetOwnerTransferRepository).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
verify(testContext.externalAssetOwnerTransferRepository).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
|
||||||
any(LocalDate.class));
|
any(LocalDate.class));
|
||||||
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
@@ -462,6 +468,89 @@ public class ExternalAssetOwnersWriteServiceTest {
|
|||||||
assertEquals(savedTransfer.getExternalLoanId(), result.getSubResourceExternalId());
|
assertEquals(savedTransfer.getExternalLoanId(), result.getSubResourceExternalId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyWhenOwnerCreationHitsConstraintViolationWithJpaSystemExceptionThenRetrySucceeds() {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
final ArgumentCaptor<ExternalAssetOwnerTransfer> transferCaptor = ArgumentCaptor.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
final SQLException sqlException = new SQLException("Duplicate entry", "23505");
|
||||||
|
final JpaSystemException jpaException = new JpaSystemException(new RuntimeException(sqlException));
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class))).thenReturn(Optional.empty());
|
||||||
|
when(testContext.externalAssetOwnerHelper.findOrCreateId(any(ExternalId.class))).thenThrow(jpaException).thenReturn(99L);
|
||||||
|
when(testContext.externalAssetOwnerRepository.getReferenceById(99L)).thenReturn(testContext.externalAssetOwner);
|
||||||
|
|
||||||
|
// when
|
||||||
|
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.externalAssetOwnerHelper, times(2)).findOrCreateId(any(ExternalId.class));
|
||||||
|
verify(testContext.externalAssetOwnerRepository).getReferenceById(99L);
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(transferCaptor.capture());
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer savedTransfer = transferCaptor.getValue();
|
||||||
|
assertAssertOwnerTransferValues(testContext, savedTransfer, ExternalTransferStatus.PENDING);
|
||||||
|
assertEquals(savedTransfer.getId(), result.getResourceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyWhenOwnerCreationHitsConstraintViolationWithDataIntegrityViolationExceptionThenRetrySucceeds() {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
final ArgumentCaptor<ExternalAssetOwnerTransfer> transferCaptor = ArgumentCaptor.forClass(ExternalAssetOwnerTransfer.class);
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
final SQLException sqlException = new SQLException("Duplicate entry", "23000");
|
||||||
|
final DataIntegrityViolationException diveException = new DataIntegrityViolationException("Duplicate", sqlException);
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class))).thenReturn(Optional.empty());
|
||||||
|
when(testContext.externalAssetOwnerHelper.findOrCreateId(any(ExternalId.class))).thenThrow(diveException).thenReturn(99L);
|
||||||
|
when(testContext.externalAssetOwnerRepository.getReferenceById(99L)).thenReturn(testContext.externalAssetOwner);
|
||||||
|
|
||||||
|
// when
|
||||||
|
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(testContext.externalAssetOwnerHelper, times(2)).findOrCreateId(any(ExternalId.class));
|
||||||
|
verify(testContext.externalAssetOwnerRepository).getReferenceById(99L);
|
||||||
|
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(transferCaptor.capture());
|
||||||
|
|
||||||
|
ExternalAssetOwnerTransfer savedTransfer = transferCaptor.getValue();
|
||||||
|
assertAssertOwnerTransferValues(testContext, savedTransfer, ExternalTransferStatus.PENDING);
|
||||||
|
assertEquals(savedTransfer.getId(), result.getResourceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyWhenOwnerCreationThrowsNonConstraintJpaSystemExceptionThenExceptionPropagates() {
|
||||||
|
final TestContext testContext = new TestContext();
|
||||||
|
|
||||||
|
// given
|
||||||
|
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
|
||||||
|
|
||||||
|
final JpaSystemException jpaException = new JpaSystemException(new RuntimeException("Connection lost"));
|
||||||
|
|
||||||
|
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
|
||||||
|
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
|
||||||
|
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
|
||||||
|
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class))).thenReturn(Optional.empty());
|
||||||
|
when(testContext.externalAssetOwnerHelper.findOrCreateId(any(ExternalId.class))).thenThrow(jpaException);
|
||||||
|
|
||||||
|
// when & then
|
||||||
|
assertThrows(JpaSystemException.class, () -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
||||||
|
|
||||||
|
verify(testContext.externalAssetOwnerHelper, times(1)).findOrCreateId(any(ExternalId.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void verifyWhenLoanIsNotFoundThenExceptionIsThrown() {
|
public void verifyWhenLoanIsNotFoundThenExceptionIsThrown() {
|
||||||
final TestContext testContext = new TestContext();
|
final TestContext testContext = new TestContext();
|
||||||
@@ -574,7 +663,7 @@ public class ExternalAssetOwnersWriteServiceTest {
|
|||||||
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
||||||
|
|
||||||
assertEquals(exception.getMessage(), "This loan cannot be sold, there is already an in progress transfer");
|
assertEquals("This loan cannot be sold, there is already an in progress transfer", exception.getMessage());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
||||||
@@ -607,7 +696,7 @@ public class ExternalAssetOwnersWriteServiceTest {
|
|||||||
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
|
||||||
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
|
||||||
|
|
||||||
assertEquals(exception.getMessage(), "This loan cannot be sold, no effective transfer found.");
|
assertEquals("This loan cannot be sold, no effective transfer found.", exception.getMessage());
|
||||||
// then
|
// then
|
||||||
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
|
||||||
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
|
||||||
@@ -699,7 +788,7 @@ public class ExternalAssetOwnersWriteServiceTest {
|
|||||||
ExternalAssetOwnerTransfer savedTransfer = savedTransferCaptor.getValue();
|
ExternalAssetOwnerTransfer savedTransfer = savedTransferCaptor.getValue();
|
||||||
assertNotNull(savedTransfer);
|
assertNotNull(savedTransfer);
|
||||||
assertFalse(transfers.isEmpty());
|
assertFalse(transfers.isEmpty());
|
||||||
ExternalAssetOwnerTransfer expectedEffectiveTransfer = transfers.get(0);
|
ExternalAssetOwnerTransfer expectedEffectiveTransfer = transfers.getFirst();
|
||||||
assertEquals(testContext.transferExternalId, savedTransfer.getExternalId().getValue());
|
assertEquals(testContext.transferExternalId, savedTransfer.getExternalId().getValue());
|
||||||
assertEquals(expectedEffectiveTransfer.getOwner(), savedTransfer.getOwner());
|
assertEquals(expectedEffectiveTransfer.getOwner(), savedTransfer.getOwner());
|
||||||
assertEquals(expectedStatus, savedTransfer.getStatus());
|
assertEquals(expectedStatus, savedTransfer.getStatus());
|
||||||
@@ -867,6 +956,9 @@ public class ExternalAssetOwnersWriteServiceTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private ExternalAssetOwnersReadService externalAssetOwnersReadService;
|
private ExternalAssetOwnersReadService externalAssetOwnersReadService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ExternalAssetOwnerHelper externalAssetOwnerHelper;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private ExternalAssetOwnersWriteServiceImpl externalAssetOwnersWriteServiceImpl;
|
private ExternalAssetOwnersWriteServiceImpl externalAssetOwnersWriteServiceImpl;
|
||||||
|
|
||||||
@@ -938,7 +1030,6 @@ public class ExternalAssetOwnersWriteServiceTest {
|
|||||||
lenient().when(configurationDomainService.getAllowedLoanStatusesOfDelayedSettlementForExternalAssetTransfer())
|
lenient().when(configurationDomainService.getAllowedLoanStatusesOfDelayedSettlementForExternalAssetTransfer())
|
||||||
.thenReturn(List.of("ACTIVE", "TRANSFER_IN_PROGRESS", "TRANSFER_ON_HOLD", "OVERPAID", "CLOSED_OBLIGATIONS_MET"));
|
.thenReturn(List.of("ACTIVE", "TRANSFER_IN_PROGRESS", "TRANSFER_ON_HOLD", "OVERPAID", "CLOSED_OBLIGATIONS_MET"));
|
||||||
lenient().when(externalAssetOwnersReadService.retrieveActiveTransferData(any(Long.class), any(), any())).thenReturn(null);
|
lenient().when(externalAssetOwnersReadService.retrieveActiveTransferData(any(Long.class), any(), any())).thenReturn(null);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+77
@@ -46,12 +46,17 @@ import java.time.LocalDate;
|
|||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.DateTimeFormatterBuilder;
|
import java.time.format.DateTimeFormatterBuilder;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
import org.apache.fineract.accounting.common.AccountingConstants;
|
import org.apache.fineract.accounting.common.AccountingConstants;
|
||||||
@@ -1361,6 +1366,78 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void saleTransferWithSameOwnerExternalIdInParallelShouldNotFail() {
|
||||||
|
try {
|
||||||
|
globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_AUTO_GENERATED_EXTERNAL_ID, true);
|
||||||
|
setInitialBusinessDate("2020-03-02");
|
||||||
|
|
||||||
|
final int threadCount = 10;
|
||||||
|
final String sharedOwnerExternalId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
final List<Integer> loanIDs = new ArrayList<>();
|
||||||
|
for (int i = 0; i < threadCount; i++) {
|
||||||
|
Integer clientID = createClient();
|
||||||
|
Integer loanID = createLoanForClient(clientID);
|
||||||
|
loanIDs.add(loanID);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
|
||||||
|
final CountDownLatch startLatch = new CountDownLatch(1);
|
||||||
|
final CountDownLatch doneLatch = new CountDownLatch(threadCount);
|
||||||
|
final List<PostInitiateTransferResponse> results = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
final List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
for (int i = 0; i < threadCount; i++) {
|
||||||
|
final Integer loanID = loanIDs.get(i);
|
||||||
|
executorService.execute(() -> {
|
||||||
|
try {
|
||||||
|
startLatch.await();
|
||||||
|
PostInitiateTransferResponse response = EXTERNAL_ASSET_OWNER_HELPER.initiateTransferByLoanId(loanID.longValue(),
|
||||||
|
"sale",
|
||||||
|
new ExternalAssetOwnerRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en")
|
||||||
|
.transferExternalId(UUID.randomUUID().toString())
|
||||||
|
.transferExternalGroupId(UUID.randomUUID().toString()).ownerExternalId(sharedOwnerExternalId)
|
||||||
|
.purchasePriceRatio("1.0"));
|
||||||
|
results.add(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
exceptions.add(e);
|
||||||
|
} finally {
|
||||||
|
doneLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startLatch.countDown();
|
||||||
|
assertTrue(doneLatch.await(30, TimeUnit.SECONDS), "All threads should complete within timeout");
|
||||||
|
executorService.shutdown();
|
||||||
|
assertTrue(executorService.awaitTermination(10, TimeUnit.SECONDS), "ExecutorService should terminate");
|
||||||
|
|
||||||
|
assertTrue(exceptions.isEmpty(),
|
||||||
|
"Expected no exceptions but got " + exceptions.size() + ": " + exceptions.stream().map(Throwable::getMessage).toList());
|
||||||
|
assertEquals(threadCount, results.size(), "All transfers should succeed");
|
||||||
|
results.forEach(response -> {
|
||||||
|
assertNotNull(response.getResourceId());
|
||||||
|
assertNotNull(response.getResourceExternalId());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify all transfers reference the same owner
|
||||||
|
for (Integer loanID : loanIDs) {
|
||||||
|
PageExternalTransferData transfers = EXTERNAL_ASSET_OWNER_HELPER.retrieveTransfersByLoanId(loanID.longValue());
|
||||||
|
assertEquals(1, transfers.getTotalElements());
|
||||||
|
assertNotNull(transfers.getContent());
|
||||||
|
assertNotNull(transfers.getContent().getFirst().getOwner());
|
||||||
|
assertEquals(sharedOwnerExternalId, transfers.getContent().getFirst().getOwner().getExternalId(),
|
||||||
|
"All transfers should reference the same owner");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
cleanUpAndRestoreBusinessDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateBusinessDateAndExecuteCOBJob(String date) {
|
private void updateBusinessDateAndExecuteCOBJob(String date) {
|
||||||
BusinessDateHelper.updateBusinessDate(REQUEST_SPEC, RESPONSE_SPEC, BUSINESS_DATE, LocalDate.parse(date));
|
BusinessDateHelper.updateBusinessDate(REQUEST_SPEC, RESPONSE_SPEC, BUSINESS_DATE, LocalDate.parse(date));
|
||||||
SCHEDULER_JOB_HELPER.executeAndAwaitJob("Loan COB");
|
SCHEDULER_JOB_HELPER.executeAndAwaitJob("Loan COB");
|
||||||
|
|||||||
Reference in New Issue
Block a user