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 CREA_ANAGRAFICA = Boolean.TRUE;
public static final boolean SALVA_DOCUMENTI = Boolean.TRUE; public static final boolean SALVA_DOCUMENTI = Boolean.TRUE;
public static final String VISURA_PROVIDER = "cerved"; 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 VISURA_MODE = "visure";
public static final String COD_AGENTE = "UtenzaAPIPortal"; public static final String COD_AGENTE = "UtenzaAPIPortal";
public static final boolean IS_FROM_RATING = Boolean.FALSE; 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 COD_OPERAZIONE = "codOperazione";
public static final String MOTIVAZIONE_ID = "id"; public static final String MOTIVAZIONE_ID = "id";
public static final String PRODOTTO_CODE = "code"; 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.config.jwt.TokenProvider;
import net.gepafin.tendermanagement.constants.AppointmentApiConstant; import net.gepafin.tendermanagement.constants.AppointmentApiConstant;
import net.gepafin.tendermanagement.constants.GepafinConstant; import net.gepafin.tendermanagement.constants.GepafinConstant;
import net.gepafin.tendermanagement.entities.ApplicationAmendmentRequestEntity; import net.gepafin.tendermanagement.entities.*;
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.enums.*; import net.gepafin.tendermanagement.enums.*;
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;
@@ -30,11 +25,7 @@ import net.gepafin.tendermanagement.model.response.AppointmentCreationResponse;
import net.gepafin.tendermanagement.model.response.AppointmentLoginResponse; import net.gepafin.tendermanagement.model.response.AppointmentLoginResponse;
import net.gepafin.tendermanagement.model.response.DocumentUploadResponse; import net.gepafin.tendermanagement.model.response.DocumentUploadResponse;
import net.gepafin.tendermanagement.model.response.NdgResponse; import net.gepafin.tendermanagement.model.response.NdgResponse;
import net.gepafin.tendermanagement.repositories.ApplicationRepository; import net.gepafin.tendermanagement.repositories.*;
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.service.AmazonS3Service; import net.gepafin.tendermanagement.service.AmazonS3Service;
import net.gepafin.tendermanagement.service.ApplicationService; import net.gepafin.tendermanagement.service.ApplicationService;
import net.gepafin.tendermanagement.service.CompanyService; import net.gepafin.tendermanagement.service.CompanyService;
@@ -62,13 +53,8 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ScheduledExecutorService;
@Slf4j @Slf4j
@Component @Component
@@ -177,7 +163,7 @@ public class AppointmentDao {
// Update application status // Update application status
log.info("Updating NDG status to IN_PROGRESS. applicationId: {}", applicationId); 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); applicationRepository.save(application);
loggingUtil.addVersionHistory( loggingUtil.addVersionHistory(
@@ -401,6 +387,7 @@ public class AppointmentDao {
} }
throw new RuntimeException("Max retries exceeded. Failed to login to Odessa."); throw new RuntimeException("Max retries exceeded. Failed to login to Odessa.");
} }
private void CheckPasswordExpiredOrErrorInResponse(ApplicationEntity application, FeignException.Forbidden forbiddenException) { private void CheckPasswordExpiredOrErrorInResponse(ApplicationEntity application, FeignException.Forbidden forbiddenException) {
String responseBody = forbiddenException.contentUTF8(); String responseBody = forbiddenException.contentUTF8();
@@ -440,85 +427,85 @@ public class AppointmentDao {
} }
} }
private void startAsyncNdgProcessing(Long applicationId) { // private void startAsyncNdgProcessing(Long applicationId) {
// If already polling for this applicationId, do nothing: // // If already polling 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;
} // }
ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); // ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
//
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> { // ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread t = new Thread(runnable); // Thread t = new Thread(runnable);
t.setName("AsyncNdgProcessing-" + applicationId); // t.setName("AsyncNdgProcessing-" + applicationId);
return t; // return t;
}); // });
executorMap.put(applicationId, scheduler); // executorMap.put(applicationId, scheduler);
//
// Record the start time so we can stop after 2 hours: // // Record the start time so we can stop after 2 hours:
long startTime = System.currentTimeMillis(); // long startTime = System.currentTimeMillis();
long twoHoursMs = TimeUnit.HOURS.toMillis(2); // long twoHoursMs = TimeUnit.HOURS.toMillis(2);
long fifteenMin = 15; // in MINUTES // long fifteenMin = 15; // in MINUTES
//
// We need a reference to cancel the scheduled task from inside itself when we're done: // // We need a reference to cancel the scheduled task from inside itself when we're done:
AtomicReference<ScheduledFuture<?>> futureRef = new AtomicReference<>(); // AtomicReference<ScheduledFuture<?>> futureRef = new AtomicReference<>();
//
Runnable pollingTask = () -> { // Runnable pollingTask = () -> {
RequestContextHolder.setRequestAttributes(requestAttributes, true); // RequestContextHolder.setRequestAttributes(requestAttributes, true);
Utils.setHttpServletRequestForThread(request,HttpMethodEnum.POST.getValue(),GepafinConstant.CREATE_NDG, (Long) request.getAttribute(GepafinConstant.USER_ACTION_ID)); // Utils.setHttpServletRequestForThread(request,HttpMethodEnum.POST.getValue(),GepafinConstant.CREATE_NDG, (Long) request.getAttribute(GepafinConstant.USER_ACTION_ID));
try { // try {
// 1) If 2 hours have already passed, mark as FAILED and shut down: // // 1) If 2 hours have already passed, mark as FAILED and shut down:
if (System.currentTimeMillis() - startTime > twoHoursMs) { // if (System.currentTimeMillis() - startTime > twoHoursMs) {
ApplicationEntity app = applicationService.validateApplication(applicationId); // ApplicationEntity app = applicationService.validateApplication(applicationId);
log.warn("2-hour timeout reached for applicationId {}. Marking NDG_FAILED.", applicationId); // log.warn("2-hour timeout reached for applicationId {}. Marking NDG_FAILED.", applicationId);
app.setNdgStatus(GepafinConstant.NDG_FAILED); // app.setNdgStatus(GepafinConstant.NDG_FAILED);
applicationRepository.save(app); // applicationRepository.save(app);
//
futureRef.get().cancel(false); // futureRef.get().cancel(false);
shutdownScheduler(applicationId); // shutdownScheduler(applicationId);
return; // return;
} // }
//
// 2) Otherwise, call processNdgGeneration once: // // 2) Otherwise, call processNdgGeneration once:
processNdgGeneration(applicationId); // processNdgGeneration(applicationId);
//
// 3) After return, check if NDG is now set or timed out. If so, cancel & shut down: // // 3) After return, check if NDG is now set or timed out. If so, cancel & shut down:
ApplicationEntity updated = applicationService.validateApplication(applicationId); // ApplicationEntity updated = applicationService.validateApplication(applicationId);
if (isNdgValid(updated.getNdg())) { // if (isNdgValid(updated.getNdg())) {
log.info("NDG found for applicationId {}. Shutting down scheduler.", applicationId); // log.info("NDG found for applicationId {}. Shutting down scheduler.", applicationId);
futureRef.get().cancel(false); // futureRef.get().cancel(false);
shutdownScheduler(applicationId); // shutdownScheduler(applicationId);
} else if (updated.getNdgStatus() != null && updated.getNdgStatus().equals(GepafinConstant.NDG_FAILED)) { // } else if (updated.getNdgStatus() != null && updated.getNdgStatus().equals(GepafinConstant.NDG_FAILED)) {
log.info("NDG status is NDG_FAILED for applicationId {}. Shutting down scheduler.", applicationId); // log.info("NDG status is NDG_FAILED for applicationId {}. Shutting down scheduler.", applicationId);
futureRef.get().cancel(false); // futureRef.get().cancel(false);
shutdownScheduler(applicationId); // shutdownScheduler(applicationId);
} // }
// Otherwise: no NDG yet, not timed out → next run happens in 15 minutes automatically. // // Otherwise: no NDG yet, not timed out → next run happens in 15 minutes automatically.
} catch (Exception ex) { // } catch (Exception ex) {
log.error("Unexpected error during scheduled polling for applicationId {}: {}", applicationId, ex.getMessage(), ex); // log.error("Unexpected error during scheduled polling for applicationId {}: {}", applicationId, ex.getMessage(), ex);
try { // try {
ApplicationEntity checkApp = applicationService.validateApplication(applicationId); // ApplicationEntity checkApp = applicationService.validateApplication(applicationId);
if (System.currentTimeMillis() - startTime > twoHoursMs) { // if (System.currentTimeMillis() - startTime > twoHoursMs) {
log.warn("After exception, 2-hour window passed for applicationId {}. Marking NDG_FAILED.", applicationId); // log.warn("After exception, 2-hour window passed for applicationId {}. Marking NDG_FAILED.", applicationId);
checkApp.setNdgStatus(GepafinConstant.NDG_FAILED); // checkApp.setNdgStatus(GepafinConstant.NDG_FAILED);
applicationRepository.save(checkApp); // applicationRepository.save(checkApp);
//
futureRef.get().cancel(false); // futureRef.get().cancel(false);
shutdownScheduler(applicationId); // shutdownScheduler(applicationId);
} // }
} catch (Exception ignore) { // } catch (Exception ignore) {
futureRef.get().cancel(false); // futureRef.get().cancel(false);
shutdownScheduler(applicationId); // shutdownScheduler(applicationId);
} // }
} // }
}; // };
//
// Schedule pollingTask: run now (delay=0), then every fifteen minutes: // // Schedule pollingTask: run now (delay=0), then every fifteen minutes:
ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(pollingTask, 0, // initial delay = 0 min → run immediately // ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(pollingTask, 0, // initial delay = 0 min → run immediately
fifteenMin, // subsequent runs every 15 minutes // fifteenMin, // subsequent runs every 15 minutes
TimeUnit.MINUTES); // TimeUnit.MINUTES);
futureRef.set(future); // futureRef.set(future);
} // }
private void shutdownScheduler(Long applicationId) { private void shutdownScheduler(Long applicationId) {
@@ -529,12 +516,10 @@ public class AppointmentDao {
log.info("Scheduler shut down for applicationId: {}", applicationId); 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 // Validate application, company, and hub
Long applicationId = application.getId();
log.info("Starting NDG generation process for applicationId: {}", applicationId); 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)) { if (!hub.getUniqueUuid().equals(defaultHubUuid)) {
log.info("Ndg cannot be created for another Hub, it is default for Gepafin."); 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 // Try retrieving NDG by VAT number
AppointmentLoginResponse ndgResponse = retrieveNdgByVatNumber(company.getVatNumber(), authorizationToken, hub, application); AppointmentLoginResponse ndgResponse = retrieveNdgByVatNumber(company.getVatNumber(), authorizationToken, hub, application);
if (isNdgValid(ndgResponse.getNdg())) { if (isNdgValid(ndgResponse.getNdg())) {
saveNdgAndIdVisura(application, company, ndgResponse.getNdg()); saveNdg(application, company, ndgResponse.getNdg());
log.info("NDG successfully generated for applicationId: {}", applicationId); log.info("NDG successfully generated for applicationId: {}", applicationId);
} else { } else {
log.info("Polling for NDG for applicationId: {}", applicationId); log.info("Polling for NDG for applicationId: {}", applicationId);
@@ -589,7 +574,7 @@ public class AppointmentDao {
company.setNdg(fetchedNdg); company.setNdg(fetchedNdg);
application.setNdg(fetchedNdg); application.setNdg(fetchedNdg);
application.setNdgStatus(GepafinConstant.NDG_GENERATED); application.setNdgStatus(NdgStatusEnum.NDG_GENERATED.getValue());
application.setStatus(ApplicationStatusTypeEnum.NDG.getValue()); application.setStatus(ApplicationStatusTypeEnum.NDG.getValue());
application.setIdVisura(visuraResponse.getIdVisura()); application.setIdVisura(visuraResponse.getIdVisura());
@@ -678,7 +663,7 @@ public class AppointmentDao {
return ndg != null && !ndg.isEmpty(); 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); ApplicationEntity oldApplication = Utils.getClonedEntityForData(application);
CompanyEntity oldCompanyEntity = Utils.getClonedEntityForData(company); CompanyEntity oldCompanyEntity = Utils.getClonedEntityForData(company);
@@ -1328,4 +1313,107 @@ public class AppointmentDao {
throw new RuntimeException("Failed to parse response: " + e.getMessage(), e); 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;
}
}