Merge pull request #311 from Kitzanos/feature/GEPAFINBE-229-prod

Cherry-pick (Refactor NDG handling)
This commit is contained in:
Rinaldo
2025-06-16 12:22:14 +02:00
committed by GitHub
3 changed files with 229 additions and 116 deletions

View File

@@ -18,7 +18,7 @@ public class AppointmentApiConstant {
public static final boolean CREA_ANAGRAFICA = Boolean.TRUE;
public static final boolean SALVA_DOCUMENTI = Boolean.TRUE;
public static final String VISURA_PROVIDER = "cerved";
public static final String VISURA_TYPE = "StandardReport";
public static final String VISURA_TYPE = "FullReport";
public static final String VISURA_MODE = "visure";
public static final String COD_AGENTE = "UtenzaAPIPortal";
public static final boolean IS_FROM_RATING = Boolean.FALSE;
@@ -38,4 +38,7 @@ public class AppointmentApiConstant {
public static final String COD_OPERAZIONE = "codOperazione";
public static final String MOTIVAZIONE_ID = "id";
public static final String PRODOTTO_CODE = "code";
public static final String WS_ANAGRAFICA_URL= "/WSAnagrafica.getList";
}

View File

@@ -12,12 +12,7 @@ import net.gepafin.tendermanagement.config.Translator;
import net.gepafin.tendermanagement.config.jwt.TokenProvider;
import net.gepafin.tendermanagement.constants.AppointmentApiConstant;
import net.gepafin.tendermanagement.constants.GepafinConstant;
import net.gepafin.tendermanagement.entities.ApplicationAmendmentRequestEntity;
import net.gepafin.tendermanagement.entities.ApplicationEntity;
import net.gepafin.tendermanagement.entities.ApplicationEvaluationEntity;
import net.gepafin.tendermanagement.entities.CompanyEntity;
import net.gepafin.tendermanagement.entities.DocumentEntity;
import net.gepafin.tendermanagement.entities.HubEntity;
import net.gepafin.tendermanagement.entities.*;
import net.gepafin.tendermanagement.enums.*;
import net.gepafin.tendermanagement.model.request.AppointmentCreationRequest;
import net.gepafin.tendermanagement.model.request.AppointmentNdgRequest;
@@ -30,11 +25,7 @@ import net.gepafin.tendermanagement.model.response.AppointmentCreationResponse;
import net.gepafin.tendermanagement.model.response.AppointmentLoginResponse;
import net.gepafin.tendermanagement.model.response.DocumentUploadResponse;
import net.gepafin.tendermanagement.model.response.NdgResponse;
import net.gepafin.tendermanagement.repositories.ApplicationRepository;
import net.gepafin.tendermanagement.repositories.CompanyRepository;
import net.gepafin.tendermanagement.repositories.DocumentRepository;
import net.gepafin.tendermanagement.repositories.HubRepository;
import net.gepafin.tendermanagement.repositories.UserRepository;
import net.gepafin.tendermanagement.repositories.*;
import net.gepafin.tendermanagement.service.AmazonS3Service;
import net.gepafin.tendermanagement.service.ApplicationService;
import net.gepafin.tendermanagement.service.CompanyService;
@@ -62,13 +53,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ScheduledExecutorService;
@Slf4j
@Component
@@ -177,7 +163,7 @@ public class AppointmentDao {
// Update application status
log.info("Updating NDG status to IN_PROGRESS. applicationId: {}", applicationId);
application.setNdgStatus(GepafinConstant.NDG_IN_PROGRESS);
application.setNdgStatus(NdgStatusEnum.NDG_INITITATED.getValue());
applicationRepository.save(application);
loggingUtil.addVersionHistory(
@@ -401,6 +387,7 @@ public class AppointmentDao {
}
throw new RuntimeException("Max retries exceeded. Failed to login to Odessa.");
}
private void CheckPasswordExpiredOrErrorInResponse(ApplicationEntity application, FeignException.Forbidden forbiddenException) {
String responseBody = forbiddenException.contentUTF8();
@@ -436,89 +423,89 @@ public class AppointmentDao {
}
}
} catch (IOException e) {
log.error("Unexpected exception during Odessa login.Error: {}",e.getMessage(), e);
log.error("Unexpected exception during Odessa login.Error: {}", e.getMessage(), e);
}
}
private void startAsyncNdgProcessing(Long applicationId) {
// If already polling for this applicationId, do nothing:
if (executorMap.containsKey(applicationId)) {
log.warn("Async processing already running for applicationId: {}", applicationId);
return;
}
ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread t = new Thread(runnable);
t.setName("AsyncNdgProcessing-" + applicationId);
return t;
});
executorMap.put(applicationId, scheduler);
// 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 = () -> {
RequestContextHolder.setRequestAttributes(requestAttributes, true);
Utils.setHttpServletRequestForThread(request,HttpMethodEnum.POST.getValue(),GepafinConstant.CREATE_NDG, (Long) request.getAttribute(GepafinConstant.USER_ACTION_ID));
try {
// 1) If 2 hours have already passed, mark as FAILED and shut down:
if (System.currentTimeMillis() - startTime > twoHoursMs) {
ApplicationEntity app = applicationService.validateApplication(applicationId);
log.warn("2-hour timeout reached for applicationId {}. Marking NDG_FAILED.", applicationId);
app.setNdgStatus(GepafinConstant.NDG_FAILED);
applicationRepository.save(app);
futureRef.get().cancel(false);
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);
}
}
};
// 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 startAsyncNdgProcessing(Long applicationId) {
// // If already polling for this applicationId, do nothing:
// if (executorMap.containsKey(applicationId)) {
// log.warn("Async processing already running for applicationId: {}", applicationId);
// return;
// }
// ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
//
// ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
// Thread t = new Thread(runnable);
// t.setName("AsyncNdgProcessing-" + applicationId);
// return t;
// });
// executorMap.put(applicationId, scheduler);
//
// // 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 = () -> {
// RequestContextHolder.setRequestAttributes(requestAttributes, true);
// Utils.setHttpServletRequestForThread(request,HttpMethodEnum.POST.getValue(),GepafinConstant.CREATE_NDG, (Long) request.getAttribute(GepafinConstant.USER_ACTION_ID));
// try {
// // 1) If 2 hours have already passed, mark as FAILED and shut down:
// if (System.currentTimeMillis() - startTime > twoHoursMs) {
// ApplicationEntity app = applicationService.validateApplication(applicationId);
// log.warn("2-hour timeout reached for applicationId {}. Marking NDG_FAILED.", applicationId);
// app.setNdgStatus(GepafinConstant.NDG_FAILED);
// applicationRepository.save(app);
//
// futureRef.get().cancel(false);
// 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);
// }
// }
// };
//
// // 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) {
@@ -529,12 +516,10 @@ public class AppointmentDao {
log.info("Scheduler shut down for applicationId: {}", applicationId);
}
private void processNdgGeneration(Long applicationId) {
private void processNdgGeneration(ApplicationEntity application, CompanyEntity company, HubEntity hub) {
// Validate application, company, and hub
Long applicationId = application.getId();
log.info("Starting NDG generation process for applicationId: {}", applicationId);
ApplicationEntity application = applicationService.validateApplication(applicationId);
CompanyEntity company = companyService.validateCompany(application.getCompanyId());
HubEntity hub = hubRepository.findByHubId(application.getHubId());
if (!hub.getUniqueUuid().equals(defaultHubUuid)) {
log.info("Ndg cannot be created for another Hub, it is default for Gepafin.");
@@ -552,7 +537,7 @@ public class AppointmentDao {
// Try retrieving NDG by VAT number
AppointmentLoginResponse ndgResponse = retrieveNdgByVatNumber(company.getVatNumber(), authorizationToken, hub, application);
if (isNdgValid(ndgResponse.getNdg())) {
saveNdgAndIdVisura(application, company, ndgResponse.getNdg());
saveNdg(application, company, ndgResponse.getNdg());
log.info("NDG successfully generated for applicationId: {}", applicationId);
} else {
log.info("Polling for NDG for applicationId: {}", applicationId);
@@ -568,7 +553,7 @@ public class AppointmentDao {
log.info("Starting singleshot NDG polling attempt for applicationId: {}, CompanyId: {}, HubId: {}", application.getId(), company.getId(), hub.getId());
ApplicationEntity oldApplication = Utils.getClonedEntityForData(application);
CompanyEntity oldCompanyEntity=Utils.getClonedEntityForData(company);
CompanyEntity oldCompanyEntity = Utils.getClonedEntityForData(company);
long startTime = System.currentTimeMillis();
long twoHoursMs = TimeUnit.HOURS.toMillis(2);
@@ -589,7 +574,7 @@ public class AppointmentDao {
company.setNdg(fetchedNdg);
application.setNdg(fetchedNdg);
application.setNdgStatus(GepafinConstant.NDG_GENERATED);
application.setNdgStatus(NdgStatusEnum.NDG_GENERATED.getValue());
application.setStatus(ApplicationStatusTypeEnum.NDG.getValue());
application.setIdVisura(visuraResponse.getIdVisura());
@@ -678,10 +663,10 @@ public class AppointmentDao {
return ndg != null && !ndg.isEmpty();
}
private void saveNdgAndIdVisura(ApplicationEntity application, CompanyEntity company, String ndg) {
private void saveNdg(ApplicationEntity application, CompanyEntity company, String ndg) {
ApplicationEntity oldApplication = Utils.getClonedEntityForData(application);
CompanyEntity oldCompanyEntity=Utils.getClonedEntityForData(company);
CompanyEntity oldCompanyEntity = Utils.getClonedEntityForData(company);
application.setNdg(ndg);
application.setNdgStatus(GepafinConstant.NDG_GENERATED);
application.setStatus(ApplicationStatusTypeEnum.NDG.getValue());
@@ -738,13 +723,13 @@ public class AppointmentDao {
// Parse and return the NDG response
return parseNdgResponse(responseJson);
} catch (FeignException.Forbidden forbiddenException) {
log.error("403 Forbidden during NDG retrieval | ApplicationId: {}, HubId: {}", application.getId(), hub.getId());
log.error("403 Forbidden during NDG retrieval | ApplicationId: {}, HubId: {}", application.getId(), hub.getId());
logForbiddenError();
// Regenerate the token and retry
String newAuthorizationToken = regenerateTokenAndSave(hub, application);
return retrieveNdgByVatNumber(vatNumber, newAuthorizationToken, hub, application);
} catch (Exception e) {
log.error("Error during NDG retrieval | ApplicationId: {}, HubId: {}, Message: {}", application.getId(), hub.getId(), e.getMessage(), e);
log.error("Error during NDG retrieval | ApplicationId: {}, HubId: {}, Message: {}", application.getId(), hub.getId(), e.getMessage(), e);
throw new RuntimeException("NDG retrieval failed.", e);
}
}
@@ -993,7 +978,7 @@ public class AppointmentDao {
return appointmentCreationResponse;
} catch (FeignException.Forbidden forbiddenException) {
log.error("403 Forbidden received while retrieving template. Attempting to regenerate token and retry. Application ID: {}", applicationId);
log.error("403 Forbidden received while retrieving template. Attempting to regenerate token and retry. Application ID: {}", applicationId);
regenerateTokenAndSave(hub, application);
return createAppointment(applicationId, createAppointmentRequest);
}
@@ -1094,7 +1079,7 @@ public class AppointmentDao {
}
public AppointmentCreationRequest buildAppointmentCreationRequest(Long applicationId, CreateAppointmentRequest createAppointmentRequest, Long areaCode,
AppointmentCreationRequest templateRichiestaData) {
AppointmentCreationRequest templateRichiestaData) {
ApplicationEntity application = applicationService.validateApplication(applicationId);
CreateAppointmentRequest.Nota nota = createAppointmentRequest.getNota();
@@ -1207,7 +1192,7 @@ public class AppointmentDao {
}
private void uploadDocumentToExternalSystemSync(Long documentId, UploadDocToExternalSystemRequest docToExternalSystemRequest, ApplicationEntity application) {
log.info("Starting sync document upload for documentId: {}", documentId);
log.info("Starting sync document upload for documentId: {}", documentId);
// Synchronous upload logic
DocumentEntity systemDoc = documentDao.validateDocument(documentId);
@@ -1248,7 +1233,7 @@ public class AppointmentDao {
log.info("Document uploaded successfully to external system: {}", parsedResponse);
} catch (FeignException.Forbidden forbiddenException) {
log.error("403 Forbidden from external system during upload for documentId: {}. Retrying with new token...", documentId);
log.error("403 Forbidden from external system during upload for documentId: {}. Retrying with new token...", documentId);
regenerateTokenAndSave(hub, application);
uploadDocumentToExternalSystemSync(documentId, docToExternalSystemRequest, application);
} catch (Exception e) {
@@ -1328,4 +1313,107 @@ public class AppointmentDao {
throw new RuntimeException("Failed to parse response: " + e.getMessage(), e);
}
}
private void startAsyncNdgProcessing(Long applicationId) {
if (executorMap.containsKey(applicationId)) {
log.warn("Async processing already running for applicationId: {}", applicationId);
return;
}
ApplicationEntity application = applicationService.validateApplication(applicationId);
CompanyEntity company = companyService.validateCompany(application.getCompanyId());
ApplicationEntity oldApplication = Utils.getClonedEntityForData(application);
CompanyEntity oldCompanyEntity = Utils.getClonedEntityForData(company);
ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r);
t.setName("AsyncNdgPolling-" + applicationId);
return t;
});
executorMap.put(applicationId, scheduler);
application.setNdgStatus(NdgStatusEnum.NDG_IN_PROGRESS.getValue());
applicationRepository.save(application);
loggingUtil.addVersionHistory(
VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldApplication).newData(application).build());
long startTime = System.currentTimeMillis();
long twoHoursMs = TimeUnit.HOURS.toMillis(2);
long fifteenMin = 15;
HubEntity hub = hubRepository.findByHubId(application.getHubId());
// 1. Run full NDG generation logic once upfront
processNdgGeneration(application, company, hub);
AtomicReference<ScheduledFuture<?>> futureRef = new AtomicReference<>();
// 2. Now define the polling logic ONLY
Runnable pollingTask = () -> {
RequestContextHolder.setRequestAttributes(requestAttributes, true);
Utils.setHttpServletRequestForThread(request, HttpMethodEnum.POST.getValue(),
GepafinConstant.CREATE_NDG, (Long) request.getAttribute(GepafinConstant.USER_ACTION_ID));
try {
// Stop if timeout
if (System.currentTimeMillis() - startTime > twoHoursMs) {
log.warn("Polling timed out for applicationId {}. Marking NDG_FAILED.", applicationId);
application.setNdgStatus(GepafinConstant.NDG_FAILED);
applicationRepository.save(application);
loggingUtil.addVersionHistory(
VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldApplication).newData(application).build());
futureRef.get().cancel(false);
shutdownScheduler(applicationId);
return;
}
// If NDG already present or marked failed
if (isNdgValid(application.getNdg()) || GepafinConstant.NDG_FAILED.equals(application.getNdgStatus())) {
log.info("NDG already present or failed for applicationId {}. Stopping polling.", applicationId);
futureRef.get().cancel(false);
shutdownScheduler(applicationId);
return;
}
// Only Visura polling here:
String visuraListJson = getVisuraList(application.getIdVisura(), hub.getAppointmentAuthTokenId(), application, hub);
String parsedNdg = parseNdgFromVisuraListResponse(visuraListJson);
if (isNdgValid(parsedNdg)) {
log.info("Valid NDG parsed from VisuraList: {} | applicationId: {}", parsedNdg, application.getId());
company.setNdg(parsedNdg);
application.setNdg(parsedNdg);
application.setNdgStatus(GepafinConstant.NDG_GENERATED);
application.setStatus(ApplicationStatusTypeEnum.NDG.getValue());
// application.setIdVisura(visuraResponse.getIdVisura());
companyRepository.save(company);
applicationRepository.save(application);
loggingUtil.addVersionHistory(
VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldApplication).newData(application).build());
loggingUtil.addVersionHistory(
VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldCompanyEntity).newData(company).build());
ApplicationEvaluationEntity eval = applicationEvaluationService.validateApplicationEvaluation(application.getApplicationEvaluationId());
Map<String, String> placeholders = new HashMap<>();
placeholders.put("{{call_name}}", application.getCall().getName());
placeholders.put("{{protocol_number}}", String.valueOf(application.getProtocol().getProtocolNumber()));
notificationDao.sendNotificationToInstructor(placeholders, eval, NotificationTypeEnum.NDG_GENERATION);
notificationDao.sendNotificationToSuperUser(application, placeholders, NotificationTypeEnum.NDG_GENERATION);
log.info("NDG saved successfully for applicationId: {}", application.getId());
return;
}
} catch (Exception ex) {
log.error("Error during NDG polling: {}", ex.getMessage(), ex);
}
};
ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(pollingTask, fifteenMin, fifteenMin, TimeUnit.MINUTES);
futureRef.set(future);
}
}

View File

@@ -0,0 +1,22 @@
package net.gepafin.tendermanagement.enums;
import com.fasterxml.jackson.annotation.JsonValue;
public enum NdgStatusEnum {
NDG_INITITATED("NDG_INITITATED"),
NDG_GENERATED("NDG_GENERATED"),
NDG_IN_PROGRESS("NDG_IN_PROGRESS");
private String value;
NdgStatusEnum(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
}