package net.gepafin.tendermanagement.dao; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.GetObjectRequest; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import feign.FeignException; import io.jsonwebtoken.Claims; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; 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.ApplicationEntity; import net.gepafin.tendermanagement.entities.CompanyEntity; import net.gepafin.tendermanagement.entities.DocumentEntity; import net.gepafin.tendermanagement.entities.HubEntity; import net.gepafin.tendermanagement.enums.ApplicationStatusTypeEnum; 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.AppointmentNdgRequest; import net.gepafin.tendermanagement.model.request.AppointmentVisuraListRequest; import net.gepafin.tendermanagement.model.request.AppointmentVisuraRequest; import net.gepafin.tendermanagement.model.request.CreateAppointmentRequest; import net.gepafin.tendermanagement.model.request.UploadDocToExternalSystemRequest; import net.gepafin.tendermanagement.model.request.VersionHistoryRequest; 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.service.ApplicationService; import net.gepafin.tendermanagement.service.CompanyService; import net.gepafin.tendermanagement.service.feignClient.AppointmentApiService; import net.gepafin.tendermanagement.util.LoggingUtil; import net.gepafin.tendermanagement.util.Utils; import net.gepafin.tendermanagement.web.rest.api.errors.CustomValidationException; import net.gepafin.tendermanagement.web.rest.api.errors.Status; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @Slf4j @Component public class AppointmentDao { @Value("${appointment.portal.user}") private String user; @Value("${appointment.portal.password}") private String password; @Value("${appointment.portal.source}") private String source; @Value("${appointment.portal.context}") private String context; @Value("${default.hub.uuid}") private String defaultHubUuid; @Value("${aws.s3.url}") private String s3Url; @Value("${aws.s3.bucket.name}") private String OLD_BUCKET; @Value("${flagDaFirmare}") private Boolean flagDaFirmare; @Autowired private HubRepository hubRepository; @Autowired private AppointmentApiService appointmentApiService; @Autowired private ApplicationService applicationService; @Autowired private CompanyService companyService; @Autowired private ApplicationRepository applicationRepository; @Autowired private CompanyRepository companyRepository; @Autowired private DocumentDao documentDao; @Autowired private AmazonS3Client s3Client; @Autowired private DocumentRepository documentRepository; @Autowired private HttpServletRequest request; @Autowired private LoggingUtil loggingUtil; @Autowired private TokenProvider tokenProvider; @Autowired private NotificationDao notificationDao; @Autowired private UserRepository userRepository; private final Map executorMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap threadForDocumentMap = new ConcurrentHashMap<>(); private static final ThreadLocal threadLocalHubId = new ThreadLocal<>(); public NdgResponse checkNdgForAppointment(Long applicationId) { ApplicationEntity application = applicationService.validateApplication(applicationId); NdgResponse ndgResponse = new NdgResponse(); if (application.getNdgStatus() != null && application.getNdgStatus().equalsIgnoreCase(GepafinConstant.NDG_IN_PROGRESS)) { throw new CustomValidationException(Status.SUCCESS, Translator.toLocale(GepafinConstant.NDG_GENERATION_IS_IN_PROGRESS)); } if (application.getNdgStatus() != null && application.getNdgStatus().equalsIgnoreCase(GepafinConstant.NDG_GENERATED) && application.getNdg() != null) { ndgResponse.setNdg(application.getNdg()); return ndgResponse; } // Update application status application.setNdgStatus(GepafinConstant.NDG_IN_PROGRESS); applicationRepository.save(application); // Start async processing startAsyncNdgProcessing(applicationId); throw new CustomValidationException(Status.SUCCESS, Translator.toLocale(GepafinConstant.NDG_GENERATION_IS_IN_PROGRESS)); } private void startAsyncNdgProcessing(Long applicationId) { // Check if a thread is already running for this application if (executorMap.containsKey(applicationId)) { log.warn("Async processing already running for applicationId: {}", applicationId); return; } // Create a dedicated thread for asynchronous processing ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> { Thread thread = new Thread(runnable); thread.setName("AsyncNdgProcessing-" + applicationId); return thread; }); executorMap.put(applicationId, executor); executor.submit(() -> { try { log.info("Starting async processing for applicationId: {}", applicationId); processNdgGeneration(applicationId); } catch (Exception e) { log.error("Error in async NDG processing for applicationId: {}", applicationId, e); } finally { // Cleanup resources ExecutorService executorToShutdown = executorMap.remove(applicationId); if (executorToShutdown != null) { executorToShutdown.shutdown(); } log.info("Async processing completed for applicationId: {}", applicationId); } }); } private void processNdgGeneration(Long applicationId) { // Validate application, company, and hub 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."); throw new CustomValidationException(Status.BAD_REQUEST, Translator.toLocale(GepafinConstant.NO_NDG_FOR_ANOTHER_HUB)); } try { // Authenticate and fetch token if required if (hub.getAppointmentAuthTokenId() == null || hub.getAreaCode() == null) { authenticateAndSaveToken(hub); } String authorizationToken = getBearerToken(hub); // Try retrieving NDG by VAT number AppointmentLoginResponse ndgResponse = retrieveNdgByVatNumber(company.getVatNumber(), authorizationToken, hub, application); if (isNdgValid(ndgResponse.getNdg())) { saveNdgAndIdVisura(application, company, ndgResponse.getNdg(), null); log.info("NDG successfully generated for applicationId: {}", applicationId); } else { // If NDG isn't immediately available, start polling handleNdgPolling(application, company, hub, authorizationToken); } } catch (Exception e) { log.error("Error during NDG generation for applicationId: {}", applicationId, e); } } private void handleNdgPolling(ApplicationEntity application, CompanyEntity company, HubEntity hub, String authorizationToken) { try { log.info("Starting NDG polling for applicationId: {}", application.getId()); long startTime = System.currentTimeMillis(); while (true) { if (application.getNdg() != null) { log.info("NDG retrieved for applicationId: {}", application.getId()); break; } try { // Fetch Visura list and attempt to parse NDG String visuraListJson = getVisuraList(application.getIdVisura(), authorizationToken, application, hub); String ndg = parseNdgFromVisuraListResponse(visuraListJson); if (isNdgValid(ndg)) { // CompanyEntity oldCompanyData = Utils.getClonedEntityForData(company); // ApplicationEntity oldApplicationData = Utils.getClonedEntityForData(application); company.setNdg(ndg); application.setNdg(ndg); application.setNdgStatus(GepafinConstant.NDG_GENERATED); application.setStatus(ApplicationStatusTypeEnum.NDG.getValue()); applicationRepository.save(application); companyRepository.save(company); log.info("NDG saved successfully for applicationId: {}", application.getId()); // /** This code is responsible for adding a version history log for the "update application ndg code, status, and Id visura" // operation. **/ // loggingUtil.addVersionHistory( // VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldApplicationData) // .newData(application).build()); // // /** This code is responsible for adding a version history log for the "update company ndg code" operation. **/ // loggingUtil.addVersionHistory( // VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldCompanyData) // .newData(company).build()); break; } // Check if polling has timed out if (System.currentTimeMillis() - startTime > TimeUnit.HOURS.toMillis(2)) { log.warn("NDG polling timed out for applicationId: {}", application.getId()); application.setNdgStatus(GepafinConstant.NDG_FAILED); applicationRepository.save(application); break; } // Wait before the next polling attempt Thread.sleep(TimeUnit.MINUTES.toMillis(15)); } catch (InterruptedException e) { log.warn("NDG polling interrupted for applicationId: {}", application.getId()); Thread.currentThread().interrupt(); break; } catch (Exception e) { log.error("Error during NDG polling for applicationId: {}", application.getId(), e); } } } finally { log.info("NDG polling completed for applicationId: {}", application.getId()); } } private static String getBearerToken(HubEntity hub) { return "Bearer " + hub.getAppointmentAuthTokenId(); } private boolean isNdgValid(String ndg) { return ndg != null && !ndg.isEmpty(); } private void saveNdgAndIdVisura(ApplicationEntity application, CompanyEntity company, String ndg, String idVisura) { //cloned for old application and company data // ApplicationEntity oldApplicationData = Utils.getClonedEntityForData(application); // CompanyEntity oldCompanyData = Utils.getClonedEntityForData(company); application.setNdg(ndg); application.setIdVisura(idVisura); application.setNdgStatus(GepafinConstant.NDG_GENERATED); application.setStatus(ApplicationStatusTypeEnum.NDG.getValue()); company.setNdg(ndg); companyRepository.save(company); applicationRepository.save(application); Map placeHolders=notificationDao.sendNotificationToBeneficiary(application,NotificationTypeEnum.NDG_GENERATION); notificationDao.sendNotificationToSuperUser(application,placeHolders,NotificationTypeEnum.NDG_GENERATION); // /** This code is responsible for adding a version history log for the "update application ndg code, status, and Id visura" operation. **/ // loggingUtil.addVersionHistory( // VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldApplicationData).newData(application).build()); // // /** This code is responsible for adding a version history log for the "update company ndg code" operation. **/ // loggingUtil.addVersionHistory(VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldCompanyData).newData // (company).build()); log.info("NDG saved for applicationId: {}, {}", application.getId(), application.getNdg()); } private String getVisuraList(String idVisura, String authorizationToken, ApplicationEntity application, HubEntity hub) { AppointmentVisuraListRequest visuraListRequest = new AppointmentVisuraListRequest(); AppointmentVisuraListRequest.VisuraFilter filter = new AppointmentVisuraListRequest.VisuraFilter(); filter.setIdVisura(idVisura); visuraListRequest.setFilter(filter); try { String requestJson = Utils.convertObjectToJson(visuraListRequest); ResponseEntity response = appointmentApiService.getVisuraList(requestJson, authorizationToken); return Utils.convertObjectToJson(response.getBody()); } catch (FeignException.Forbidden forbiddenException) { log.error("403 Forbidden received while getting visuraList for Ndg code. Regenerating token..."); // Regenerate the token and retry String newAuthorizationToken = regenerateTokenAndSave(hub); return getVisuraList(idVisura, newAuthorizationToken, application, hub); } catch (Exception e) { log.error("Failed to fetch Ndg code: {}", e.getMessage(), e); throw new RuntimeException("Error fetching Ndg List", e); } } private HubEntity authenticateAndSaveToken(HubEntity hub) { // HubEntity oldHubData = Utils.getClonedEntityForData(hub); try { //code to generate token with payload having "iat" epoch timestamp and secret key with no expiry and send in below method call String authJwtToken = Utils.generateAuthTokenForLoginToOdessa(); log.info("Got the auth for login to odessa {}", authJwtToken); hub.setAuthToken(authJwtToken); hubRepository.save(hub); // /** This code is responsible for adding a version history log for the "Updating auth token for login api in hub" operation. **/ // loggingUtil.addVersionHistory(VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldHubData).newData // (hub).build()); // Prepare the request body (adjust if necessary for login API) Map body = Collections.emptyMap(); // Perform login API call ResponseEntity responseLogin = appointmentApiService.loginWithOdessa(authJwtToken, source, context, user, password, body); // Handle successful login if (responseLogin.getStatusCode() == HttpStatus.OK) { log.info("Login successful to odessa. Parsing response."); String loginResponseJson = Utils.convertObjectToJson(responseLogin.getBody()); AppointmentLoginResponse parsedResponse = parseLoginResponse(loginResponseJson); // Validate and save token if (parsedResponse.getTokenId() != null) { hub.setAppointmentAuthTokenId(parsedResponse.getTokenId()); hubRepository.save(hub); // /** This code is responsible for adding a version history log for the "inserting token and areaCode from login odessa response for // appointment flow api's" // * operation. **/ // loggingUtil.addVersionHistory( // VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldHubData).newData(hub) // .build()); log.info("Saved new authToken and areaCode for Hub."); return hub; } else { throw new RuntimeException("Login response is missing a valid tokenId for login to odessa system, please try again."); } } // Handle non-OK response throw new CustomValidationException(Status.BAD_REQUEST, Translator.toLocale(GepafinConstant.ERROR_IN_GENERATING_NDG_TRY_AGAIN)); } catch (FeignException.Forbidden forbiddenException) { logForbiddenError(); // Regenerate the token and retry regenerateTokenAndSave(hub); } catch (Exception e) { log.error("Failed to authenticate user on Odessa : {}", e.getMessage(), e); throw new RuntimeException("Authentication failed on Odessa. try again", e); } return null; } private AppointmentLoginResponse retrieveNdgByVatNumber(String vatNumber, String authorizationToken, HubEntity hub, ApplicationEntity application) { try { // Prepare the NDG request AppointmentNdgRequest ndgRequest = getAppointmentNdgRequest(vatNumber); // Call the API to retrieve NDG ResponseEntity response = appointmentApiService.getNdgByVatNumber(ndgRequest, authorizationToken); String responseJson = Utils.convertObjectToJson(response.getBody()); // Parse and return the NDG response return parseNdgResponse(responseJson); } catch (FeignException.Forbidden forbiddenException) { logForbiddenError(); // Regenerate the token and retry String newAuthorizationToken = regenerateTokenAndSave(hub); return retrieveNdgByVatNumber(vatNumber, newAuthorizationToken, hub, application); } catch (Exception e) { log.error("Failed to retrieve NDG by VAT number: {}", e.getMessage(), e); throw new RuntimeException("NDG retrieval failed.", e); } } private String regenerateTokenAndSave(HubEntity hub) { try { hub = authenticateAndSaveToken(hub); return "Bearer " + hub.getAppointmentAuthTokenId(); } catch (Exception e) { log.error("Failed to regenerate token from Odessa: {}", e.getMessage()); throw new RuntimeException("Token regeneration failed from Odessa.", e); } } private AppointmentLoginResponse createVisura(CompanyEntity company, String authorizationToken, HubEntity hub) { try { String visuraRequest = getAppointmentVisuraRequest(company, hub.getAreaCode()); ResponseEntity response = appointmentApiService.createVisura(visuraRequest, authorizationToken); String responseJson = Utils.convertObjectToJson(response.getBody()); return parseVisuraResponse(responseJson); } catch (FeignException.Forbidden forbiddenException) { logForbiddenError(); // Regenerate the token and retry String newAuthorizationToken = regenerateTokenAndSave(hub); return createVisura(company, newAuthorizationToken, hub); } catch (Exception e) { log.error("Failed to create Visura for Ndg : {}", e.getMessage()); throw new RuntimeException("Visura creation failed for Ndg.", e); } } private static void logForbiddenError() { log.error("403 Forbidden received while retrieving NDG. Regenerating token..."); } private static AppointmentNdgRequest getAppointmentNdgRequest(String vatNumber) { AppointmentNdgRequest request = new AppointmentNdgRequest(); AppointmentNdgRequest.Filter filter = new AppointmentNdgRequest.Filter(); filter.setPartitaIva(vatNumber); AppointmentNdgRequest.Pagination pagination = new AppointmentNdgRequest.Pagination(); pagination.setTargetPage(AppointmentApiConstant.TARGET_PAGE_SIZE); pagination.setRecordsPerPage(AppointmentApiConstant.RECORD_PER_PAGE_SIZE); request.setFilter(filter); request.setPagination(pagination); return request; } private static String getAppointmentVisuraRequest(CompanyEntity company, String areaCode) { AppointmentVisuraRequest visuraRequest = new AppointmentVisuraRequest(); AppointmentVisuraRequest.VisuraInput input = new AppointmentVisuraRequest.VisuraInput(); input.setPartitaIva(company.getVatNumber()); input.setCodiceFiscale(company.getCodiceFiscale()); input.setCodArea(areaCode); input.setVisuraMode(AppointmentApiConstant.VISURA_MODE); input.setVisuraProvider(AppointmentApiConstant.VISURA_PROVIDER); input.setCodAgente(AppointmentApiConstant.COD_AGENTE); input.setAnagraficaLegame(AppointmentApiConstant.IS_ANAGRAFICA_LEGAME); input.setCreaAnagrafica(AppointmentApiConstant.CREA_ANAGRAFICA); input.setFromRating(AppointmentApiConstant.IS_FROM_RATING); input.setSalvaDocumenti(AppointmentApiConstant.SALVA_DOCUMENTI); input.setVisuraType(AppointmentApiConstant.VISURA_TYPE); visuraRequest.setInput(input); return Utils.convertObjectToJson(visuraRequest); } private String parseNdgFromVisuraListResponse(String jsonResponse) { try { ObjectMapper objectMapper = new ObjectMapper(); JsonNode rootNode = objectMapper.readTree(jsonResponse); JsonNode dataNode = rootNode.get(GepafinConstant.DATA_STRING); if (dataNode != null && dataNode.isArray() && dataNode.size() > 0) { JsonNode firstEntry = dataNode.get(0); JsonNode ndgClienteNode = firstEntry.get("ndgCliente"); if (ndgClienteNode != null && ndgClienteNode.get("code") != null) { String code = ndgClienteNode.get("code").asText(); return normalizeNullValue(code); } } log.warn("NDG not found in Visura List API response."); return null; } catch (Exception e) { log.error("Failed to parse NDG from Visura List API response: {}", e.getMessage(), e); throw new RuntimeException("Error parsing NDG from Visura List API response", e); } } public AppointmentLoginResponse parseLoginResponse(String jsonResponse) { try { ObjectMapper objectMapper = new ObjectMapper(); JsonNode rootNode = objectMapper.readTree(jsonResponse); JsonNode dataNode = rootNode.get(GepafinConstant.DATA_STRING); if (dataNode != null) { AppointmentLoginResponse response = new AppointmentLoginResponse(); response.setTokenId(dataNode.get("tokenId").asText()); JsonNode areasNode = dataNode.get("areas"); if (areasNode != null && areasNode.isArray() && areasNode.size() > 0) { response.setAreaCode(areasNode.get(0).get("code").asText()); } response.setCompanyId(dataNode.get("companyId").asLong()); return response; } else { throw new RuntimeException("Invalid JSON structure: Missing 'data' node."); } } catch (Exception e) { throw new RuntimeException("Failed to parse response from loginApi for odessa: " + e.getMessage(), e); } } public AppointmentLoginResponse parseVisuraResponse(String jsonResponse) { try { ObjectMapper objectMapper = new ObjectMapper(); JsonNode rootNode = objectMapper.readTree(jsonResponse); JsonNode dataNode = rootNode.get(GepafinConstant.DATA_STRING); if (dataNode != null) { AppointmentLoginResponse response = new AppointmentLoginResponse(); response.setIdVisura(normalizeNullValue(dataNode.get(GepafinConstant.ID_VISURA_STRING).asText())); response.setNdg(normalizeNullValue(dataNode.get(GepafinConstant.NDG_STRING).asText())); return response; } else { throw new RuntimeException("Invalid JSON structure: Missing 'data' node."); } } catch (Exception e) { throw new RuntimeException("Failed to parse response: " + e.getMessage(), e); } } public AppointmentLoginResponse parseNdgResponse(String jsonResponse) { try { ObjectMapper objectMapper = new ObjectMapper(); JsonNode rootNode = objectMapper.readTree(jsonResponse); JsonNode dataArray = rootNode.get(GepafinConstant.DATA_STRING); if (dataArray == null || !dataArray.isArray() || dataArray.isEmpty()) { log.info("NDG data is empty or missing in the response."); AppointmentLoginResponse emptyResponse = new AppointmentLoginResponse(); emptyResponse.setNdg(null); return emptyResponse; } JsonNode firstDataEntry = dataArray.get(0); AppointmentLoginResponse response = new AppointmentLoginResponse(); if (firstDataEntry.has(GepafinConstant.NDG_STRING)) { response.setNdg(normalizeNullValue(firstDataEntry.get(GepafinConstant.NDG_STRING).asText())); } return response; } catch (Exception e) { log.error("Failed to parse response: {}", e.getMessage(), e); throw new RuntimeException("Failed to parse NDG response.", e); } } private String normalizeNullValue(String value) { return (value == null || GepafinConstant.NULL_STRING.equalsIgnoreCase(value.trim())) ? null : value; } public AppointmentCreationResponse createAppointment(Long applicationId, CreateAppointmentRequest createAppointmentRequest) { // Validate the application ApplicationEntity application = applicationService.validateApplication(applicationId); AppointmentCreationResponse appointmentCreationResponse = new AppointmentCreationResponse(); ApplicationEntity oldApplicationData = Utils.getClonedEntityForData(application); HubEntity hub = hubRepository.findByHubId(application.getHubId()); // Check hub UUID and enforce constraints if (!hub.getUniqueUuid().equals(defaultHubUuid)) { log.info("Appointment cannot be created for another Hub; default is required for Gepafin."); throw new CustomValidationException(Status.BAD_REQUEST, Translator.toLocale(GepafinConstant.NO_APPOINTMENT_FOR_ANOTHER_HUB)); } try { // Pre-check conditions for appointment creation if (application.getNdg() != null && !Objects.equals(application.getNdgStatus(), GepafinConstant.NDG_IN_PROGRESS) && application.getAppointmentId() != null) { appointmentCreationResponse.setAppointmentId(application.getAppointmentId()); throw new CustomValidationException(Status.BAD_REQUEST, Translator.toLocale(GepafinConstant.APPOINTMENT_ALREADY_CREATED)); // return appointmentCreationResponse; } if (application.getNdg() == null && Objects.equals(application.getNdgStatus(), GepafinConstant.NDG_IN_PROGRESS)) { throw new CustomValidationException(Status.BAD_REQUEST, Translator.toLocale(GepafinConstant.NDG_NOT_FOUND_FOR_APPLICATION)); } // Generate authorization token and fetch template data String authorizationToken = getBearerToken(hub); int areaCode = Integer.parseInt(hub.getAreaCode()); ResponseEntity response = appointmentApiService.getAppointmentTemplateForTemplateCreation(authorizationToken, areaCode); if (response.getStatusCode() != HttpStatus.OK) { log.error("Failed to retrieve appointment template for appointment creation. Status: {}", response.getStatusCode()); throw new IllegalStateException("Failed to retrieve appointment template for appointment creation"); } // Parse template data String responseDataForTemplate = Utils.convertObjectToJson(response.getBody()); AppointmentCreationRequest templateRichiestaData = parseTemplateResponseData(responseDataForTemplate); // Build the appointment request body AppointmentCreationRequest appointmentCreationRequest = buildAppointmentCreationRequest(applicationId, createAppointmentRequest, hub.getAreaCode(), templateRichiestaData); String appointmentRequestBody = Utils.convertObjectToJson(appointmentCreationRequest); // Make API call to create the appointment ResponseEntity appointmentResponse = appointmentApiService.createAppointment(authorizationToken, context, appointmentRequestBody); String appointmentId = extractAppointmentIdFromResponse(appointmentResponse); if (appointmentId != null) { // Update application with the appointment ID application.setAppointmentId(appointmentId); application.setStatus(ApplicationStatusTypeEnum.APPOINTMENT.getValue()); applicationRepository.save(application); // Log version history loggingUtil.addVersionHistory( VersionHistoryRequest.builder().request(request).actionType(VersionActionTypeEnum.UPDATE).oldData(oldApplicationData).newData(application).build()); } appointmentCreationResponse.setAppointmentId(appointmentId); return appointmentCreationResponse; } catch (FeignException.Forbidden forbiddenException) { log.error("403 Forbidden received while retrieving template. Regenerating token..."); regenerateTokenAndSave(hub); return createAppointment(applicationId, createAppointmentRequest); } } private String extractAppointmentIdFromResponse(ResponseEntity appointmentResponse) { if (appointmentResponse.getBody() != null) { Map responseBody = (Map) appointmentResponse.getBody(); if (responseBody.containsKey(GepafinConstant.DATA_STRING)) { Map data = (Map) responseBody.get(GepafinConstant.DATA_STRING); if (data != null && data.containsKey(GepafinConstant.ID_STRING)) { return data.get(GepafinConstant.ID_STRING).toString(); } } } return null; } public AppointmentCreationRequest parseTemplateResponseData(String jsonResponse) { try { ObjectMapper objectMapper = new ObjectMapper(); JsonNode rootNode = objectMapper.readTree(jsonResponse); JsonNode richiesteClienteArray = rootNode.path(GepafinConstant.DATA_STRING).path(GepafinConstant.RICHIESTE_CLIENTE_STRING); // Initialize the result object AppointmentCreationRequest appointmentCreationRequest = new AppointmentCreationRequest(); AppointmentCreationRequest.Input input = new AppointmentCreationRequest.Input(); List richiestaClienteList = new ArrayList<>(); if (!richiesteClienteArray.isArray()) { log.warn("richiesteCliente array is missing or not an array."); return new AppointmentCreationRequest(); // Return empty object if array is missing } for (JsonNode richiestaNode : richiesteClienteArray) { if (richiestaNode.isNull()) continue; AppointmentCreationRequest.RichiestaCliente richiestaCliente = new AppointmentCreationRequest.RichiestaCliente(); richiestaCliente.setIdMotivazione(getIntValue(richiestaNode)); richiestaCliente.setCodProdotto(getTextValue(richiestaNode, AppointmentApiConstant.COD_PRODOTTO)); richiestaCliente.setCodAbi(getTextValue(richiestaNode, AppointmentApiConstant.COD_ABI)); richiestaCliente.setCodCab(getTextValue(richiestaNode, AppointmentApiConstant.COD_CAB)); richiestaCliente.setIdNota(getTextValue(richiestaNode, AppointmentApiConstant.ID_NOTA)); richiestaCliente.setImportoAgevolato(getTextValue(richiestaNode, AppointmentApiConstant.IMPORTO_AGEVOLATO)); richiestaCliente.setImportoMedioLungoTermine(getTextValue(richiestaNode, AppointmentApiConstant.IMPORTO_MEDIOLUNGO_TERMINE)); richiestaCliente.setCodTipoProdotto(getTextValue(richiestaNode, AppointmentApiConstant.COD_TIPO_PRODOTTO)); richiestaCliente.setCodCategoriaProdotto(getTextValue(richiestaNode, AppointmentApiConstant.COD_CATEGORIA_PRODOTTO)); richiestaCliente.setCodFormaTecnica(getTextValue(richiestaNode, AppointmentApiConstant.COD_FORMATECNICA)); richiestaCliente.setCodOperazione(getTextValue(richiestaNode, AppointmentApiConstant.COD_OPERAZIONE)); richiestaClienteList.add(richiestaCliente); } input.setRichiestaCliente(richiestaClienteList); appointmentCreationRequest.setInput(input); return appointmentCreationRequest; } catch (JsonProcessingException e) { log.error("JSON processing error: {}", e.getMessage(), e); throw new IllegalStateException("Invalid JSON structure in template response", e); } catch (Exception e) { log.error("Unexpected error while parsing template response: {}", e.getMessage(), e); throw new IllegalStateException("Failed to parse template response", e); } } private String getTextValue(JsonNode node, String fieldName) { return node.path(fieldName).isTextual() ? node.path(fieldName).asText() : null; } private int getIntValue(JsonNode node) { return node.path(AppointmentApiConstant.MOTIVAZIONE).path(AppointmentApiConstant.MOTIVAZIONE_ID).asInt(); } public AppointmentCreationRequest buildAppointmentCreationRequest(Long applicationId, CreateAppointmentRequest createAppointmentRequest, String areaCode, AppointmentCreationRequest templateRichiestaData) { ApplicationEntity application = applicationService.validateApplication(applicationId); CreateAppointmentRequest.Nota nota = createAppointmentRequest.getNota(); AppointmentCreationRequest appointmentCreationRequest = new AppointmentCreationRequest(); AppointmentCreationRequest.Input input = new AppointmentCreationRequest.Input(); // Set Input Fields input.setId(areaCode); input.setNdg(application.getNdg()); // Populate richiestaCliente from template data List richiestaClienteList = new ArrayList<>(); for (AppointmentCreationRequest.RichiestaCliente templateRichiesta : templateRichiestaData.getInput().getRichiestaCliente()) { AppointmentCreationRequest.RichiestaCliente richiestaCliente = new AppointmentCreationRequest.RichiestaCliente(); BeanUtils.copyProperties(templateRichiesta, richiestaCliente); // Add specific `nota` AppointmentCreationRequest.Nota requestNota = new AppointmentCreationRequest.Nota(); requestNota.setTitolo(nota.getTitolo()); requestNota.setTesto(nota.getTesto()); richiestaCliente.setNota(requestNota); richiestaCliente.setDurataMesiFinanziamento(createAppointmentRequest.getDurataMesiFinanziamento()); richiestaCliente.setImportoBreveTermine(createAppointmentRequest.getImportoBreveTermine()); richiestaClienteList.add(richiestaCliente); } input.setRichiestaCliente(richiestaClienteList); appointmentCreationRequest.setInput(input); return appointmentCreationRequest; } public DocumentUploadResponse uploadDocumentToExternalSystem(Long documentId, UploadDocToExternalSystemRequest docToExternalSystemRequest) { // Check if the document is already being processed DocumentEntity systemDoc = documentDao.validateDocument(documentId); Claims claims = tokenProvider.getClaimsFromToken(tokenProvider.extractTokenFromRequest(request)); Long hubId = Utils.extractHubIdFromPayload(claims.getSubject()); if (systemDoc.getDocumentAttachmentId() != null) { // If the documentAttachmentId is already set, return the response log.info("Document already uploaded with documentAttachmentId: {}", systemDoc.getDocumentAttachmentId()); DocumentUploadResponse response = new DocumentUploadResponse(); response.setDocumentAttachmentId(systemDoc.getDocumentAttachmentId()); return response; } // Check if a thread is already running for this document upload if (threadForDocumentMap.containsKey(documentId)) { log.warn("Document upload already running for documentId: {}", documentId); throw new CustomValidationException(Status.SUCCESS, Translator.toLocale(GepafinConstant.DOCUMENT_UPLOADING_IN_PROGRESS)); } // Start the upload process in the background ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> { Thread thread = new Thread(runnable); thread.setName(GepafinConstant.ASYNC_DOCUMENT_UPLOAD_NAME + documentId); return thread; }); threadForDocumentMap.put(documentId, executor); executor.submit(() -> { threadLocalHubId.set(hubId); try { log.info("Starting async document upload for documentId: {}", documentId); uploadDocumentToExternalSystemSync(documentId, docToExternalSystemRequest); } catch (Exception e) { log.error("Error in async document upload for documentId: {}", documentId, e); } finally { // Cleanup resources ExecutorService executorToShutdown = threadForDocumentMap.remove(documentId); if (executorToShutdown != null) { executorToShutdown.shutdown(); threadLocalHubId.remove(); } log.info("Async document upload completed for documentId: {}", documentId); } }); return null; // Return an immediate response indicating the process is in progress // throw new CustomValidationException(Status.SUCCESS, Translator.toLocale(GepafinConstant.DOCUMENT_UPLOADING_IN_PROGRESS)); } private void uploadDocumentToExternalSystemSync(Long documentId, UploadDocToExternalSystemRequest docToExternalSystemRequest) { // Synchronous upload logic DocumentEntity systemDoc = documentDao.validateDocument(documentId); Long hubId = threadLocalHubId.get(); HubEntity hub = hubRepository.findByHubId(hubId); if (!hub.getUniqueUuid().equals(defaultHubUuid)) { log.info("Document cannot be uploaded for another Hub, it is default for Gepafin."); throw new CustomValidationException(Status.BAD_REQUEST, Translator.toLocale(GepafinConstant.NO_DOCUMENT_UPLOAD_FOR_ANOTHER_HUB)); } log.info("Got Document in system: {}", systemDoc); String oldUrl = systemDoc.getFilePath(); String authorizationToken = getBearerToken(hub); try { File localFile = downloadFileFromS3(oldUrl); MultipartFile multipartFile = convertFileToMultipartFile(localFile); UploadDocToExternalSystemRequest externalSystemRequest = new UploadDocToExternalSystemRequest(); externalSystemRequest.setInput(getUploadDocumentInput(docToExternalSystemRequest)); String uploadDocRequest = Utils.convertObjectToJson(externalSystemRequest); ResponseEntity uploadedDocumentData = appointmentApiService.uploadDocumentToExternalSystemForAppointment(authorizationToken, context, uploadDocRequest, multipartFile); String responseData = Utils.convertObjectToJson(uploadedDocumentData.getBody()); DocumentUploadResponse parsedResponse = parseDocumentUploadResponse(responseData); if (parsedResponse == null) { throw new CustomValidationException(Status.BAD_REQUEST, Translator.toLocale(GepafinConstant.ERROR_UPLOADING_DOCUMENT)); } // Save the documentAttachmentId to the database systemDoc.setDocumentAttachmentId(parsedResponse.getDocumentAttachmentId()); documentRepository.save(systemDoc); log.info("Document uploaded successfully to external system: {}", parsedResponse); } catch (FeignException.Forbidden forbiddenException) { log.error("403 Forbidden received while uploading document. Regenerating token..."); regenerateTokenAndSave(hub); uploadDocumentToExternalSystemSync(documentId, docToExternalSystemRequest); } catch (Exception e) { log.error("Exception during document upload: {}", e.getMessage(), e); throw new CustomValidationException(Status.BAD_REQUEST, Translator.toLocale(GepafinConstant.EXTERNAL_DOCUMENT_UPLOAD_FAILURE_MSG)); } } private UploadDocToExternalSystemRequest.Input getUploadDocumentInput(UploadDocToExternalSystemRequest docToExternalSystemRequest) { UploadDocToExternalSystemRequest.Input input = new UploadDocToExternalSystemRequest.Input(); input.setIdTipoProtocollo(docToExternalSystemRequest.getInput().getIdTipoProtocollo()); input.setIdClassificazione(docToExternalSystemRequest.getInput().getIdClassificazione()); input.setFlagDaFirmare(flagDaFirmare); input.setDescrizione(docToExternalSystemRequest.getInput().getDescrizione()); UploadDocToExternalSystemRequest.Input.Attributes attributes = new UploadDocToExternalSystemRequest.Input.Attributes(); attributes.setNdg(docToExternalSystemRequest.getInput().getAttributes().getNdg()); attributes.setEmail(docToExternalSystemRequest.getInput().getAttributes().getEmail()); input.setAttributes(attributes); return input; } public static MultipartFile convertFileToMultipartFile(File file) throws IOException { FileInputStream input = new FileInputStream(file); return new MockMultipartFile(file.getName(), file.getName(), MediaType.APPLICATION_OCTET_STREAM_VALUE, input); } private File downloadFileFromS3(String fileUrl) throws Exception { String key = extractS3KeyFromUrl(fileUrl); File localFile = new File(GepafinConstant.TEMP_FILE_PATH + extractFileName(key)); GetObjectRequest getObjectRequest = new GetObjectRequest(OLD_BUCKET, key); try (InputStream s3Stream = s3Client.getObject(getObjectRequest).getObjectContent(); FileOutputStream outputStream = new FileOutputStream(localFile)) { s3Stream.transferTo(outputStream); } log.info("Downloaded file from old S3 bucket: {}", key); return localFile; } private String extractS3KeyFromUrl(String url) { return url.replace(s3Url, ""); } private String extractFileName(String filePath) { String[] parts = filePath.split("/"); return parts[parts.length - 1]; } public DocumentUploadResponse parseDocumentUploadResponse(String jsonResponse) { try { ObjectMapper objectMapper = new ObjectMapper(); JsonNode rootNode = objectMapper.readTree(jsonResponse); // Navigate to the "data" node JsonNode dataNode = rootNode.get(GepafinConstant.DATA_STRING); if (dataNode != null) { DocumentUploadResponse response = new DocumentUploadResponse(); // Extract "documentAttachmentId" JsonNode documentAttachmentIdNode = dataNode.get(GepafinConstant.DOCUMENT_ATTACHMENT_ID_STRING); if (documentAttachmentIdNode != null) { response.setDocumentAttachmentId(documentAttachmentIdNode.asText()); } else { throw new RuntimeException("Invalid JSON structure: Missing 'documentAttachmentId' node."); } return response; } else { return null; } } catch (Exception e) { throw new RuntimeException("Failed to parse response: " + e.getMessage(), e); } } }