package com.imprimelibros.erp.externalApi; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.imprimelibros.erp.configuracion.margenes_presupuestos.MargenPresupuesto; import com.imprimelibros.erp.configuracion.margenes_presupuestos.MargenPresupuestoDao; import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta; import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion; import java.util.Map; import java.util.Optional; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.function.Supplier; import java.util.Locale; @Service public class skApiClient { @Value("${safekat.api.url}") private String skApiUrl; private final AuthService authService; private final RestTemplate restTemplate; private final MargenPresupuestoDao margenPresupuestoDao; private final MessageSource messageSource; public skApiClient(AuthService authService, MargenPresupuestoDao margenPresupuestoDao, MessageSource messageSource) { this.authService = authService; this.restTemplate = new RestTemplate(); this.margenPresupuestoDao = margenPresupuestoDao; this.messageSource = messageSource; } public String getPrice(Map requestBody, TipoEncuadernacion tipoEncuadernacion, TipoCubierta tipoCubierta) { return performWithRetry(() -> { String url = this.skApiUrl + "api/calcular"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(authService.getToken()); HttpEntity> entity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.POST, entity, String.class); try { Map responseBody = new ObjectMapper().readValue( response.getBody(), new TypeReference>() { }); ObjectMapper mapper = new ObjectMapper(); if (responseBody.get("error") == null) { Object dataObj = responseBody.get("data"); if (dataObj instanceof Map) { @SuppressWarnings("unchecked") Map data = (Map) dataObj; List tiradas = mapper.convertValue( data.get("tiradas"), new TypeReference>() { }); List precios = mapper.convertValue( data.get("precios"), new TypeReference>() { }); for (int i = 0; i < precios.size(); i++) { BigDecimal importe = new BigDecimal(precios.get(i)); BigDecimal importeTotal = importe.multiply(BigDecimal.valueOf(tiradas.get(i))); MargenPresupuesto margen = margenPresupuestoDao .findByImporte(importeTotal).orElse(null); if (margen != null) { BigDecimal margenValue = calcularMargen( importeTotal, margen.getImporteMin(), margen.getImporteMax(), margen.getMargenMax(), margen.getMargenMin()); BigDecimal nuevoPrecio = new BigDecimal(precios.get(i)).multiply(BigDecimal.ONE .add(margenValue.divide(BigDecimal.valueOf(100), RoundingMode.HALF_UP))); precios.set(i, nuevoPrecio.setScale(4, RoundingMode.HALF_UP).doubleValue()); // redondear // a 4 // decimales } else { System.out.println("No se encontró margen para importe " + importe); } } // <-- Clave: sustituir la lista en el map que se devuelve data.put("precios", precios); // (tiradas no cambia, pero si la modificases: data.put("tiradas", tiradas);) } return mapper.writeValueAsString(Map.of("data", responseBody.get("data"))); } else { return "{\"error\": 1}"; } } catch (JsonProcessingException e) { e.printStackTrace(); return "{\"error\": 1}"; } }); } public Map savePresupuesto(Map requestBody) { return performWithRetryMap(() -> { String url = this.skApiUrl + "api/guardar"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(authService.getToken()); HttpEntity> entity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.POST, entity, String.class); ObjectMapper mapper = new ObjectMapper(); try { Map responseBody = mapper.readValue( response.getBody(), new TypeReference>() { }); // Si la API devuelve "error" a nivel raíz if (responseBody.get("error") != null) { // Devolvemos un mapa con sólo el error para que el caller decida return Map.of("error", responseBody.get("error")); } Object dataObj = responseBody.get("data"); if (dataObj instanceof Map dataRaw) { @SuppressWarnings("unchecked") Map data = (Map) dataRaw; Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false); Long id = ((Integer) data.get("id")).longValue(); String iskn = (String) data.get("iskn"); // OJO: aquí mantengo tu lógica tal cual (success == null o false => OK) // Si tu API realmente usa success=true como éxito, esto habría que invertirlo. if (success != null && success) { if (id != null && iskn != null) { data.put("id", Long.valueOf(id)); data.put("iskn", iskn); } } else { // Tu lógica actual: si success es true u otra cosa → error 2 return Map.of("error", 2); } // Devolvemos sólo la parte interesante: el data ya enriquecido return Map.of("data", data); } // Si data no es un Map, devolvemos error genérico return Map.of("error", 1); } catch (JsonProcessingException e) { e.printStackTrace(); return Map.of("error", 1); } }); } public Long crearPedido(Map requestBody) { Map result = performWithRetryMap(() -> { String url = this.skApiUrl + "api/crear-pedido"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(authService.getToken()); HttpEntity> entity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.POST, entity, String.class); ObjectMapper mapper = new ObjectMapper(); try { Map responseBody = mapper.readValue( response.getBody(), new TypeReference>() { }); // Si la API devuelve "error" a nivel raíz if (responseBody.get("error") != null) { // Devolvemos un mapa con sólo el error para que el caller decida return Map.of("error", responseBody.get("error")); } Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false); Long id = ((Integer) responseBody.get("id")).longValue(); if (success != null && id != null && success) { return Map.of("data", id); } else { // Tu lógica actual: si success es true u otra cosa → error 2 return Map.of("error", 2); } } catch (JsonProcessingException e) { e.printStackTrace(); return Map.of("error", 1); } }); if (result.get("error") != null) { throw new RuntimeException("Error al crear el pedido: " + result.get("error")); } return (Long) result.get("data"); } public Map getMaxSolapas(Map requestBody, Locale locale) { try { String jsonResponse = performWithRetry(() -> { String url = this.skApiUrl + "api/calcular-solapas"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(authService.getToken()); // token actualizado Map data = new HashMap<>(); data.put("clienteId", requestBody.get("clienteId")); data.put("tamanio", requestBody.get("tamanio")); data.put("tirada", requestBody.get("tirada")); data.put("paginas", requestBody.get("paginas")); data.put("paginasColor", requestBody.get("paginasColor")); data.put("papelInteriorDiferente", 0); data.put("paginasCuadernillo", requestBody.get("paginasCuadernillo")); data.put("tipo", requestBody.get("tipo")); data.put("isColor", requestBody.get("isColor")); data.put("isHq", requestBody.get("isHq")); data.put("interior", requestBody.get("interior")); HttpEntity> entity = new HttpEntity<>(data, headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.POST, entity, String.class); return response.getBody(); }); ObjectMapper mapper = new ObjectMapper(); JsonNode root = mapper.readTree(jsonResponse); if (root.get("data") == null || !root.get("data").isInt()) { throw new RuntimeException( messageSource.getMessage("presupuesto.errores.error-interior", new Object[] { 1 }, locale)); } Integer maxSolapas = root.get("data").asInt(); Double lomo = root.get("lomo").asDouble(); return Map.of( "maxSolapas", maxSolapas, "lomo", lomo); } catch (JsonProcessingException e) { // Fallback al 80% del ancho Map tamanio = new ObjectMapper().convertValue( requestBody.get("tamanio"), new TypeReference>() { }); if (tamanio == null || tamanio.get("ancho") == null) throw new RuntimeException("Tamaño no válido en la solicitud: " + requestBody); else { int ancho = (int) tamanio.get("ancho"); return Map.of( "maxSolapas", (int) (ancho * 0.8), "lomo", 0.0); } } } public Double getRetractilado(Map requestBody) { String value = performWithRetry(() -> { String url = this.skApiUrl + "api/calcular-retractilado"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(authService.getToken()); HttpEntity> entity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.POST, entity, String.class); try { Map responseBody = new ObjectMapper().readValue( response.getBody(), new TypeReference>() { }); return responseBody.get("data").toString(); } catch (JsonProcessingException e) { e.printStackTrace(); return "0.0"; // Fallback en caso de error } }); try { return Double.parseDouble(value); } catch (NumberFormatException e) { throw new RuntimeException("Error al parsear el valor de retractilado: " + value, e); } } public Map getCosteEnvio(Map data, Locale locale) { return performWithRetryMap(() -> { String url = this.skApiUrl + "api/calcular-envio"; URI uri = UriComponentsBuilder.fromUriString(url) .queryParam("pais_code3", data.get("pais_code3")) .queryParam("cp", data.get("cp")) .queryParam("peso", data.get("peso")) .queryParam("unidades", data.get("unidades")) .queryParam("palets", data.get("palets")) .build(true) // no re-encode [] .toUri(); HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(authService.getToken()); ResponseEntity response = restTemplate.exchange( uri, HttpMethod.GET, new HttpEntity<>(headers), String.class); try { Map responseBody = new ObjectMapper().readValue( response.getBody(), new TypeReference>() { }); Boolean error = (Boolean) responseBody.get("error"); if (error != null && error) { return Map.of("error", messageSource.getMessage("direcciones.error.noShippingCost", null, locale)); } else { Double total = Optional.ofNullable(responseBody.get("data")) .filter(Number.class::isInstance) .map(Number.class::cast) .map(Number::doubleValue) .orElse(0.0); return Map.of("data", total); } } catch (JsonProcessingException e) { e.printStackTrace(); return Map.of("error", "Internal Server Error: 1"); // Fallback en caso de error } }); } public Map checkPedidoEstado(Long presupuestoId, Locale locale) { try { String jsonResponse = performWithRetry(() -> { String url = this.skApiUrl + "api/estado-pedido/" + presupuestoId; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(authService.getToken()); // token actualizado HttpEntity entity = new HttpEntity<>(headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.GET, entity, String.class); return response.getBody(); }); ObjectMapper mapper = new ObjectMapper(); JsonNode root = mapper.readTree(jsonResponse); if (root.get("data") == null) { throw new RuntimeException( "Sin respuesta desde el servidor del proveedor"); } String estado = root.get("data").asText(); return Map.of( "estado", estado); } catch (JsonProcessingException e) { // Fallback al 80% del ancho return Map.of( "estado", null); } } public Map getFilesTypes(Long presupuestoId, Locale locale) { try { Map result = performWithRetryMap(() -> { String url = this.skApiUrl + "api/files-presupuesto/" + presupuestoId; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(authService.getToken()); // token actualizado HttpEntity entity = new HttpEntity<>(headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.GET, entity, String.class); ObjectMapper mapper = new ObjectMapper(); try { Map responseBody = mapper.readValue( response.getBody(), new TypeReference>() { }); // Si la API devuelve "error" a nivel raíz if (responseBody.get("error") != null) { // Devolvemos un mapa con sólo el error para que el caller decida return Map.of("error", responseBody.get("error")); } Boolean hasError = (Boolean) (responseBody.get("error") == null || responseBody.get("error") == "null" ? false : true); Map files = (Map) responseBody.get("data"); if (files != null && !hasError) { return Map.of("data", files); } else { // Tu lógica actual: si success es true u otra cosa → error 2 return Map.of("error", 2); } } catch (JsonProcessingException e) { e.printStackTrace(); return Map.of("error", 1); } }); if (result.get("error") != null) { throw new RuntimeException( messageSource.getMessage("pedido.errors.connecting-server-error", null, locale)); } Map data = (Map) result.get("data"); return data; } catch (RuntimeException e) { throw new RuntimeException( messageSource.getMessage("pedido.errors.connecting-server-error", null, locale)); } } public byte[] downloadFile(Long presupuestoId, String fileType, Locale locale) { return performWithRetryBytes(() -> { String normalized = (fileType == null) ? "" : fileType.trim().toLowerCase(); String endpoint = switch (normalized) { case "ferro" -> "api/get-ferro/" + presupuestoId; case "cubierta" -> "api/get-cubierta/" + presupuestoId; case "tapa" -> "api/get-tapa/" + presupuestoId; default -> throw new IllegalArgumentException("Tipo de fichero no soportado: " + fileType); }; // OJO: skApiUrl debería terminar en "/" para que concatene bien String url = this.skApiUrl + endpoint; HttpHeaders headers = new HttpHeaders(); // Si tu CI4 requiere Bearer, mantenlo. Si NO lo requiere, puedes quitar esta // línea. headers.setBearerAuth(authService.getToken()); headers.setAccept(List.of(MediaType.APPLICATION_PDF, MediaType.APPLICATION_OCTET_STREAM)); try { ResponseEntity response = restTemplate.exchange( url, HttpMethod.GET, new HttpEntity<>(headers), byte[].class); if (response.getStatusCode().is2xxSuccessful()) { return response.getBody(); // bytes del PDF } return null; } catch (HttpClientErrorException.NotFound e) { // CI4 no tiene ese fichero return null; } }); } public Boolean aceptarFerro(Long presupuestoId, Locale locale) { String result = performWithRetry(() -> { String url = this.skApiUrl + "api/aceptar-ferro/" + presupuestoId; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(authService.getToken()); HttpEntity entity = new HttpEntity<>(headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.POST, entity, String.class); try { Map responseBody = new ObjectMapper().readValue( response.getBody(), new TypeReference>() { }); Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false); return success.toString(); } catch (JsonProcessingException e) { e.printStackTrace(); return "false"; // Fallback en caso de error } }); return Boolean.parseBoolean(result); } public Boolean cancelarPedido(Long pedidoId) { String result = performWithRetry(() -> { String url = this.skApiUrl + "api/cancelar-pedido/" + pedidoId; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(authService.getToken()); HttpEntity entity = new HttpEntity<>(headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.POST, entity, String.class); try { Map responseBody = new ObjectMapper().readValue( response.getBody(), new TypeReference>() { }); Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false); return success.toString(); } catch (JsonProcessingException e) { e.printStackTrace(); return "false"; // Fallback en caso de error } }); return Boolean.parseBoolean(result); } /****************** * PRIVATE METHODS ******************/ private String performWithRetry(Supplier request) { try { return request.get(); } catch (HttpClientErrorException.Unauthorized e) { // Token expirado, renovar y reintentar authService.invalidateToken(); try { return request.get(); // segundo intento } catch (HttpClientErrorException ex) { throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex); } } } private Map performWithRetryMap(Supplier> request) { try { return request.get(); } catch (HttpClientErrorException.Unauthorized e) { // Token expirado, renovar y reintentar authService.invalidateToken(); try { return request.get(); // segundo intento } catch (HttpClientErrorException ex) { throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex); } } } private byte[] performWithRetryBytes(Supplier request) { try { return request.get(); } catch (HttpClientErrorException.Unauthorized e) { authService.invalidateToken(); try { return request.get(); } catch (HttpClientErrorException ex) { throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex); } } } private static BigDecimal calcularMargen( BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax, BigDecimal margenMax, BigDecimal margenMin) { if (importe.compareTo(importeMin) <= 0) return margenMax; if (importe.compareTo(importeMax) >= 0) return margenMin; return margenMax.subtract(margenMax.subtract(margenMin) .multiply(importe.subtract(importeMin) .divide(importeMax.subtract(importeMin), RoundingMode.HALF_UP))); } }