@@ -18,10 +18,7 @@ import net.gepafin.tendermanagement.entities.ApplicationEvaluationEntity;
import net.gepafin.tendermanagement.entities.CompanyEntity ;
import net.gepafin.tendermanagement.entities.CompanyEntity ;
import net.gepafin.tendermanagement.entities.DocumentEntity ;
import net.gepafin.tendermanagement.entities.DocumentEntity ;
import net.gepafin.tendermanagement.entities.HubEntity ;
import net.gepafin.tendermanagement.entities.HubEntity ;
import net.gepafin.tendermanagement.enums.ApplicationStatusTypeEnum ;
import net.gepafin.tendermanagement.enums.* ;
import net.gepafin.tendermanagement.enums.DocumentSourceTypeEnum ;
import net.gepafin.tendermanagement.enums.NotificationTypeEnum ;
import net.gepafin.tendermanagement.enums.VersionActionTypeEnum ;
import net.gepafin.tendermanagement.model.request.AppointmentCreationRequest ;
import net.gepafin.tendermanagement.model.request.AppointmentCreationRequest ;
import net.gepafin.tendermanagement.model.request.AppointmentNdgRequest ;
import net.gepafin.tendermanagement.model.request.AppointmentNdgRequest ;
import net.gepafin.tendermanagement.model.request.AppointmentVisuraListRequest ;
import net.gepafin.tendermanagement.model.request.AppointmentVisuraListRequest ;
@@ -66,7 +63,10 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap ;
import java.util.concurrent.ConcurrentHashMap ;
import java.util.concurrent.ExecutorService ;
import java.util.concurrent.ExecutorService ;
import java.util.concurrent.Executors ;
import java.util.concurrent.Executors ;
import java.util.concurrent.ScheduledFuture ;
import java.util.concurrent.TimeUnit ;
import java.util.concurrent.TimeUnit ;
import java.util.concurrent.atomic.AtomicReference ;
import java.util.concurrent.ScheduledExecutorService ;
@Slf4j
@Slf4j
@Component
@Component
@@ -150,7 +150,8 @@ public class AppointmentDao {
@Autowired
@Autowired
private ApplicationEvaluationDao applicationEvaluationDao ;
private ApplicationEvaluationDao applicationEvaluationDao ;
private final Map < Long , ExecutorService > executorMap = new ConcurrentHashMap < > ( ) ;
private final Map < Long , Scheduled ExecutorService> executorMap = new ConcurrentHashMap < > ( ) ;
private final ConcurrentHashMap < Long , ExecutorService > threadForDocumentMap = new ConcurrentHashMap < > ( ) ;
private final ConcurrentHashMap < Long , ExecutorService > threadForDocumentMap = new ConcurrentHashMap < > ( ) ;
@@ -434,35 +435,89 @@ public class AppointmentDao {
}
}
private void startAsyncNdgProcessing ( Long applicationId ) {
private void startAsyncNdgProcessing ( Long applicationId ) {
// Check if a thread is already runn ing for this application
// If already poll ing for this applicationId, do nothing:
if ( executorMap . containsKey ( applicationId ) ) {
if ( executorMap . containsKey ( applicationId ) ) {
log . warn ( " Async processing already running for applicationId: {} " , applicationId ) ;
log . warn ( " Async processing already running for applicationId: {} " , applicationId ) ;
return ;
return ;
}
}
// Create a dedicated thread for asynchronous processing
ScheduledExecutorService scheduler = Executors . newSingleThreadScheduledExecutor ( runnable - > {
ExecutorService executor = Executors . newSingleThreadExecutor ( runnable - > {
Thread t = new Thread ( runnable ) ;
Thread thread = new Thread ( runnable ) ;
t . setName ( " AsyncNdgProcessing- " + applicationId ) ;
thread . setName ( " AsyncNdgProcessing- " + applicationId ) ;
return t ;
return thread ;
} ) ;
} ) ;
executorMap . put ( applicationId , executo r) ;
executorMap . put ( applicationId , schedule r) ;
executor . submit ( ( ) - > {
// Record the start time so we can stop after 2 hours:
long startTime = System . currentTimeMillis ( ) ;
long twoHoursMs = TimeUnit . HOURS . toMillis ( 2 ) ;
long fifteenMin = 15 ; // in MINUTES
// We need a reference to cancel the scheduled task from inside itself when we're done:
AtomicReference < ScheduledFuture < ? > > futureRef = new AtomicReference < > ( ) ;
Runnable pollingTask = ( ) - > {
try {
try {
log . info ( " Starting async processing for applicationId: {} " , applicationId ) ;
// 1) If 2 hours have already passed, mark as FAILED and shut down:
processNdgGeneration ( applicationId ) ;
if ( System . currentTimeMillis ( ) - startTime > twoHoursMs ) {
} catch ( Exception e ) {
ApplicationEntity app = applicationService . validateApplication ( applicationId ) ;
log . error ( " Error in async NDG processing for applicationId: {}" , applicationId , e );
log . warn ( " 2-hour timeout reached for applicationId {}. Marking NDG_FAILED. " , applicationId ) ;
} finally {
app . setNdgStatus ( GepafinConstant . NDG_FAILED ) ;
// Cleanup resources
applicationRepository . save ( app ) ;
ExecutorService executorToShutdown = executorMap . remove ( applicationId ) ;
if ( executorToShutdown ! = null ) {
futureRef . get ( ) . cancel ( false ) ;
executorToShutdown . shutdown ( ) ;
shutdownScheduler ( applicationId ) ;
return ;
}
// 2) Otherwise, call processNdgGeneration once:
processNdgGeneration ( applicationId ) ;
// 3) After return, check if NDG is now set or timed out. If so, cancel & shut down:
ApplicationEntity updated = applicationService . validateApplication ( applicationId ) ;
if ( isNdgValid ( updated . getNdg ( ) ) ) {
log . info ( " NDG found for applicationId {}. Shutting down scheduler. " , applicationId ) ;
futureRef . get ( ) . cancel ( false ) ;
shutdownScheduler ( applicationId ) ;
} else if ( updated . getNdgStatus ( ) ! = null & & updated . getNdgStatus ( ) . equals ( GepafinConstant . NDG_FAILED ) ) {
log . info ( " NDG status is NDG_FAILED for applicationId {}. Shutting down scheduler. " , applicationId ) ;
futureRef . get ( ) . cancel ( false ) ;
shutdownScheduler ( applicationId ) ;
}
// Otherwise: no NDG yet, not timed out → next run happens in 15 minutes automatically.
} catch ( Exception ex ) {
log . error ( " Unexpected error during scheduled polling for applicationId {}: {} " , applicationId , ex . getMessage ( ) , ex ) ;
try {
ApplicationEntity checkApp = applicationService . validateApplication ( applicationId ) ;
if ( System . currentTimeMillis ( ) - startTime > twoHoursMs ) {
log . warn ( " After exception, 2-hour window passed for applicationId {}. Marking NDG_FAILED. " , applicationId ) ;
checkApp . setNdgStatus ( GepafinConstant . NDG_FAILED ) ;
applicationRepository . save ( checkApp ) ;
futureRef . get ( ) . cancel ( false ) ;
shutdownScheduler ( applicationId ) ;
}
} catch ( Exception ignore ) {
futureRef . get ( ) . cancel ( false ) ;
shutdownScheduler ( applicationId ) ;
}
}
log . info ( " Async processing completed for applicationId: {} " , applicationId ) ;
}
}
} ) ;
} ;
// Schedule pollingTask: run now (delay=0), then every fifteen minutes:
ScheduledFuture < ? > future = scheduler . scheduleWithFixedDelay ( pollingTask , 0 , // initial delay = 0 min → run immediately
fifteenMin , // subsequent runs every 15 minutes
TimeUnit . MINUTES ) ;
futureRef . set ( future ) ;
}
private void shutdownScheduler ( Long applicationId ) {
ScheduledExecutorService shed = executorMap . remove ( applicationId ) ;
if ( shed ! = null ) {
shed . shutdownNow ( ) ;
}
log . info ( " Scheduler shut down for applicationId: {} " , applicationId ) ;
}
}
private void processNdgGeneration ( Long applicationId ) {
private void processNdgGeneration ( Long applicationId ) {
@@ -491,97 +546,108 @@ public class AppointmentDao {
saveNdgAndIdVisura ( application , company , ndgResponse . getNdg ( ) ) ;
saveNdgAndIdVisura ( application , company , ndgResponse . getNdg ( ) ) ;
log . info ( " NDG successfully generated for applicationId: {} " , applicationId ) ;
log . info ( " NDG successfully generated for applicationId: {} " , applicationId ) ;
} else {
} else {
// If NDG isn't immediately available, start polling
log . info ( " Polling for NDG for applicationId: {} " , applicationId ) ;
handleNdgPolling ( application , company , hub , authorizationToken ) ;
handleNdgPolling ( application , company , hub , authorizationToken ) ;
}
}
} catch ( Exception e ) {
} catch ( Exception e ) {
log . error ( " Exception occurred during NDG generation. ApplicationId: {}, CompanyId: {}, HubId: {}, Error: {} " ,
log . error ( " Exception occurred during NDG generation. ApplicationId: {}, CompanyId: {}, HubId: {}, Error: {} " , applicationId , company . getId ( ) , hub . getId ( ) ,
applicationId , company . getId ( ) , hub . getId ( ) , e. getMessage ( ) , e ) ;
e . getMessage ( ) , e ) ;
}
}
}
}
private void handleNdgPolling ( ApplicationEntity application , CompanyEntity company , HubEntity hub , String authorizationToken ) {
private void handleNdgPolling ( ApplicationEntity application , CompanyEntity company , HubEntity hub , String authorizationToken ) {
log . info ( " Starting single‐ shot NDG polling attempt for applicationId: {}, CompanyId: {}, HubId: {} " , application . getId ( ) , company . getId ( ) , hub . getId ( ) ) ;
long startTime = System . currentTimeMillis ( ) ;
long twoHoursMs = TimeUnit . HOURS . toMillis ( 2 ) ;
try {
try {
log . info ( " Starting NDG polling for applicationId: {}, CompanyId: {}, HubId: {} " , application . getId ( ) , company . getId ( ) , hub . getId ( ) ) ;
// 1) If NDG was already populated (perhaps by another thread), skip polling.
long startTime = System . currentTimeMillis ( ) ;
if ( application . getNdg ( ) ! = null ) {
log . info ( " NDG already present for applicationId {}. Exiting single‐ shot polling. " , application . getId ( ) ) ;
while ( true ) {
return ;
if ( application . getNdg ( ) ! = null ) {
}
log . info ( " NDG retrieved for applicationId: {} " , application . getId ( ) ) ;
break ;
// 2) Attempt to create Visura (this may immediately return a valid NDG)
}
AppointmentLoginResponse visuraResponse = createVisura ( company , authorizationToken , hub ) ;
try {
// 2a) If createVisura gave us a valid NDG, persist & exit
// F etch Ndg via creating visura
String f etched Ndg = visuraResponse . getNdg ( ) ;
AppointmentLoginResponse visuraResponse = createVisura ( company , authorizationToken , hub ) ;
if ( isNdgValid ( fetchedNdg ) ) {
if ( isNdgValid ( visuraResponse . getNdg ( ) ) ) {
log . info ( " Valid NDG retrieved from createVisura(): {} | applicationId: {} " , fetchedNdg , application . getId ( ) ) ;
log . info ( " Valid NDG retrieved from create visura api response: {} | ApplicationId: {} " , visuraResponse . getNdg ( ) , application . getId ( ) ) ;
company . setNdg ( visuraResponse . getNdg ( ) ) ;
company . setNdg ( fetchedNdg ) ;
application . setNdg ( visuraResponse . getNdg ( ) ) ;
application . setNdg ( fetchedNdg ) ;
application . setNdgStatus ( GepafinConstant . NDG_GENERATED ) ;
application . setNdgStatus ( GepafinConstant . NDG_GENERATED ) ;
application . setStatus ( ApplicationStatusTypeEnum . NDG . getValue ( ) ) ;
application . setStatus ( ApplicationStatusTypeEnum . NDG . getValue ( ) ) ;
application . setIdVisura ( visuraResponse . getIdVisura ( ) ) ;
application . setIdVisura ( visuraResponse . getIdVisura ( ) ) ;
applicationRepository . save ( application ) ;
companyRepository . save ( company ) ;
companyRepository . save ( company ) ;
A pplicationEvaluationEntity applicationEvaluationEntity = applicationEvaluationService . validateApplicationEvaluation (
a pplicationRepository . save ( application ) ;
application . getApplicationEvaluationId ( ) ) ;
Map < String , String > placeHolders = new HashMap < > ( ) ;
ApplicationEvaluationEntity eval = applicationEvaluationService . validateApplicationEvaluation ( application . getApplicationEvaluationId ( ) ) ;
placeH olders . put ( " {{call_name}} " , application . getCall ( ) . getName ( ) ) ;
Map < String , String > placeh olders = new HashMap < > ( ) ;
placeH olders . put ( " {{protocol_number}} " , String . valueOf ( application . getProtoco l ( ) . getProtocolNumber ( ) ) ) ;
placeh olders . put ( " {{call_name}} " , application . getCal l ( ) . getName ( ) ) ;
notificationDao . sendNotificationToInstructor ( placeHolders , applicationEvaluationEntity , NotificationTypeEnum . NDG_GENERATION ) ;
placeholders . put ( " {{protocol_number}} " , String . valueOf ( application . getProtocol ( ) . getProtocolNumber ( ) ) ) ;
notificationDao . sendNotificationToSuperUser ( application , placeHolders , NotificationTypeEnum . NDG_GENERATION ) ;
notificationDao . sendNotificationToInstructor ( placeholders , eval , NotificationTypeEnum . NDG_GENERATION ) ;
log . info ( " Got NDG and saved successfully for applicationId: {} " , appl ication. getId ( ) ) ;
notificationDao . sendNotificationToSuperUser ( application , placeholders , Notif icationTypeEnum . NDG_GENERATION ) ;
break ;
} else {
log . info ( " NDG saved successfully for applicationId: {} " , application . getId ( ) ) ;
// Fetch Visura list and attempt to parse NDG
return ;
String visuraListJson = getVisuraList ( visuraResponse . getIdVisura ( ) , authorizationToken , application , hub ) ;
}
log . debug ( " Parsing NDG from visura list response | ApplicationId: {} " , application . getId ( ) ) ;
String ndg = parseNdgFromVisuraListResponse ( v isuraL istJson ) ;
// 2b) If no immediate NDG, fetch the V isura l ist JSON and parse out NDG
if ( isNdgValid ( ndg ) ) {
String visuraListJson = getVisuraList ( v isuraResponse . getIdVisura ( ) , authorizationToken , application , hub ) ;
// CompanyEntity oldCompanyData = Utils.getClonedEntityForData(company) ;
log . debug ( " Parsing NDG from VisuraList JSON for applicationId: {} " , application . getId ( ) ) ;
// ApplicationEntity oldApplicationData = Utils.getClonedEntityForData(application) ;
String parsedNdg = parseNdgFromVisuraListResponse ( visuraListJson ) ;
log . info ( " Valid NDG retrieved: {} | ApplicationId: {} " , ndg , application . getId ( ) ) ;
if ( isNdgValid ( parsedNdg ) ) {
company . s etNdg ( ndg ) ;
log . info ( " Valid NDG parsed from VisuraList: {} | applicationId: {} " , parsedNdg , application . g etId ( ) ) ;
application . setNdg ( ndg ) ;
application . setNdgStatus ( GepafinConstant . NDG_GENERATED ) ;
company . setNdg ( parsedNdg ) ;
application . setStatus ( ApplicationStatusTypeEnum . NDG . getValue ( ) ) ;
application . setNdg ( parsedNdg ) ;
application . setIdVisura ( visuraResponse . getIdVisura ( ) ) ;
application . setNdgStatus ( GepafinConstant . NDG_GENERATED ) ;
applicationRepository . save ( a pplication) ;
application. setStatus ( A pplicationStatusTypeEnum . NDG . getValue ( ) ) ;
companyRepository . save ( company ) ;
application . setIdVisura ( visuraResponse . getIdVisura ( ) ) ;
ApplicationEvaluationEntity applicationEvaluationEntity = applicationEvaluationService . validateApplicationEvaluation (
application . getApplicationEvaluationId ( ) ) ;
companyRepository . save ( company ) ;
M ap< String , String > placeHolders = new HashMap < > ( ) ;
applicationRepository . save ( application ) ;
placeHolders . put ( " {{call_name}} " , application . getCall ( ) . getName ( ) ) ;
placeHolders . put ( " {{protocol_number}} " , String . valueOf ( application . getProtocol ( ) . getProtocolNumber ( ) ) ) ;
ApplicationEvaluationEntity eval = applicationEvaluationService . validateApplicationEvaluation ( application . getApplicationEvaluationId ( ) ) ;
notificationDao . sendNotificationToInstructor ( placeH olders , applicationEvaluationEntity , NotificationTypeEnum . NDG_GENERATION ) ;
Map < String , String > placeh olders = new HashMap < > ( ) ;
notificationDao . sendNotificationToSuperUser ( application , placeHolders , NotificationTypeEnum . NDG_GENERATION ) ;
placeholders . put ( " {{call_name}} " , application . getCall ( ) . getName ( ) ) ;
log . info ( " NDG saved successfully for applicationId: {} " , application . getId ( ) ) ;
placeholders . put ( " {{protocol_number}} " , String . valueOf ( application . getProtocol ( ) . getProtocolNumber ( ) ) ) ;
break ;
notificationDao . sendNotificationToInstructor ( placeholders , eval , NotificationTypeEnum . NDG_GENERATION ) ;
}
notificationDao . sendNotificationToSuperUser ( application , placeholders , NotificationTypeEnum . NDG_GENERATION ) ;
}
// Check if polling has timed out
log . info ( " NDG saved successfully for applicationId: {} " , application . getId ( ) ) ;
if ( System . currentTimeMillis ( ) - startTime > TimeUnit . HOURS . toMillis ( 2 ) ) {
return ;
log . warn ( " NDG polling timed out for applicationId: {} " , application . getId ( ) ) ;
}
application . setNdgStatus ( GepafinConstant . NDG_FAILED ) ;
applicationRepository . save ( application ) ;
// 3) Neither direct API nor parsing yielded a valid NDG. Check timeout.
break ;
if ( System . currentTimeMillis ( ) - startTime > twoHoursMs ) {
}
log . warn ( " NDG polling timed out after 2 hours for applicationId: {} " , application . getId ( ) ) ;
application . setNdgStatus ( GepafinConstant . NDG_FAILED ) ;
// Wait before the next polling attempt
applicationRepository . save ( application ) ;
Thread . sleep ( TimeUnit . MINUTES . toMillis ( 15 ) ) ;
return ;
} catch ( InterruptedException e ) {
}
log . warn ( " NDG polling interrupted for applicationId: {} " , application . getId ( ) ) ;
Thread . currentThread ( ) . interrupt ( ) ;
// 4) No NDG yet—just return. The scheduler will retry in 15 m inu tes.
break ;
log . info ( " No valid NDG yet for applicationId {}. Returning to scheduler—next attempt in 15 minutes. " , application . getId ( ) ) ;
} catch ( Exception e ) {
} catch ( Exception e ) {
log . error ( " Error during NDG polling for applicationId: {} " , application . getId ( ) , e ) ;
log . error ( " Exception during NDG polling for applicationId: {} " , application . getId ( ) , e ) ;
}
// If timeout after exception, mark NDG_FAILED
if ( System . currentTimeMillis ( ) - startTime > twoHoursMs ) {
log . warn ( " Example: exiting single‐ shot polling due to timeout after exception for applicationId: {} " , application . getId ( ) ) ;
application . setNdgStatus ( GepafinConstant . NDG_FAILED ) ;
applicationRepository . save ( application ) ;
} else {
log . info ( " Exception occurred but not timed out for applicationId {}. Returning to scheduler for next attempt in 15 minutes. " , application . getId ( ) ) ;
}
}
} finally {
log . info ( " NDG polling completed for applicationId: {} " , application . getId ( ) ) ;
}
}
log . info ( " NDG polling completed for applicationId: {} " , application . getId ( ) ) ;
}
}
private static String getBearerToken ( HubEntity hub ) {
private static String getBearerToken ( HubEntity hub ) {
@@ -596,12 +662,15 @@ public class AppointmentDao {
private void saveNdgAndIdVisura ( ApplicationEntity application , CompanyEntity company , String ndg ) {
private void saveNdgAndIdVisura ( ApplicationEntity application , CompanyEntity company , String ndg ) {
ApplicationEntity oldApplication = Utils . getClonedEntityForData ( application ) ;
application . setNdg ( ndg ) ;
application . setNdg ( ndg ) ;
application . setNdgStatus ( GepafinConstant . NDG_GENERATED ) ;
application . setNdgStatus ( GepafinConstant . NDG_GENERATED ) ;
application . setStatus ( ApplicationStatusTypeEnum . NDG . getValue ( ) ) ;
application . setStatus ( ApplicationStatusTypeEnum . NDG . getValue ( ) ) ;
company . setNdg ( ndg ) ;
company . setNdg ( ndg ) ;
companyRepository . save ( company ) ;
companyRepository . save ( company ) ;
applicationRepository . save ( application ) ;
applicationRepository . save ( application ) ;
loggingUtil . addVersionHistory (
VersionHistoryRequest . builder ( ) . request ( request ) . actionType ( VersionActionTypeEnum . UPDATE ) . oldData ( oldApplication ) . newData ( application ) . build ( ) ) ;
ApplicationEvaluationEntity applicationEvaluationEntity = applicationEvaluationService . validateApplicationEvaluation ( application . getApplicationEvaluationId ( ) ) ;
ApplicationEvaluationEntity applicationEvaluationEntity = applicationEvaluationService . validateApplicationEvaluation ( application . getApplicationEvaluationId ( ) ) ;
// Map<String, String> placeHolders = notificationDao.sendNotificationToBeneficiary(application, NotificationTypeEnum.NDG_GENERATION);
// Map<String, String> placeHolders = notificationDao.sendNotificationToBeneficiary(application, NotificationTypeEnum.NDG_GENERATION);
Map < String , String > placeHolders = new HashMap < > ( ) ;
Map < String , String > placeHolders = new HashMap < > ( ) ;