2525import com .google .gson .JsonElement ;
2626import com .google .gson .reflect .TypeToken ;
2727import java .lang .reflect .Type ;
28+ import java .sql .SQLException ;
2829import java .time .LocalDate ;
2930import java .util .ArrayList ;
3031import java .util .Arrays ;
6566import org .apache .fineract .portfolio .loanaccount .domain .LoanRepository ;
6667import org .apache .fineract .portfolio .loanaccount .domain .LoanStatus ;
6768import org .apache .fineract .portfolio .loanaccount .exception .LoanNotFoundException ;
69+ import org .springframework .dao .DataAccessException ;
70+ import org .springframework .dao .DataIntegrityViolationException ;
71+ import org .springframework .orm .jpa .JpaSystemException ;
6872import org .springframework .stereotype .Service ;
6973import org .springframework .transaction .annotation .Transactional ;
7074
@@ -77,6 +81,8 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
7781 ExternalTransferStatus .ACTIVE );
7882 private static final List <ExternalTransferStatus > BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT = List
7983 .of (ExternalTransferStatus .ACTIVE_INTERMEDIATE , ExternalTransferStatus .ACTIVE );
84+ private static final String SQL_STATE_INTEGRITY_CONSTRAINT_VIOLATION = "23" ;
85+
8086 private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository ;
8187 private final ExternalAssetOwnerRepository externalAssetOwnerRepository ;
8288 private final FromJsonHelper fromApiJsonHelper ;
@@ -85,6 +91,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
8591 private final ConfigurationDomainService configurationDomainService ;
8692 private final ExternalAssetOwnersReadService externalAssetOwnersReadService ;
8793 private final ExternalAssetOwnerValidator externalAssetOwnerValidator ;
94+ private final ExternalAssetOwnerTransactionalHelper externalAssetOwnerTransactionalHelper ;
8895
8996 @ Override
9097 @ Transactional
@@ -171,15 +178,16 @@ private void validateEffectiveTransferForSale(final List<ExternalAssetOwnerTrans
171178 if (effectiveTransfers .size () == 2 ) {
172179 throw new ExternalAssetOwnerInitiateTransferException ("This loan cannot be sold, there is already an in progress transfer" );
173180 } else if (effectiveTransfers .size () == 1 ) {
174- if (PENDING .equals (effectiveTransfers .get ( 0 ).getStatus ())) {
181+ if (PENDING .equals (effectiveTransfers .getFirst ( ).getStatus ())) {
175182 throw new ExternalAssetOwnerInitiateTransferException (
176183 "External asset owner transfer is already in PENDING state for this loan" );
177- } else if (ExternalTransferStatus .ACTIVE .equals (effectiveTransfers .get ( 0 ).getStatus ())) {
184+ } else if (ExternalTransferStatus .ACTIVE .equals (effectiveTransfers .getFirst ( ).getStatus ())) {
178185 throw new ExternalAssetOwnerInitiateTransferException (
179186 "This loan cannot be sold, because it is owned by an external asset owner" );
180187 } else {
181- throw new ExternalAssetOwnerInitiateTransferException (String .format (
182- "This loan cannot be sold, because it is incorrect state! (transferId = %s)" , effectiveTransfers .get (0 ).getId ()));
188+ throw new ExternalAssetOwnerInitiateTransferException (
189+ String .format ("This loan cannot be sold, because it is incorrect state! (transferId = %s)" ,
190+ effectiveTransfers .getFirst ().getId ()));
183191 }
184192 }
185193 }
@@ -188,7 +196,7 @@ private void validateEffectiveTransferForDelayedSettlementSale(final List<Extern
188196 if (effectiveTransfers .size () > 1 ) {
189197 throw new ExternalAssetOwnerInitiateTransferException ("This loan cannot be sold, there is already an in progress transfer" );
190198 } else if (effectiveTransfers .size () == 1 ) {
191- if (!ACTIVE_INTERMEDIATE .equals (effectiveTransfers .get ( 0 ).getStatus ())) {
199+ if (!ACTIVE_INTERMEDIATE .equals (effectiveTransfers .getFirst ( ).getStatus ())) {
192200 throw new ExternalAssetOwnerInitiateTransferException (
193201 "This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state." );
194202 }
@@ -204,15 +212,16 @@ private void validateEffectiveTransferForIntermediarySale(final ExternalAssetOwn
204212 if (effectiveTransfers .size () > 1 ) {
205213 throw new ExternalAssetOwnerInitiateTransferException ("This loan cannot be sold, there is already an in progress transfer" );
206214 } else if (effectiveTransfers .size () == 1 ) {
207- if (PENDING_INTERMEDIATE .equals (effectiveTransfers .get ( 0 ).getStatus ())) {
215+ if (PENDING_INTERMEDIATE .equals (effectiveTransfers .getFirst ( ).getStatus ())) {
208216 throw new ExternalAssetOwnerInitiateTransferException (
209217 "External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan" );
210- } else if (ExternalTransferStatus .ACTIVE .equals (effectiveTransfers .get ( 0 ).getStatus ())) {
218+ } else if (ExternalTransferStatus .ACTIVE .equals (effectiveTransfers .getFirst ( ).getStatus ())) {
211219 throw new ExternalAssetOwnerInitiateTransferException (
212220 "This loan cannot be sold, because it is owned by an external asset owner" );
213221 } else {
214- throw new ExternalAssetOwnerInitiateTransferException (String .format (
215- "This loan cannot be sold, because it is incorrect state! (transferId = %s)" , effectiveTransfers .get (0 ).getId ()));
222+ throw new ExternalAssetOwnerInitiateTransferException (
223+ String .format ("This loan cannot be sold, because it is incorrect state! (transferId = %s)" ,
224+ effectiveTransfers .getFirst ().getId ()));
216225 }
217226 }
218227 }
@@ -232,17 +241,17 @@ private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuyback(
232241 } else if (effectiveTransfers .size () == 2 ) {
233242 throw new ExternalAssetOwnerInitiateTransferException (
234243 "This loan cannot be bought back, external asset owner buyback transfer is already in progress" );
235- } else if (!BUYBACK_READY_STATUSES .contains (effectiveTransfers .get ( 0 ).getStatus ())) {
244+ } else if (!BUYBACK_READY_STATUSES .contains (effectiveTransfers .getFirst ( ).getStatus ())) {
236245 throw new ExternalAssetOwnerInitiateTransferException (
237246 String .format ("This loan cannot be bought back, effective transfer is not in right state: %s" ,
238- effectiveTransfers .get ( 0 ).getStatus ()));
239- } else if (DateUtils .isBefore (settlementDate , effectiveTransfers .get ( 0 ).getSettlementDate ())) {
247+ effectiveTransfers .getFirst ( ).getStatus ()));
248+ } else if (DateUtils .isBefore (settlementDate , effectiveTransfers .getFirst ( ).getSettlementDate ())) {
240249 throw new ExternalAssetOwnerInitiateTransferException (
241250 String .format ("This loan cannot be bought back, settlement date is earlier than effective transfer settlement date: %s" ,
242- effectiveTransfers .get ( 0 ).getSettlementDate ()));
251+ effectiveTransfers .getFirst ( ).getSettlementDate ()));
243252 }
244253
245- return effectiveTransfers .get ( 0 );
254+ return effectiveTransfers .getFirst ( );
246255 }
247256
248257 private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuybackWithDelayedSettlement (
@@ -265,17 +274,17 @@ private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuybackWi
265274 || Set .of (ExternalTransferStatus .ACTIVE , ExternalTransferStatus .BUYBACK ).equals (effectiveTransferStatuses )) {
266275 throw new ExternalAssetOwnerInitiateTransferException (
267276 "This loan cannot be bought back, external asset owner buyback transfer is already in progress" );
268- } else if (!BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT .contains (effectiveTransfers .get ( 0 ).getStatus ())) {
277+ } else if (!BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT .contains (effectiveTransfers .getFirst ( ).getStatus ())) {
269278 throw new ExternalAssetOwnerInitiateTransferException (
270279 String .format ("This loan cannot be bought back, effective transfer is not in right state: %s" ,
271- effectiveTransfers .get ( 0 ).getStatus ()));
272- } else if (DateUtils .isBefore (settlementDate , effectiveTransfers .get ( 0 ).getSettlementDate ())) {
280+ effectiveTransfers .getFirst ( ).getStatus ()));
281+ } else if (DateUtils .isBefore (settlementDate , effectiveTransfers .getFirst ( ).getSettlementDate ())) {
273282 throw new ExternalAssetOwnerInitiateTransferException (
274283 String .format ("This loan cannot be bought back, settlement date is earlier than effective transfer settlement date: %s" ,
275- effectiveTransfers .get ( 0 ).getSettlementDate ()));
284+ effectiveTransfers .getFirst ( ).getSettlementDate ()));
276285 }
277286
278- return effectiveTransfers .get ( 0 );
287+ return effectiveTransfers .getFirst ( );
279288 }
280289
281290 private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForCancel (final Long transferId ) {
@@ -287,10 +296,9 @@ private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForCancel(fi
287296 .findEffectiveTransfersOrderByIdDesc (selectedTransfer .getLoanId (), DateUtils .getBusinessLocalDate ());
288297 if (effective .isEmpty ()) {
289298 throw new ExternalAssetOwnerInitiateTransferException (
290- String .format ("This loan cannot be cancelled, there is no effective transfer for this loan" ));
291- } else if (!Objects .equals (effective .get (0 ).getId (), selectedTransfer .getId ())) {
292- throw new ExternalAssetOwnerInitiateTransferException (
293- String .format ("This loan cannot be cancelled, selected transfer is not the latest" ));
299+ "This loan cannot be cancelled, there is no effective transfer for this loan" );
300+ } else if (!Objects .equals (effective .getFirst ().getId (), selectedTransfer .getId ())) {
301+ throw new ExternalAssetOwnerInitiateTransferException ("This loan cannot be cancelled, selected transfer is not the latest" );
294302 } else if (selectedTransfer .getStatus () != PENDING && selectedTransfer .getStatus () != ExternalTransferStatus .BUYBACK ) {
295303 throw new ExternalAssetOwnerInitiateTransferException (
296304 "This loan cannot be cancelled, the selected transfer status is not pending or buyback" );
@@ -318,8 +326,7 @@ private ExternalAssetOwnerTransfer createBuybackTransfer(ExternalAssetOwnerTrans
318326
319327 private ExternalTransferStatus determineStatusAfterBuyback (ExternalAssetOwnerTransfer effectiveTransfer ) {
320328 return switch (effectiveTransfer .getStatus ()) {
321- case PENDING -> ExternalTransferStatus .BUYBACK ;
322- case ACTIVE -> ExternalTransferStatus .BUYBACK ;
329+ case PENDING , ACTIVE -> ExternalTransferStatus .BUYBACK ;
323330 case ACTIVE_INTERMEDIATE -> ExternalTransferStatus .BUYBACK_INTERMEDIATE ;
324331 default -> throw new ExternalAssetOwnerInitiateTransferException (String .format (
325332 "This loan cannot be bought back, effective transfer is not in right state: %s" , effectiveTransfer .getStatus ()));
@@ -582,17 +589,33 @@ private String getPurchasePriceRatioFromJson(JsonElement json) {
582589 return fromApiJsonHelper .extractStringNamed (ExternalTransferRequestParameters .PURCHASE_PRICE_RATIO , json );
583590 }
584591
585- private ExternalAssetOwner getOwner (JsonElement json ) {
586- String ownerExternalId = fromApiJsonHelper .extractStringNamed (ExternalTransferRequestParameters .OWNER_EXTERNAL_ID , json );
587- Optional <ExternalAssetOwner > byExternalId = externalAssetOwnerRepository
588- .findByExternalId (ExternalIdFactory .produce (ownerExternalId ));
589- return byExternalId .orElseGet (() -> createAndGetAssetOwner (ownerExternalId ));
592+ private ExternalAssetOwner getOwner (final JsonElement json ) {
593+ final String ownerExternalId = fromApiJsonHelper .extractStringNamed (ExternalTransferRequestParameters .OWNER_EXTERNAL_ID , json );
594+ final ExternalId externalId = ExternalIdFactory .produce (ownerExternalId );
595+ return externalAssetOwnerRepository .findByExternalId (externalId ).orElseGet (() -> {
596+ final Long ownerId = findOrCreateOwnerId (externalId );
597+ // getReferenceById returns a lazy proxy without hitting the DB. findById would fail
598+ // here because the outer transaction's persistence context does not contain the entity
599+ // committed by the inner REQUIRES_NEW transaction.
600+ return externalAssetOwnerRepository .getReferenceById (ownerId );
601+ });
602+ }
603+
604+ private Long findOrCreateOwnerId (final ExternalId externalId ) {
605+ try {
606+ return externalAssetOwnerTransactionalHelper .findOrCreateId (externalId );
607+ } catch (JpaSystemException | DataIntegrityViolationException e ) {
608+ if (!isConstraintViolation (e )) {
609+ throw e ;
610+ }
611+ // Another thread created the owner concurrently - retry
612+ return externalAssetOwnerTransactionalHelper .findOrCreateId (externalId );
613+ }
590614 }
591615
592- private ExternalAssetOwner createAndGetAssetOwner (String externalId ) {
593- ExternalAssetOwner externalAssetOwner = new ExternalAssetOwner ();
594- externalAssetOwner .setExternalId (ExternalIdFactory .produce (externalId ));
595- return externalAssetOwnerRepository .saveAndFlush (externalAssetOwner );
616+ private boolean isConstraintViolation (final DataAccessException e ) {
617+ return e .getMostSpecificCause () instanceof SQLException sqlEx && sqlEx .getSQLState () != null
618+ && sqlEx .getSQLState ().startsWith (SQL_STATE_INTEGRITY_CONSTRAINT_VIOLATION );
596619 }
597620
598621 private List <LoanStatus > getAllowedLoanStatuses () {
0 commit comments