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 ;
6364import org .apache .fineract .portfolio .loanaccount .domain .LoanRepository ;
6465import org .apache .fineract .portfolio .loanaccount .domain .LoanStatus ;
6566import org .apache .fineract .portfolio .loanaccount .exception .LoanNotFoundException ;
67+ import org .springframework .dao .DataAccessException ;
68+ import org .springframework .dao .DataIntegrityViolationException ;
69+ import org .springframework .orm .jpa .JpaSystemException ;
6670import org .springframework .stereotype .Service ;
6771import org .springframework .transaction .annotation .Transactional ;
6872
@@ -82,6 +86,9 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
8286 private final DelayedSettlementAttributeService delayedSettlementAttributeService ;
8387 private final ConfigurationDomainService configurationDomainService ;
8488 private final ExternalAssetOwnersReadService externalAssetOwnersReadService ;
89+ private static final String SQL_STATE_INTEGRITY_CONSTRAINT_VIOLATION = "23" ;
90+
91+ private final ExternalAssetOwnerTransactionalHelper externalAssetOwnerTransactionalHelper ;
8592
8693 @ Override
8794 @ Transactional
@@ -168,15 +175,16 @@ private void validateEffectiveTransferForSale(final List<ExternalAssetOwnerTrans
168175 if (effectiveTransfers .size () == 2 ) {
169176 throw new ExternalAssetOwnerInitiateTransferException ("This loan cannot be sold, there is already an in progress transfer" );
170177 } else if (effectiveTransfers .size () == 1 ) {
171- if (PENDING .equals (effectiveTransfers .get ( 0 ).getStatus ())) {
178+ if (PENDING .equals (effectiveTransfers .getFirst ( ).getStatus ())) {
172179 throw new ExternalAssetOwnerInitiateTransferException (
173180 "External asset owner transfer is already in PENDING state for this loan" );
174- } else if (ExternalTransferStatus .ACTIVE .equals (effectiveTransfers .get ( 0 ).getStatus ())) {
181+ } else if (ExternalTransferStatus .ACTIVE .equals (effectiveTransfers .getFirst ( ).getStatus ())) {
175182 throw new ExternalAssetOwnerInitiateTransferException (
176183 "This loan cannot be sold, because it is owned by an external asset owner" );
177184 } else {
178- throw new ExternalAssetOwnerInitiateTransferException (String .format (
179- "This loan cannot be sold, because it is incorrect state! (transferId = %s)" , effectiveTransfers .get (0 ).getId ()));
185+ throw new ExternalAssetOwnerInitiateTransferException (
186+ String .format ("This loan cannot be sold, because it is incorrect state! (transferId = %s)" ,
187+ effectiveTransfers .getFirst ().getId ()));
180188 }
181189 }
182190 }
@@ -185,7 +193,7 @@ private void validateEffectiveTransferForDelayedSettlementSale(final List<Extern
185193 if (effectiveTransfers .size () > 1 ) {
186194 throw new ExternalAssetOwnerInitiateTransferException ("This loan cannot be sold, there is already an in progress transfer" );
187195 } else if (effectiveTransfers .size () == 1 ) {
188- if (!ACTIVE_INTERMEDIATE .equals (effectiveTransfers .get ( 0 ).getStatus ())) {
196+ if (!ACTIVE_INTERMEDIATE .equals (effectiveTransfers .getFirst ( ).getStatus ())) {
189197 throw new ExternalAssetOwnerInitiateTransferException (
190198 "This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state." );
191199 }
@@ -201,15 +209,16 @@ private void validateEffectiveTransferForIntermediarySale(final ExternalAssetOwn
201209 if (effectiveTransfers .size () > 1 ) {
202210 throw new ExternalAssetOwnerInitiateTransferException ("This loan cannot be sold, there is already an in progress transfer" );
203211 } else if (effectiveTransfers .size () == 1 ) {
204- if (PENDING_INTERMEDIATE .equals (effectiveTransfers .get ( 0 ).getStatus ())) {
212+ if (PENDING_INTERMEDIATE .equals (effectiveTransfers .getFirst ( ).getStatus ())) {
205213 throw new ExternalAssetOwnerInitiateTransferException (
206214 "External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan" );
207- } else if (ExternalTransferStatus .ACTIVE .equals (effectiveTransfers .get ( 0 ).getStatus ())) {
215+ } else if (ExternalTransferStatus .ACTIVE .equals (effectiveTransfers .getFirst ( ).getStatus ())) {
208216 throw new ExternalAssetOwnerInitiateTransferException (
209217 "This loan cannot be sold, because it is owned by an external asset owner" );
210218 } else {
211- throw new ExternalAssetOwnerInitiateTransferException (String .format (
212- "This loan cannot be sold, because it is incorrect state! (transferId = %s)" , effectiveTransfers .get (0 ).getId ()));
219+ throw new ExternalAssetOwnerInitiateTransferException (
220+ String .format ("This loan cannot be sold, because it is incorrect state! (transferId = %s)" ,
221+ effectiveTransfers .getFirst ().getId ()));
213222 }
214223 }
215224 }
@@ -229,17 +238,17 @@ private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuyback(
229238 } else if (effectiveTransfers .size () == 2 ) {
230239 throw new ExternalAssetOwnerInitiateTransferException (
231240 "This loan cannot be bought back, external asset owner buyback transfer is already in progress" );
232- } else if (!BUYBACK_READY_STATUSES .contains (effectiveTransfers .get ( 0 ).getStatus ())) {
241+ } else if (!BUYBACK_READY_STATUSES .contains (effectiveTransfers .getFirst ( ).getStatus ())) {
233242 throw new ExternalAssetOwnerInitiateTransferException (
234243 String .format ("This loan cannot be bought back, effective transfer is not in right state: %s" ,
235- effectiveTransfers .get ( 0 ).getStatus ()));
236- } else if (DateUtils .isBefore (settlementDate , effectiveTransfers .get ( 0 ).getSettlementDate ())) {
244+ effectiveTransfers .getFirst ( ).getStatus ()));
245+ } else if (DateUtils .isBefore (settlementDate , effectiveTransfers .getFirst ( ).getSettlementDate ())) {
237246 throw new ExternalAssetOwnerInitiateTransferException (
238247 String .format ("This loan cannot be bought back, settlement date is earlier than effective transfer settlement date: %s" ,
239- effectiveTransfers .get ( 0 ).getSettlementDate ()));
248+ effectiveTransfers .getFirst ( ).getSettlementDate ()));
240249 }
241250
242- return effectiveTransfers .get ( 0 );
251+ return effectiveTransfers .getFirst ( );
243252 }
244253
245254 private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuybackWithDelayedSettlement (
@@ -262,17 +271,17 @@ private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuybackWi
262271 || Set .of (ExternalTransferStatus .ACTIVE , ExternalTransferStatus .BUYBACK ).equals (effectiveTransferStatuses )) {
263272 throw new ExternalAssetOwnerInitiateTransferException (
264273 "This loan cannot be bought back, external asset owner buyback transfer is already in progress" );
265- } else if (!BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT .contains (effectiveTransfers .get ( 0 ).getStatus ())) {
274+ } else if (!BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT .contains (effectiveTransfers .getFirst ( ).getStatus ())) {
266275 throw new ExternalAssetOwnerInitiateTransferException (
267276 String .format ("This loan cannot be bought back, effective transfer is not in right state: %s" ,
268- effectiveTransfers .get ( 0 ).getStatus ()));
269- } else if (DateUtils .isBefore (settlementDate , effectiveTransfers .get ( 0 ).getSettlementDate ())) {
277+ effectiveTransfers .getFirst ( ).getStatus ()));
278+ } else if (DateUtils .isBefore (settlementDate , effectiveTransfers .getFirst ( ).getSettlementDate ())) {
270279 throw new ExternalAssetOwnerInitiateTransferException (
271280 String .format ("This loan cannot be bought back, settlement date is earlier than effective transfer settlement date: %s" ,
272- effectiveTransfers .get ( 0 ).getSettlementDate ()));
281+ effectiveTransfers .getFirst ( ).getSettlementDate ()));
273282 }
274283
275- return effectiveTransfers .get ( 0 );
284+ return effectiveTransfers .getFirst ( );
276285 }
277286
278287 private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForCancel (final Long transferId ) {
@@ -284,10 +293,9 @@ private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForCancel(fi
284293 .findEffectiveTransfersOrderByIdDesc (selectedTransfer .getLoanId (), DateUtils .getBusinessLocalDate ());
285294 if (effective .isEmpty ()) {
286295 throw new ExternalAssetOwnerInitiateTransferException (
287- String .format ("This loan cannot be cancelled, there is no effective transfer for this loan" ));
288- } else if (!Objects .equals (effective .get (0 ).getId (), selectedTransfer .getId ())) {
289- throw new ExternalAssetOwnerInitiateTransferException (
290- String .format ("This loan cannot be cancelled, selected transfer is not the latest" ));
296+ "This loan cannot be cancelled, there is no effective transfer for this loan" );
297+ } else if (!Objects .equals (effective .getFirst ().getId (), selectedTransfer .getId ())) {
298+ throw new ExternalAssetOwnerInitiateTransferException ("This loan cannot be cancelled, selected transfer is not the latest" );
291299 } else if (selectedTransfer .getStatus () != PENDING && selectedTransfer .getStatus () != ExternalTransferStatus .BUYBACK ) {
292300 throw new ExternalAssetOwnerInitiateTransferException (
293301 "This loan cannot be cancelled, the selected transfer status is not pending or buyback" );
@@ -315,8 +323,7 @@ private ExternalAssetOwnerTransfer createBuybackTransfer(ExternalAssetOwnerTrans
315323
316324 private ExternalTransferStatus determineStatusAfterBuyback (ExternalAssetOwnerTransfer effectiveTransfer ) {
317325 return switch (effectiveTransfer .getStatus ()) {
318- case PENDING -> ExternalTransferStatus .BUYBACK ;
319- case ACTIVE -> ExternalTransferStatus .BUYBACK ;
326+ case PENDING , ACTIVE -> ExternalTransferStatus .BUYBACK ;
320327 case ACTIVE_INTERMEDIATE -> ExternalTransferStatus .BUYBACK_INTERMEDIATE ;
321328 default -> throw new ExternalAssetOwnerInitiateTransferException (String .format (
322329 "This loan cannot be bought back, effective transfer is not in right state: %s" , effectiveTransfer .getStatus ()));
@@ -579,17 +586,30 @@ private String getPurchasePriceRatioFromJson(JsonElement json) {
579586 return fromApiJsonHelper .extractStringNamed (ExternalTransferRequestParameters .PURCHASE_PRICE_RATIO , json );
580587 }
581588
582- private ExternalAssetOwner getOwner (JsonElement json ) {
583- String ownerExternalId = fromApiJsonHelper .extractStringNamed (ExternalTransferRequestParameters .OWNER_EXTERNAL_ID , json );
584- Optional <ExternalAssetOwner > byExternalId = externalAssetOwnerRepository
585- .findByExternalId (ExternalIdFactory .produce (ownerExternalId ));
586- return byExternalId .orElseGet (() -> createAndGetAssetOwner (ownerExternalId ));
589+ private ExternalAssetOwner getOwner (final JsonElement json ) {
590+ final String ownerExternalId = fromApiJsonHelper .extractStringNamed (ExternalTransferRequestParameters .OWNER_EXTERNAL_ID , json );
591+ final ExternalId externalId = ExternalIdFactory .produce (ownerExternalId );
592+ return externalAssetOwnerRepository .findByExternalId (externalId ).orElseGet (() -> {
593+ // Created in a REQUIRES_NEW transaction — use getReferenceById for a managed proxy
594+ return externalAssetOwnerRepository .getReferenceById (findOrCreateOwnerId (externalId ));
595+ });
596+ }
597+
598+ private Long findOrCreateOwnerId (final ExternalId externalId ) {
599+ try {
600+ return externalAssetOwnerTransactionalHelper .findOrCreateId (externalId );
601+ } catch (JpaSystemException | DataIntegrityViolationException e ) {
602+ if (!isConstraintViolation (e )) {
603+ throw e ;
604+ }
605+ // Another thread created the owner concurrently — retry
606+ return externalAssetOwnerTransactionalHelper .findOrCreateId (externalId );
607+ }
587608 }
588609
589- private ExternalAssetOwner createAndGetAssetOwner (String externalId ) {
590- ExternalAssetOwner externalAssetOwner = new ExternalAssetOwner ();
591- externalAssetOwner .setExternalId (ExternalIdFactory .produce (externalId ));
592- return externalAssetOwnerRepository .saveAndFlush (externalAssetOwner );
610+ private boolean isConstraintViolation (final DataAccessException e ) {
611+ return e .getMostSpecificCause () instanceof SQLException sqlEx && sqlEx .getSQLState () != null
612+ && sqlEx .getSQLState ().startsWith (SQL_STATE_INTEGRITY_CONSTRAINT_VIOLATION );
593613 }
594614
595615 private List <LoanStatus > getAllowedLoanStatuses () {
0 commit comments