From f6a683de817e4d84013287a61a9b0455127f4d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Mon, 27 Oct 2025 20:30:11 +0100 Subject: [PATCH 01/11] modificando carrito --- .../erp/cart/CartController.java | 41 +--- .../imprimelibros/erp/cart/CartService.java | 7 + .../erp/checkout/CheckoutController.java | 86 ++++++++ .../com/imprimelibros/erp/common/Utils.java | 22 ++ .../erp/direcciones/DireccionController.java | 87 +++++++- .../erp/direcciones/DireccionService.java | 86 ++++++++ .../erp/externalApi/skApiClient.java | 59 ++++++ src/main/resources/application.properties | 8 +- src/main/resources/i18n/app_es.properties | 1 + .../resources/i18n/direcciones_es.properties | 2 + src/main/resources/i18n/pedidos_en.properties | 0 src/main/resources/i18n/pedidos_es.properties | 17 ++ .../pages/imprimelibros/checkout/checkout.js | 193 ++++++++++++++++++ .../templates/imprimelibros/cart/cart.html | 27 ++- .../imprimelibros/checkout/_envio.html | 43 ++++ .../imprimelibros/checkout/_pago.html | 3 + .../imprimelibros/checkout/checkout.html | 175 ++++++++++++++++ .../direccion-form-fixed-user.html | 169 +++++++++++++++ .../direcciones/direccion-form.html | 1 + .../direcciones/direccionCard.html | 18 ++ .../templates/imprimelibros/login/login.html | 3 + .../com/imprimelibros/erp/calcularEnvios.java | 44 ++++ 22 files changed, 1050 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java create mode 100644 src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java create mode 100644 src/main/resources/i18n/pedidos_en.properties create mode 100644 src/main/resources/i18n/pedidos_es.properties create mode 100644 src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js create mode 100644 src/main/resources/templates/imprimelibros/checkout/_envio.html create mode 100644 src/main/resources/templates/imprimelibros/checkout/_pago.html create mode 100644 src/main/resources/templates/imprimelibros/checkout/checkout.html create mode 100644 src/main/resources/templates/imprimelibros/direcciones/direccion-form-fixed-user.html create mode 100644 src/main/resources/templates/imprimelibros/direcciones/direccionCard.html create mode 100644 src/test/java/com/imprimelibros/erp/calcularEnvios.java diff --git a/src/main/java/com/imprimelibros/erp/cart/CartController.java b/src/main/java/com/imprimelibros/erp/cart/CartController.java index 4bb4f78..9692451 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartController.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartController.java @@ -4,14 +4,10 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; -import com.imprimelibros.erp.users.UserDetailsImpl; - import jakarta.servlet.http.HttpServletRequest; -import com.imprimelibros.erp.users.User; - import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; +import com.imprimelibros.erp.common.Utils; import java.security.Principal; import java.util.Locale; @@ -27,32 +23,11 @@ public class CartController { this.service = service; } - /** - * Obtiene el ID de usuario desde tu seguridad. - * Adáptalo a tu UserDetails (e.g., SecurityContext con getId()) - */ - private Long currentUserId(Principal principal) { - if (principal == null) { - throw new IllegalStateException("Usuario no autenticado"); - } - - if (principal instanceof Authentication auth) { - Object principalObj = auth.getPrincipal(); - - if (principalObj instanceof UserDetailsImpl udi) { - return udi.getId(); - } else if (principalObj instanceof User u && u.getId() != null) { - return u.getId(); - } - } - - throw new IllegalStateException("No se pudo obtener el ID del usuario actual"); - } /** Vista del carrito */ @GetMapping public String viewCart(Model model, Principal principal, Locale locale) { - var items = service.listItems(currentUserId(principal), locale); + var items = service.listItems(Utils.currentUserId(principal), locale); model.addAttribute("items", items); return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple) } @@ -60,14 +35,14 @@ public class CartController { /** Añadir presupuesto via POST form */ @PostMapping("/add") public String add(@PathVariable(name = "presupuestoId", required = true) Long presupuestoId, Principal principal) { - service.addPresupuesto(currentUserId(principal), presupuestoId); + service.addPresupuesto(Utils.currentUserId(principal), presupuestoId); return "redirect:/cart"; } /** Añadir presupuesto con ruta REST (opcional) */ @PostMapping("/add/{presupuestoId}") public Object addPath(@PathVariable Long presupuestoId, Principal principal, HttpServletRequest request) { - service.addPresupuesto(currentUserId(principal), presupuestoId); + service.addPresupuesto(Utils.currentUserId(principal), presupuestoId); boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With")); if (isAjax) { // Responder 200 con la URL a la que quieres ir @@ -83,13 +58,13 @@ public class CartController { public long getCount(Principal principal) { if (principal == null) return 0; - return service.countItems(currentUserId(principal)); + return service.countItems(Utils.currentUserId(principal)); } /** Eliminar línea por ID de item */ @DeleteMapping("/{itemId}/remove") public String remove(@PathVariable Long itemId, Principal principal) { - service.removeItem(currentUserId(principal), itemId); + service.removeItem(Utils.currentUserId(principal), itemId); return "redirect:/cart"; } @@ -97,14 +72,14 @@ public class CartController { @DeleteMapping("/delete/item/{presupuestoId}") @ResponseBody public String removeByPresupuesto(@PathVariable Long presupuestoId, Principal principal) { - service.removeByPresupuesto(currentUserId(principal), presupuestoId); + service.removeByPresupuesto(Utils.currentUserId(principal), presupuestoId); return "redirect:/cart"; } /** Vaciar carrito completo */ @DeleteMapping("/clear") public String clear(Principal principal) { - service.clear(currentUserId(principal)); + service.clear(Utils.currentUserId(principal)); return "redirect:/cart"; } } diff --git a/src/main/java/com/imprimelibros/erp/cart/CartService.java b/src/main/java/com/imprimelibros/erp/cart/CartService.java index d04b349..b529819 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartService.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartService.java @@ -134,8 +134,15 @@ public class CartService { resumen.put("presupuestoId", presupuesto.getId()); + if(presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("ejemplar-prueba")) { + resumen.put("hasSample", true); + } else { + resumen.put("hasSample", false); + } Map detalles = utils.getTextoPresupuesto(presupuesto, locale); + resumen.put("tirada", presupuesto.getSelectedTirada()); + resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale)); resumen.put("base", presupuesto.getBaseImponible()); resumen.put("iva4", presupuesto.getIvaImporte4()); diff --git a/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java new file mode 100644 index 0000000..db83787 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java @@ -0,0 +1,86 @@ +package com.imprimelibros.erp.checkout; + +import java.security.Principal; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.springframework.context.MessageSource; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.server.ResponseStatusException; + +import com.imprimelibros.erp.common.Utils; +import com.imprimelibros.erp.direcciones.Direccion; +import com.imprimelibros.erp.i18n.TranslationService; +import com.imprimelibros.erp.paises.PaisesService; + +import jakarta.mail.Message; + +import com.imprimelibros.erp.direcciones.DireccionService; + +import com.imprimelibros.erp.cart.CartService; + +@Controller +@RequestMapping("/checkout") +public class CheckoutController { + + protected CartService cartService; + protected TranslationService translationService; + protected PaisesService paisesService; + protected DireccionService direccionService; + protected MessageSource messageSource; + + public CheckoutController(CartService cartService, TranslationService translationService, + PaisesService paisesService, DireccionService direccionService, MessageSource messageSource) { + this.cartService = cartService; + this.translationService = translationService; + this.paisesService = paisesService; + this.direccionService = direccionService; + this.messageSource = messageSource; + } + + @GetMapping + public String view(Model model, Principal principal, Locale locale) { + + List keys = List.of( + "app.cancelar", + "app.seleccionar", + "checkout.shipping.add.title", + "checkout.shipping.select-placeholder", + "checkout.shipping.new-address", + "app.yes", + "app.cancelar"); + + Map translations = translationService.getTranslations(locale, keys); + model.addAttribute("languageBundle", translations); + + var items = this.cartService.listItems(Utils.currentUserId(principal), locale); + for (var item : items) { + if (item.get("hasSample") != null && (Boolean) item.get("hasSample")) { + model.addAttribute("hasSample", true); + break; + } + } + model.addAttribute("items", items); + return "imprimelibros/checkout/checkout"; // crea esta vista si quieres (tabla simple) + } + + @GetMapping("/get-address/{id}") + public String getDireccionCard(@PathVariable Long id, Model model, Locale locale) { + Direccion dir = direccionService.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + model.addAttribute("pais", messageSource.getMessage("paises." + dir.getPais().getKeyword(), null, + dir.getPais().getKeyword(), locale)); + model.addAttribute("direccion", dir); + + return "imprimelibros/direcciones/direccionCard :: direccionCard(direccion=${direccion})"; + } +} diff --git a/src/main/java/com/imprimelibros/erp/common/Utils.java b/src/main/java/com/imprimelibros/erp/common/Utils.java index b6b2a79..9d41573 100644 --- a/src/main/java/com/imprimelibros/erp/common/Utils.java +++ b/src/main/java/com/imprimelibros/erp/common/Utils.java @@ -2,6 +2,7 @@ package com.imprimelibros.erp.common; import java.math.BigDecimal; import java.math.RoundingMode; +import java.security.Principal; import java.text.NumberFormat; import java.util.ArrayList; import java.util.HashMap; @@ -12,6 +13,7 @@ import java.util.Optional; import java.util.function.BiFunction; import org.springframework.context.MessageSource; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import com.fasterxml.jackson.core.JsonProcessingException; @@ -22,6 +24,8 @@ import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices; import com.imprimelibros.erp.presupuesto.marcapaginas.Marcapaginas; +import com.imprimelibros.erp.users.User; +import com.imprimelibros.erp.users.UserDetailsImpl; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Path; @@ -40,6 +44,24 @@ public class Utils { this.messageSource = messageSource; } + public static Long currentUserId(Principal principal) { + + if (principal == null) { + throw new IllegalStateException("Usuario no autenticado"); + } + + if (principal instanceof Authentication auth) { + Object principalObj = auth.getPrincipal(); + + if (principalObj instanceof UserDetailsImpl udi) { + return udi.getId(); + } else if (principalObj instanceof User u && u.getId() != null) { + return u.getId(); + } + } + throw new IllegalStateException("No se pudo obtener el ID del usuario actual"); + } + public static String formatCurrency(BigDecimal amount, Locale locale) { NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale); return currencyFormatter.format(amount); diff --git a/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java b/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java index 8993f40..cf05771 100644 --- a/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java +++ b/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java @@ -1,5 +1,6 @@ package com.imprimelibros.erp.direcciones; +import java.security.Principal; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -43,20 +44,23 @@ import jakarta.validation.Valid; @RequestMapping("/direcciones") public class DireccionController { + private final DireccionService direccionService; + protected final DireccionRepository repo; protected final PaisesService paisesService; protected final MessageSource messageSource; protected final UserDao userRepo; protected final TranslationService translationService; - public DireccionController(DireccionRepository repo, PaisesService paisesService, - MessageSource messageSource, UserDao userRepo, TranslationService translationService) { + MessageSource messageSource, UserDao userRepo, TranslationService translationService, + DireccionService direccionService) { this.repo = repo; this.paisesService = paisesService; this.messageSource = messageSource; this.userRepo = userRepo; this.translationService = translationService; + this.direccionService = direccionService; } @GetMapping() @@ -295,6 +299,33 @@ public class DireccionController { return "imprimelibros/direcciones/direccion-form :: direccionForm"; } + @GetMapping("direction-form") + public String getForm(@RequestParam(required = false) Long id, + Direccion direccion, + BindingResult binding, + Model model, + HttpServletResponse response, + Principal principal, + Locale locale) { + + model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results")); + + Direccion newDireccion = new Direccion(); + + User user = null; + if (principal instanceof UserDetailsImpl udi) { + user = new User(); + user.setId(udi.getId()); + } else if (principal instanceof User u && u.getId() != null) { + user = u; + } + newDireccion.setUser(user); + model.addAttribute("dirForm", newDireccion); + model.addAttribute("action", "/direcciones/add"); + + return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm"; + } + @PostMapping public String create( @Valid @ModelAttribute("dirForm") Direccion direccion, @@ -327,6 +358,34 @@ public class DireccionController { return null; } + // para el formulario modal en checkout + @PostMapping("/add") + public String create2( + @Valid @ModelAttribute("dirForm") Direccion direccion, + BindingResult binding, + Model model, + HttpServletResponse response, + Authentication auth, + Locale locale) { + + User current = userRepo.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(auth.getName()).orElse(null); + direccion.setUser(current); + + if (binding.hasErrors()) { + response.setStatus(422); + model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results")); + model.addAttribute("action", "/direcciones/add"); + model.addAttribute("dirForm", direccion); + return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm"; + } + + var data = direccion; + + repo.save(data); + response.setStatus(201); + return null; + } + @PostMapping("/{id}") public String update( @PathVariable Long id, @@ -416,12 +475,33 @@ public class DireccionController { } } + @GetMapping(value = "/select2", produces = "application/json") + @ResponseBody + public Map getSelect2( + @RequestParam(value = "q", required = false) String q1, + @RequestParam(value = "term", required = false) String q2, + Authentication auth) { + + boolean isAdmin = auth.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN")); + + Long currentUserId = null; + if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) { + currentUserId = udi.getId(); + } else if (auth != null) { + currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null); + } + + return direccionService.getForSelect(q1, q2, isAdmin ? null : currentUserId); + + } + private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) { if (auth == null) { return false; } boolean isAdmin = auth.getAuthorities().stream() - .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN")); if (isAdmin) { return true; } @@ -434,4 +514,5 @@ public class DireccionController { } return currentUserId != null && currentUserId.equals(ownerId); } + } diff --git a/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java new file mode 100644 index 0000000..116d3e0 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java @@ -0,0 +1,86 @@ +package com.imprimelibros.erp.direcciones; + +import java.text.Collator; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import com.imprimelibros.erp.direcciones.DireccionRepository; +import com.imprimelibros.erp.paises.Paises; + +@Service +public class DireccionService { + + protected DireccionRepository repo; + + public DireccionService(DireccionRepository repo) { + this.repo = repo; + } + + public Map getForSelect(String q1, String q2, Long userId) { + try { + + // Termino de búsqueda (Select2 usa 'q' o 'term' según versión/config) + String search = Optional.ofNullable(q1).orElse(q2); + if (search != null) { + search = search.trim(); + } + final String q = (search == null || search.isEmpty()) + ? null + : search.toLowerCase(); + + List all = userId != null ? repo.findByUserId(userId) : repo.findAll(); + + // Mapear a opciones id/text con i18n y filtrar por búsqueda si llega + List> options = all.stream() + .map(cc -> { + String id = cc.getId().toString(); + String alias = cc.getAlias(); + String direccion = cc.getDireccion(); + String cp = String.valueOf(cc.getCp()); + String ciudad = cc.getCiudad(); + String att = cc.getAtt(); + Map m = new HashMap<>(); + m.put("id", id); // lo normal en Select2: id = valor que guardarás (code3) + m.put("text", alias); // texto mostrado, i18n con fallback a keyword + m.put("cp", cp); + m.put("ciudad", ciudad); + m.put("att", att); + m.put("alias", alias); + m.put("direccion", direccion); + return m; + }) + .filter(opt -> { + if (q == null || q.isEmpty()) + return true; + String cp = opt.get("cp"); + String ciudad = opt.get("ciudad").toLowerCase(); + String att = opt.get("att").toLowerCase(); + String alias = opt.get("alias").toLowerCase(); + String text = opt.get("text").toLowerCase(); + String direccion = opt.get("direccion").toLowerCase(); + return text.contains(q) || cp.contains(q) || ciudad.contains(q) || att.contains(q) || alias.contains(q) || direccion.contains(q); + }) + .sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance())) + .collect(Collectors.toList()); + + // Estructura Select2 + Map resp = new HashMap<>(); + resp.put("results", options); + return resp; + } catch (Exception e) { + e.printStackTrace(); + return Map.of("results", List.of()); + } + } + + public Optional findById(Long id) { + return repo.findById(id); + } + +} diff --git a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java index 4e8fcb3..26bbd76 100644 --- a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java +++ b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java @@ -6,6 +6,7 @@ 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; @@ -20,6 +21,7 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion; import java.util.Map; 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; @@ -219,6 +221,49 @@ public class skApiClient { } } + 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 = (Double) responseBody.get("data"); + return Map.of("data", total); + } + } catch (JsonProcessingException e) { + e.printStackTrace(); + return Map.of("error", "Internal Server Error: 1"); // Fallback en caso de error + } + }); + + } + /****************** * PRIVATE METHODS ******************/ @@ -236,6 +281,20 @@ public class skApiClient { } } + 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 static BigDecimal calcularMargen( BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax, BigDecimal margenMax, BigDecimal margenMin) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d56bf95..2014254 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -11,8 +11,8 @@ logging.level.org.springframework=ERROR # # Database Configuration # -spring.datasource.url=jdbc:mysql://localhost:3309/imprimelibros -#spring.datasource.url=jdbc:mysql://127.0.0.1:3309/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid&characterEncoding=utf8 +#spring.datasource.url=jdbc:mysql://localhost:3309/imprimelibros +spring.datasource.url=jdbc:mysql://127.0.0.1:3309/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid&characterEncoding=utf8 spring.datasource.username=imprimelibros_user spring.datasource.password=om91irrDctd spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver @@ -24,8 +24,8 @@ spring.jpa.show-sql=false # # Safekat API Configuration # -safekat.api.url=http://localhost:8000/ -#safekat.api.url=https://erp-dev.safekat.es/ +#safekat.api.url=http://localhost:8000/ +safekat.api.url=https://erp-dev.safekat.es/ safekat.api.email=imnavajas@coit.es safekat.api.password=Safekat2024 diff --git a/src/main/resources/i18n/app_es.properties b/src/main/resources/i18n/app_es.properties index 8e2de87..4ce7940 100644 --- a/src/main/resources/i18n/app_es.properties +++ b/src/main/resources/i18n/app_es.properties @@ -3,6 +3,7 @@ app.yes=Sí app.no=No app.aceptar=Aceptar app.cancelar=Cancelar +app.seleccionar=Seleccionar app.guardar=Guardar app.editar=Editar app.add=Añadir diff --git a/src/main/resources/i18n/direcciones_es.properties b/src/main/resources/i18n/direcciones_es.properties index bc30637..6e3ee47 100644 --- a/src/main/resources/i18n/direcciones_es.properties +++ b/src/main/resources/i18n/direcciones_es.properties @@ -53,5 +53,7 @@ direcciones.error.delete-internal-error=Error interno al eliminar la dirección. direcciones.error.noEncontrado=Dirección no encontrada. direcciones.error.sinPermiso=No tiene permiso para realizar esta acción. +direcciones.error.noShippingCost=No se pudo calcular el coste de envío para la dirección proporcionada. + direcciones.form.error.required=Campo obligatorio. diff --git a/src/main/resources/i18n/pedidos_en.properties b/src/main/resources/i18n/pedidos_en.properties new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/i18n/pedidos_es.properties b/src/main/resources/i18n/pedidos_es.properties new file mode 100644 index 0000000..c23bb54 --- /dev/null +++ b/src/main/resources/i18n/pedidos_es.properties @@ -0,0 +1,17 @@ +checkout.title=Finalizar compra +checkout.summay=Resumen de la compra +checkout.shipping=Envío +checkout.payment=Método de pago + +checkout.shipping.info=Todos los pedidos incluyen un envío gratuito a la Península y Baleares por línea de pedido. +checkout.shipping.order=Envío del pedido +checkout.shipping.samples=Envío de pruebas +checkout.shipping.onlyOneShipping=Todo el pedido se envía a una única dirección. +checkout.shipping.add=Añadir dirección +checkout.shipping.add.title=Seleccione una dirección +checkout.shipping.select-placeholder=Buscar en direcciones... +checkout.shipping.new-address=Nueva dirección + +checkout.summary.presupuesto=#Presupuesto +checkout.summary.titulo=Título +checkout.summary.base=Base \ No newline at end of file diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js b/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js new file mode 100644 index 0000000..ea7c10b --- /dev/null +++ b/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js @@ -0,0 +1,193 @@ +$(() => { + + const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content'); + const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content'); + if (window.$ && csrfToken && csrfHeader) { + $.ajaxSetup({ + beforeSend: function (xhr) { + xhr.setRequestHeader(csrfHeader, csrfToken); + } + }); + } + + const language = document.documentElement.lang || 'es-ES'; + const modalEl = document.getElementById('direccionFormModal'); + const modal = bootstrap.Modal.getOrCreateInstance(modalEl); + + + $("#onlyOneShipping").on('change', function () { + if ($(this).is(':checked')) { + $('#shippingAddressesContainer').empty().show(); + $('#shippingMultipleAddressesContainer').show(); + } else { + $('#shippingAddressesContainer').hide(); + $('#shippingMultipleAddressesContainer').empty().hide(); + } + }); + + $('#addOrderAddress').on('click', () => { + if ($('#onlyOneShipping').is(':checked')) { + seleccionarDireccionEnvio(); + } else { + // Add address to multiple shipping addresses container + } + }); + + async function seleccionarDireccionEnvio() { + + const { value: direccionId, isDenied } = await Swal.fire({ + title: window.languageBundle['checkout.shipping.add.title'] || 'Seleccione una dirección', + html: ` + + `, + showCancelButton: true, + showDenyButton: true, + buttonsStyling: false, + confirmButtonText: window.languageBundle['app.seleccionar'] || 'Seleccionar', + cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar', + denyButtonText: window.languageBundle['checkout.shipping.new-address'] || 'Nueva dirección', + customClass: { + confirmButton: 'btn btn-secondary me-2', + cancelButton: 'btn btn-light', + denyButton: 'btn btn-secondary me-2' + }, + focusConfirm: false, + + // Inicializa cuando el DOM del modal ya existe + didOpen: () => { + const $select = $('#direccionSelect'); + $select.empty(); // limpia placeholder estático + + $select.select2({ + width: '100%', + dropdownParent: $('.swal2-container'), + ajax: { + url: '/direcciones/select2', + dataType: 'json', + delay: 250, + data: params => ({ q: params.term || '' }), + processResults: (data) => { + const items = Array.isArray(data) ? data : (data.results || []); + return { + results: items.map(item => ({ + id: item.id, + text: item.text, // ← Select2 necesita 'id' y 'text' + alias: item.alias || 'Sin alias', + att: item.att || '', + direccion: item.direccion || '', + cp: item.cp || '', + ciudad: item.ciudad || '', + html: ` +
+ ${item.alias || 'Sin alias'}
+ ${item.att ? `${item.att}
` : ''} + ${item.direccion || ''}${item.cp ? ', ' + item.cp : ''}${item.ciudad ? ', ' + item.ciudad : ''} +
+ ` + })), + pagination: { more: false } // opcional, evita que espere más páginas + }; + } + + }, + placeholder: window.languageBundle['checkout.shipping.select-placeholder'] || 'Buscar en direcciones...', + language: language, + + templateResult: data => { + if (data.loading) return data.text; + return $(data.html || data.text); + }, + // Selección más compacta (solo alias + ciudad) + templateSelection: data => { + if (!data.id) return data.text; + const alias = data.alias || data.text; + const ciudad = data.ciudad ? ` — ${data.ciudad}` : ''; + return $(`${alias}${ciudad}`); + }, + escapeMarkup: m => m + }); + + // (Opcional) Prefijar valor si ya tienes una dirección elegida: + // const preselected = { id: '123', text: 'Oficina Central — Madrid' }; + // const option = new Option(preselected.text, preselected.id, true, true); + // $select.append(option).trigger('change'); + }, + + preConfirm: () => { + const $select = $('#direccionSelect'); + const val = $select.val(); + if (!val) { + Swal.showValidationMessage( + lang.startsWith('es') ? 'Debes seleccionar una dirección' : 'You must select an address' + ); + return false; + } + return val; + }, + + didClose: () => { + // Limpieza: destruir select2 para evitar fugas + const $select = $('#direccionSelect'); + if ($select.data('select2')) { + $select.select2('destroy'); + } + } + }); + + if (isDenied) { + $.get('/direcciones/direction-form', function (html) { + $('#direccionFormModalBody').html(html); + const title = $('#direccionFormModalBody #direccionForm').data('add'); + $('#direccionFormModal .modal-title').text(title); + modal.show(); + }); + } + + if (direccionId) { + // Obtén el objeto completo seleccionado + const response = await fetch(`/checkout/get-address/${direccionId}`); + if (response.ok) { + const html = await response.text(); + $('#shippingAddressesContainer').html(html); + } + } + } + + $(document).on('submit', '#direccionForm', function (e) { + e.preventDefault(); + const $form = $(this); + + $.ajax({ + url: $form.attr('action'), + type: 'POST', // PUT simulado via _method + data: $form.serialize(), + dataType: 'html', + success: function (html) { + // Si por cualquier motivo llega 200 con fragmento, lo insertamos igual + if (typeof html === 'string' && html.indexOf('id="direccionForm"') !== -1 && html.indexOf(' 0; + const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add'); + $('#direccionModal .modal-title').text(title); + return; + } + // Éxito real: cerrar y recargar tabla + modal.hide(); + seleccionarDireccionEnvio(); + }, + error: function (xhr) { + // Con 422 devolvemos el fragmento con errores aquí + if (xhr.status === 422 && xhr.responseText) { + $('#direccionFormModalBody').html(xhr.responseText); + const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 0; + const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add'); + $('#direccionModal .modal-title').text(title); + initSelect2Cliente(true); + return; + } + // Fallback + $('#direccionFormModalBody').html('
Error inesperado.
'); + } + }); + }); +}); diff --git a/src/main/resources/templates/imprimelibros/cart/cart.html b/src/main/resources/templates/imprimelibros/cart/cart.html index d54ce93..4cee29e 100644 --- a/src/main/resources/templates/imprimelibros/cart/cart.html +++ b/src/main/resources/templates/imprimelibros/cart/cart.html @@ -36,7 +36,28 @@
+ +
+
+
+

+
+ + +
+ + +
+ +
+
+
@@ -67,13 +88,14 @@ : - + @@ -83,7 +105,8 @@ diff --git a/src/main/resources/templates/imprimelibros/checkout/_envio.html b/src/main/resources/templates/imprimelibros/checkout/_envio.html new file mode 100644 index 0000000..15687fa --- /dev/null +++ b/src/main/resources/templates/imprimelibros/checkout/_envio.html @@ -0,0 +1,43 @@ +
+
+
+
+
+
Envio del pedido +
+
+
+
+

+
+ + +
+ + +
+
+ +
+
+
+ +
+
+
Envio de pruebas +
+
+ +
+ +
+
+ + +
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/checkout/_pago.html b/src/main/resources/templates/imprimelibros/checkout/_pago.html new file mode 100644 index 0000000..42eb8c5 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/checkout/_pago.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/checkout/checkout.html b/src/main/resources/templates/imprimelibros/checkout/checkout.html new file mode 100644 index 0000000..5763197 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/checkout/checkout.html @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ +
+ +
+
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+ +
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PresupuestoTítulo + Base
+ PRESUPUESTO-001 + + Título del presupuesto + + + 0,00 + + +
:
:
: + + +
+ +
+ +
+
+ + +
+ + +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/direcciones/direccion-form-fixed-user.html b/src/main/resources/templates/imprimelibros/direcciones/direccion-form-fixed-user.html new file mode 100644 index 0000000..6980f89 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/direcciones/direccion-form-fixed-user.html @@ -0,0 +1,169 @@ +
+
+ +
+ Error +
+ + + +
+ + +
+ +
+ +
+ + +
+
+ +
+ + +
+
+ +
+
+ + +
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ +
+ +
+
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/direcciones/direccion-form.html b/src/main/resources/templates/imprimelibros/direcciones/direccion-form.html index d124b90..79e2969 100644 --- a/src/main/resources/templates/imprimelibros/direcciones/direccion-form.html +++ b/src/main/resources/templates/imprimelibros/direcciones/direccion-form.html @@ -103,6 +103,7 @@
diff --git a/src/main/resources/templates/imprimelibros/direcciones/direccionCard.html b/src/main/resources/templates/imprimelibros/direcciones/direccionCard.html new file mode 100644 index 0000000..157a45e --- /dev/null +++ b/src/main/resources/templates/imprimelibros/direcciones/direccionCard.html @@ -0,0 +1,18 @@ +
+
+
+
Alias
+ +

Att

+ +

Calle

+

CP Ciudad

+

País

+
+
+
diff --git a/src/main/resources/templates/imprimelibros/login/login.html b/src/main/resources/templates/imprimelibros/login/login.html index e3f3cda..1b2f168 100644 --- a/src/main/resources/templates/imprimelibros/login/login.html +++ b/src/main/resources/templates/imprimelibros/login/login.html @@ -6,6 +6,9 @@ + + + diff --git a/src/test/java/com/imprimelibros/erp/calcularEnvios.java b/src/test/java/com/imprimelibros/erp/calcularEnvios.java new file mode 100644 index 0000000..c3364b1 --- /dev/null +++ b/src/test/java/com/imprimelibros/erp/calcularEnvios.java @@ -0,0 +1,44 @@ +package com.imprimelibros.erp; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import java.util.Locale; + +import com.imprimelibros.erp.externalApi.skApiClient; + +@SpringBootTest +public class calcularEnvios { + @Autowired + private skApiClient apiClient; + + @Test + void testPrecioCalculadoDevuelveJson() { + + Map data = new HashMap<>(); + data.put("pais_code3", "esp"); + data.put("cp", 18200); + data.put("peso", 7.82); + data.put("unidades", 10); + data.put("palets", 0); + + + // get locale + Locale locale = Locale.forLanguageTag("es-ES"); + + + Map resultado = apiClient.getCosteEnvio(data, locale); + + System.out.println("📦 Resultado de la API:"); + System.out.println(resultado); + + assertNotNull(resultado, "El resultado no debe ser null"); + /*assertTrue(resultado.trim().startsWith("{"), "El resultado debe comenzar con { (JSON)"); + assertTrue(resultado.trim().endsWith("}"), "El resultado debe terminar con } (JSON)");*/ + } +} From 9dad59fe16fa4feacec140b0facdc70e0254cfc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Mon, 27 Oct 2025 20:30:38 +0100 Subject: [PATCH 02/11] modificando carrito --- src/main/resources/templates/imprimelibros/cart/cart.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/templates/imprimelibros/cart/cart.html b/src/main/resources/templates/imprimelibros/cart/cart.html index 4cee29e..2105cef 100644 --- a/src/main/resources/templates/imprimelibros/cart/cart.html +++ b/src/main/resources/templates/imprimelibros/cart/cart.html @@ -57,7 +57,7 @@
- +
From cecc0c0ea0c299585b8e4bca0e7f39bc0fbe1a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Tue, 28 Oct 2025 08:17:15 +0100 Subject: [PATCH 03/11] =?UTF-8?q?modificando=20carro=20para=20a=C3=B1adir?= =?UTF-8?q?=20los=20nav=20tabs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/i18n/cart_es.properties | 3 + .../pages/imprimelibros/cart/shipping-cart.js | 27 ++++ .../imprimelibros/cart/_cartItem.html | 132 +++++++++++------- .../templates/imprimelibros/cart/cart.html | 2 +- 4 files changed, 110 insertions(+), 54 deletions(-) create mode 100644 src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js diff --git a/src/main/resources/i18n/cart_es.properties b/src/main/resources/i18n/cart_es.properties index 4fff994..19f0a27 100644 --- a/src/main/resources/i18n/cart_es.properties +++ b/src/main/resources/i18n/cart_es.properties @@ -4,6 +4,9 @@ cart.empty=Tu cesta de la compra está vacía. cart.item.presupuesto-numero=Presupuesto # cart.precio=Precio +cart.tabs.details=Detalles +cart.tabs.envio=Envío + cart.resumen.title=Resumen de la cesta cart.resumen.base=Base imponible: cart.resumen.iva-4=IVA 4%: diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js b/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js new file mode 100644 index 0000000..e0ef8de --- /dev/null +++ b/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js @@ -0,0 +1,27 @@ +$(() => { + + const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content'); + const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content'); + if (window.$ && csrfToken && csrfHeader) { + $.ajaxSetup({ + beforeSend: function (xhr) { + xhr.setRequestHeader(csrfHeader, csrfToken); + } + }); + } + + const language = document.documentElement.lang || 'es-ES'; + const modalEl = document.getElementById('direccionFormModal'); + const modal = bootstrap.Modal.getOrCreateInstance(modalEl); + + + $("#onlyOneShipping").on('change', function () { + if ($(this).is(':checked')) { + $('#shippingAddressesContainer').empty().show(); + $('#shippingMultipleAddressesContainer').show(); + } else { + $('#shippingAddressesContainer').hide(); + $('#shippingMultipleAddressesContainer').empty().hide(); + } + }); +}); \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/cart/_cartItem.html b/src/main/resources/templates/imprimelibros/cart/_cartItem.html index 5ee3a2c..2ab4390 100644 --- a/src/main/resources/templates/imprimelibros/cart/_cartItem.html +++ b/src/main/resources/templates/imprimelibros/cart/_cartItem.html @@ -1,71 +1,97 @@ -
-
- -
-
- portada -
-
- - -
- -
- - Presupuesto - -
-
- Presupuesto # - # -
- - -
    -
    -
  • -
    -
  • + +
    +
    +
    +
    + portada
    -
+
-
    - Servicios adicionales - -
+ +
+ +
+ + Presupuesto + +
+
+ Presupuesto # + # +
-
    +
    +
  • +
    +
  • +
    +
+ +
    + Servicios adicionales + +
+ +
    -
  • - Datos de maquetación: - -
  • -
+
  • + Datos de maquetación: + +
  • + -
      -
    • - Datos de marcapáginas: - -
    • -
    +
  • + Datos de marcapáginas: + +
  • + +
    + + +
    +

    Precio

    +
    + 0,00 +
    +
    +
    +
    +
    - -
    -

    Precio

    -
    - 0,00 -
    diff --git a/src/main/resources/templates/imprimelibros/cart/cart.html b/src/main/resources/templates/imprimelibros/cart/cart.html index 2105cef..d43954e 100644 --- a/src/main/resources/templates/imprimelibros/cart/cart.html +++ b/src/main/resources/templates/imprimelibros/cart/cart.html @@ -53,7 +53,7 @@ -
    +
    From f770bd07d6d3a558f23c85ce64cc7c40476d7e16 Mon Sep 17 00:00:00 2001 From: jjimenez Date: Tue, 28 Oct 2025 14:05:22 +0100 Subject: [PATCH 04/11] =?UTF-8?q?direccion=20unica=20a=C3=B1adida?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../erp/cart/CartController.java | 44 +++- .../erp/checkout/CheckoutController.java | 11 - .../erp/config/SecurityConfig.java | 1 - src/main/resources/application.properties | 4 + src/main/resources/i18n/cart_es.properties | 11 + src/main/resources/i18n/pedidos_es.properties | 5 +- src/main/resources/static/assets/css/cart.css | 33 +++ .../pages/imprimelibros/cart/shipping-cart.js | 219 ++++++++++++++++-- .../pages/imprimelibros/checkout/checkout.js | 176 +------------- .../imprimelibros/cart/_cartItem.html | 39 +++- .../templates/imprimelibros/cart/cart.html | 22 +- .../direcciones/direccionCard.html | 33 +-- 12 files changed, 357 insertions(+), 241 deletions(-) create mode 100644 src/main/resources/static/assets/css/cart.css diff --git a/src/main/java/com/imprimelibros/erp/cart/CartController.java b/src/main/java/com/imprimelibros/erp/cart/CartController.java index 9692451..6ffd926 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartController.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartController.java @@ -3,13 +3,20 @@ package com.imprimelibros.erp.cart; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.context.MessageSource; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import com.imprimelibros.erp.common.Utils; +import com.imprimelibros.erp.direcciones.Direccion; +import com.imprimelibros.erp.direcciones.DireccionService; +import com.imprimelibros.erp.i18n.TranslationService; import java.security.Principal; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -17,18 +24,38 @@ import java.util.Map; @RequestMapping("/cart") public class CartController { - private final CartService service; + protected final CartService service; + protected DireccionService direccionService; + protected MessageSource messageSource; + protected TranslationService translationService; - public CartController(CartService service) { + public CartController(CartService service, DireccionService direccionService, MessageSource messageSource, TranslationService translationService) { this.service = service; + this.direccionService = direccionService; + this.messageSource = messageSource; + this.translationService = translationService; } - /** Vista del carrito */ @GetMapping public String viewCart(Model model, Principal principal, Locale locale) { + + List keys = List.of( + "app.cancelar", + "app.seleccionar", + "cart.shipping.add.title", + "cart.shipping.select-placeholder", + "cart.shipping.new-address", + "cart.shipping.errors.noAddressSelected", + "app.yes", + "app.cancelar"); + + Map translations = translationService.getTranslations(locale, keys); + model.addAttribute("languageBundle", translations); + var items = service.listItems(Utils.currentUserId(principal), locale); model.addAttribute("items", items); + model.addAttribute("cartId", service.getOrCreateActiveCart(Utils.currentUserId(principal))); return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple) } @@ -82,4 +109,15 @@ public class CartController { service.clear(Utils.currentUserId(principal)); return "redirect:/cart"; } + + @GetMapping("/get-address/{id}") + public String getDireccionCard(@PathVariable Long id, Model model, Locale locale) { + Direccion dir = direccionService.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + model.addAttribute("pais", messageSource.getMessage("paises." + dir.getPais().getKeyword(), null, + dir.getPais().getKeyword(), locale)); + model.addAttribute("direccion", dir); + + return "imprimelibros/direcciones/direccionCard :: direccionCard(direccion=${direccion})"; + } } diff --git a/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java index db83787..c8a3810 100644 --- a/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java +++ b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java @@ -72,15 +72,4 @@ public class CheckoutController { model.addAttribute("items", items); return "imprimelibros/checkout/checkout"; // crea esta vista si quieres (tabla simple) } - - @GetMapping("/get-address/{id}") - public String getDireccionCard(@PathVariable Long id, Model model, Locale locale) { - Direccion dir = direccionService.findById(id) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - model.addAttribute("pais", messageSource.getMessage("paises." + dir.getPais().getKeyword(), null, - dir.getPais().getKeyword(), locale)); - model.addAttribute("direccion", dir); - - return "imprimelibros/direcciones/direccionCard :: direccionCard(direccion=${direccion})"; - } } diff --git a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java index 392b416..096488d 100644 --- a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java +++ b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java @@ -91,7 +91,6 @@ public class SecurityConfig { // Ignora CSRF para tu recurso público (sin Ant/Mvc matchers) .csrf(csrf -> csrf .ignoringRequestMatchers(pathStartsWith("/presupuesto/public/"))) - // ====== RequestCache: sólo navegaciones HTML reales ====== .requestCache(rc -> { HttpSessionRequestCache cache = new HttpSessionRequestCache(); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2014254..321a344 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,9 @@ spring.application.name=erp +server.forward-headers-strategy=framework +server.servlet.session.cookie.secure=true + + # # Logging # diff --git a/src/main/resources/i18n/cart_es.properties b/src/main/resources/i18n/cart_es.properties index 19f0a27..d8b52b3 100644 --- a/src/main/resources/i18n/cart_es.properties +++ b/src/main/resources/i18n/cart_es.properties @@ -7,6 +7,17 @@ cart.precio=Precio cart.tabs.details=Detalles cart.tabs.envio=Envío +cart.shipping.add=Añadir dirección +cart.shipping.add.title=Seleccione una dirección +cart.shipping.select-placeholder=Buscar en direcciones... +cart.shipping.new-address=Nueva dirección +cart.shipping.info=Todos los pedidos incluyen un envío gratuito a la Península y Baleares por línea de pedido. +cart.shipping.order=Envío del pedido +cart.shipping.samples=Envío de pruebas +cart.shipping.onlyOneShipping=Todo el pedido se envía a una única dirección. + +cart.shipping.errors.noAddressSelected=Debe seleccionar una dirección de envío para el pedido. + cart.resumen.title=Resumen de la cesta cart.resumen.base=Base imponible: cart.resumen.iva-4=IVA 4%: diff --git a/src/main/resources/i18n/pedidos_es.properties b/src/main/resources/i18n/pedidos_es.properties index c23bb54..7cbfc47 100644 --- a/src/main/resources/i18n/pedidos_es.properties +++ b/src/main/resources/i18n/pedidos_es.properties @@ -7,10 +7,7 @@ checkout.shipping.info=Todos los pedidos incluyen un envío gratuito a la Penín checkout.shipping.order=Envío del pedido checkout.shipping.samples=Envío de pruebas checkout.shipping.onlyOneShipping=Todo el pedido se envía a una única dirección. -checkout.shipping.add=Añadir dirección -checkout.shipping.add.title=Seleccione una dirección -checkout.shipping.select-placeholder=Buscar en direcciones... -checkout.shipping.new-address=Nueva dirección + checkout.summary.presupuesto=#Presupuesto checkout.summary.titulo=Título diff --git a/src/main/resources/static/assets/css/cart.css b/src/main/resources/static/assets/css/cart.css new file mode 100644 index 0000000..dd4d314 --- /dev/null +++ b/src/main/resources/static/assets/css/cart.css @@ -0,0 +1,33 @@ +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + background-color: #4c5c6366 !important; +} + +.step-arrow-nav .nav { + background-color: #4c5c6333 !important; +} + +.nav-link.active .bg-soft-primary { + background-color: #ffffff33 !important; + /* #4c5c63 al 20% */ +} + +.nav-link.active .text-primary { + color: #ffffff !important; + /* #4c5c63 al 20% */ +} + +.nav-link:not(.active) .bg-soft-primary { + background-color: #4c5c6366 !important; + /* #4c5c63 al 20% */ +} + +.nav-link:not(.active) .text-primary { + color: #000000 !important; + /* #4c5c63 al 20% */ +} + +.direccion-card { + flex: 1 1 250px; /* ancho mínimo 250px, crece si hay espacio */ + max-width: 250px; /* opcional, para que no se estiren demasiado */ + min-width: 240px; /* protege el ancho mínimo */ +} \ No newline at end of file diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js b/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js index e0ef8de..52a8d1a 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js @@ -1,27 +1,214 @@ $(() => { - const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content'); - const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content'); - if (window.$ && csrfToken && csrfHeader) { - $.ajaxSetup({ - beforeSend: function (xhr) { - xhr.setRequestHeader(csrfHeader, csrfToken); - } - }); - } + $("#onlyOneShipping").on('change', function () { + if ($(this).is(':checked')) { + $('.nav-product').hide(); + document.querySelectorAll('.card.product').forEach(card => { + const detailsBtn = card.querySelector('.nav-link[id^="pills-details-"][id$="-tab"]'); + if (detailsBtn) new bootstrap.Tab(detailsBtn).show(); + }); + $('#shippingAddressesContainer').empty().show(); + $('.div-shipping-product').show(); + $('#addOrderAddress').show(); + } else { + $('.nav-product').show(); + $('#shippingAddressesContainer').hide(); + $('.div-shipping-product').empty().hide(); + $('#addOrderAddress').hide(); + } + }); + + $('#shippingAddressesContainer').on('click', '.btn-delete-direccion', function (e) { + e.preventDefault(); + const $card = $(this).closest('.direccion-card'); + $card.remove(); + $('#addOrderAddress').show(); + }); const language = document.documentElement.lang || 'es-ES'; const modalEl = document.getElementById('direccionFormModal'); const modal = bootstrap.Modal.getOrCreateInstance(modalEl); - - $("#onlyOneShipping").on('change', function () { - if ($(this).is(':checked')) { - $('#shippingAddressesContainer').empty().show(); - $('#shippingMultipleAddressesContainer').show(); + $(document).on("change", ".direccionFacturacion", function () { + const isChecked = $(this).is(':checked'); + if (isChecked) { + $('.direccionFacturacionItems').removeClass('d-none'); } else { - $('#shippingAddressesContainer').hide(); - $('#shippingMultipleAddressesContainer').empty().hide(); + $('.direccionFacturacionItems').addClass('d-none'); + $('#razonSocial').val(''); + $('#tipoIdentificacionFiscal').val('DNI'); + $('#identificacionFiscal').val(''); } }); + + $('#addOrderAddress').on('click', () => { + if ($('#onlyOneShipping').is(':checked')) { + if(seleccionarDireccionEnvio()){ + $('#addOrderAddress').hide(); + } + } else { + // Add address to multiple shipping addresses container + } + }); + + async function seleccionarDireccionEnvio() { + + const { value: direccionId, isDenied } = await Swal.fire({ + title: window.languageBundle['cart.shipping.add.title'] || 'Seleccione una dirección', + html: ` + + `, + showCancelButton: true, + showDenyButton: true, + buttonsStyling: false, + confirmButtonText: window.languageBundle['app.seleccionar'] || 'Seleccionar', + cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar', + denyButtonText: window.languageBundle['cart.shipping.new-address'] || 'Nueva dirección', + customClass: { + confirmButton: 'btn btn-secondary me-2', + cancelButton: 'btn btn-light', + denyButton: 'btn btn-secondary me-2' + }, + focusConfirm: false, + + // Inicializa cuando el DOM del modal ya existe + didOpen: () => { + const $select = $('#direccionSelect'); + $select.empty(); // limpia placeholder estático + + $select.select2({ + width: '100%', + dropdownParent: $('.swal2-container'), + ajax: { + url: '/direcciones/select2', + dataType: 'json', + delay: 250, + data: params => ({ q: params.term || '' }), + processResults: (data) => { + const items = Array.isArray(data) ? data : (data.results || []); + return { + results: items.map(item => ({ + id: item.id, + text: item.text, // ← Select2 necesita 'id' y 'text' + alias: item.alias || 'Sin alias', + att: item.att || '', + direccion: item.direccion || '', + cp: item.cp || '', + ciudad: item.ciudad || '', + html: ` +
    + ${item.alias || 'Sin alias'}
    + ${item.att ? `${item.att}
    ` : ''} + ${item.direccion || ''}${item.cp ? ', ' + item.cp : ''}${item.ciudad ? ', ' + item.ciudad : ''} +
    + ` + })), + pagination: { more: false } // opcional, evita que espere más páginas + }; + } + + }, + placeholder: window.languageBundle['cart.shipping.select-placeholder'] || 'Buscar en direcciones...', + language: language, + + templateResult: data => { + if (data.loading) return data.text; + return $(data.html || data.text); + }, + // Selección más compacta (solo alias + ciudad) + templateSelection: data => { + if (!data.id) return data.text; + const alias = data.alias || data.text; + const ciudad = data.ciudad ? ` — ${data.ciudad}` : ''; + return $(`${alias}${ciudad}`); + }, + escapeMarkup: m => m + }); + + // (Opcional) Prefijar valor si ya tienes una dirección elegida: + // const preselected = { id: '123', text: 'Oficina Central — Madrid' }; + // const option = new Option(preselected.text, preselected.id, true, true); + // $select.append(option).trigger('change'); + }, + + preConfirm: () => { + const $select = $('#direccionSelect'); + const val = $select.val(); + if (!val) { + Swal.showValidationMessage( + window.languageBundle['cart.shipping.errors.noAddressSelected'] || 'Por favor, seleccione una dirección.' + ); + return false; + } + return val; + }, + + didClose: () => { + // Limpieza: destruir select2 para evitar fugas + const $select = $('#direccionSelect'); + if ($select.data('select2')) { + $select.select2('destroy'); + } + } + }); + + if (isDenied) { + $.get('/direcciones/direction-form', function (html) { + $('#direccionFormModalBody').html(html); + const title = $('#direccionFormModalBody #direccionForm').data('add'); + $('#direccionFormModal .modal-title').text(title); + modal.show(); + }); + } + + if (direccionId) { + // Obtén el objeto completo seleccionado + const response = await fetch(`/cart/get-address/${direccionId}`); + if (response.ok) { + const html = await response.text(); + $('#shippingAddressesContainer').append(html); + return true; + } + return false; + } + return false; + } + + $(document).on('submit', '#direccionForm', function (e) { + e.preventDefault(); + const $form = $(this); + + $.ajax({ + url: $form.attr('action'), + type: 'POST', // PUT simulado via _method + data: $form.serialize(), + dataType: 'html', + success: function (html) { + // Si por cualquier motivo llega 200 con fragmento, lo insertamos igual + if (typeof html === 'string' && html.indexOf('id="direccionForm"') !== -1 && html.indexOf(' 0; + const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add'); + $('#direccionModal .modal-title').text(title); + return; + } + // Éxito real: cerrar y recargar tabla + modal.hide(); + seleccionarDireccionEnvio(); + }, + error: function (xhr) { + // Con 422 devolvemos el fragmento con errores aquí + if (xhr.status === 422 && xhr.responseText) { + $('#direccionFormModalBody').html(xhr.responseText); + const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 0; + const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add'); + $('#direccionModal .modal-title').text(title); + initSelect2Cliente(true); + return; + } + // Fallback + $('#direccionFormModalBody').html('
    Error inesperado.
    '); + } + }); + }); }); \ No newline at end of file diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js b/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js index ea7c10b..3aa8e86 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js @@ -15,179 +15,7 @@ $(() => { const modal = bootstrap.Modal.getOrCreateInstance(modalEl); - $("#onlyOneShipping").on('change', function () { - if ($(this).is(':checked')) { - $('#shippingAddressesContainer').empty().show(); - $('#shippingMultipleAddressesContainer').show(); - } else { - $('#shippingAddressesContainer').hide(); - $('#shippingMultipleAddressesContainer').empty().hide(); - } - }); + - $('#addOrderAddress').on('click', () => { - if ($('#onlyOneShipping').is(':checked')) { - seleccionarDireccionEnvio(); - } else { - // Add address to multiple shipping addresses container - } - }); - - async function seleccionarDireccionEnvio() { - - const { value: direccionId, isDenied } = await Swal.fire({ - title: window.languageBundle['checkout.shipping.add.title'] || 'Seleccione una dirección', - html: ` - - `, - showCancelButton: true, - showDenyButton: true, - buttonsStyling: false, - confirmButtonText: window.languageBundle['app.seleccionar'] || 'Seleccionar', - cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar', - denyButtonText: window.languageBundle['checkout.shipping.new-address'] || 'Nueva dirección', - customClass: { - confirmButton: 'btn btn-secondary me-2', - cancelButton: 'btn btn-light', - denyButton: 'btn btn-secondary me-2' - }, - focusConfirm: false, - - // Inicializa cuando el DOM del modal ya existe - didOpen: () => { - const $select = $('#direccionSelect'); - $select.empty(); // limpia placeholder estático - - $select.select2({ - width: '100%', - dropdownParent: $('.swal2-container'), - ajax: { - url: '/direcciones/select2', - dataType: 'json', - delay: 250, - data: params => ({ q: params.term || '' }), - processResults: (data) => { - const items = Array.isArray(data) ? data : (data.results || []); - return { - results: items.map(item => ({ - id: item.id, - text: item.text, // ← Select2 necesita 'id' y 'text' - alias: item.alias || 'Sin alias', - att: item.att || '', - direccion: item.direccion || '', - cp: item.cp || '', - ciudad: item.ciudad || '', - html: ` -
    - ${item.alias || 'Sin alias'}
    - ${item.att ? `${item.att}
    ` : ''} - ${item.direccion || ''}${item.cp ? ', ' + item.cp : ''}${item.ciudad ? ', ' + item.ciudad : ''} -
    - ` - })), - pagination: { more: false } // opcional, evita que espere más páginas - }; - } - - }, - placeholder: window.languageBundle['checkout.shipping.select-placeholder'] || 'Buscar en direcciones...', - language: language, - - templateResult: data => { - if (data.loading) return data.text; - return $(data.html || data.text); - }, - // Selección más compacta (solo alias + ciudad) - templateSelection: data => { - if (!data.id) return data.text; - const alias = data.alias || data.text; - const ciudad = data.ciudad ? ` — ${data.ciudad}` : ''; - return $(`${alias}${ciudad}`); - }, - escapeMarkup: m => m - }); - - // (Opcional) Prefijar valor si ya tienes una dirección elegida: - // const preselected = { id: '123', text: 'Oficina Central — Madrid' }; - // const option = new Option(preselected.text, preselected.id, true, true); - // $select.append(option).trigger('change'); - }, - - preConfirm: () => { - const $select = $('#direccionSelect'); - const val = $select.val(); - if (!val) { - Swal.showValidationMessage( - lang.startsWith('es') ? 'Debes seleccionar una dirección' : 'You must select an address' - ); - return false; - } - return val; - }, - - didClose: () => { - // Limpieza: destruir select2 para evitar fugas - const $select = $('#direccionSelect'); - if ($select.data('select2')) { - $select.select2('destroy'); - } - } - }); - - if (isDenied) { - $.get('/direcciones/direction-form', function (html) { - $('#direccionFormModalBody').html(html); - const title = $('#direccionFormModalBody #direccionForm').data('add'); - $('#direccionFormModal .modal-title').text(title); - modal.show(); - }); - } - - if (direccionId) { - // Obtén el objeto completo seleccionado - const response = await fetch(`/checkout/get-address/${direccionId}`); - if (response.ok) { - const html = await response.text(); - $('#shippingAddressesContainer').html(html); - } - } - } - - $(document).on('submit', '#direccionForm', function (e) { - e.preventDefault(); - const $form = $(this); - - $.ajax({ - url: $form.attr('action'), - type: 'POST', // PUT simulado via _method - data: $form.serialize(), - dataType: 'html', - success: function (html) { - // Si por cualquier motivo llega 200 con fragmento, lo insertamos igual - if (typeof html === 'string' && html.indexOf('id="direccionForm"') !== -1 && html.indexOf(' 0; - const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add'); - $('#direccionModal .modal-title').text(title); - return; - } - // Éxito real: cerrar y recargar tabla - modal.hide(); - seleccionarDireccionEnvio(); - }, - error: function (xhr) { - // Con 422 devolvemos el fragmento con errores aquí - if (xhr.status === 422 && xhr.responseText) { - $('#direccionFormModalBody').html(xhr.responseText); - const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 0; - const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add'); - $('#direccionModal .modal-title').text(title); - initSelect2Cliente(true); - return; - } - // Fallback - $('#direccionFormModalBody').html('
    Error inesperado.
    '); - } - }); - }); + }); diff --git a/src/main/resources/templates/imprimelibros/cart/_cartItem.html b/src/main/resources/templates/imprimelibros/cart/_cartItem.html index 2ab4390..209e36a 100644 --- a/src/main/resources/templates/imprimelibros/cart/_cartItem.html +++ b/src/main/resources/templates/imprimelibros/cart/_cartItem.html @@ -3,27 +3,29 @@ data-iva-21=${item.iva21}, data-base=${item.base}">
    - @@ -143,7 +145,12 @@
    +
    + +
    +
    diff --git a/src/main/resources/templates/imprimelibros/cart/_cartSummary.html b/src/main/resources/templates/imprimelibros/cart/_cartSummary.html new file mode 100644 index 0000000..3caadb7 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/cart/_cartSummary.html @@ -0,0 +1,48 @@ +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + +
    :
    :
    : + +
    + +
    + +
    +
    + + +
    + + +
    \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/cart/cart.html b/src/main/resources/templates/imprimelibros/cart/cart.html index c8235fd..0170d5b 100644 --- a/src/main/resources/templates/imprimelibros/cart/cart.html +++ b/src/main/resources/templates/imprimelibros/cart/cart.html @@ -36,100 +36,7 @@ -
    - -
    -
    - Cargando… -
    -
    - -
    - -
    - -
    - - - -
    -
    -

    -
    - - -
    - - -
    - -
    -
    - -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - -
    :
    :
    : - - - -
    - -
    - -
    -
    - - -
    - - -
    - -
    +
    diff --git a/src/main/resources/templates/imprimelibros/checkout/_envio.html b/src/main/resources/templates/imprimelibros/checkout/_envio.html index 15687fa..893721c 100644 --- a/src/main/resources/templates/imprimelibros/checkout/_envio.html +++ b/src/main/resources/templates/imprimelibros/checkout/_envio.html @@ -12,10 +12,10 @@

    - -
    diff --git a/src/main/resources/templates/imprimelibros/cart/_cartItem.html b/src/main/resources/templates/imprimelibros/cart/_cartItem.html index 6f963aa..6920352 100644 --- a/src/main/resources/templates/imprimelibros/cart/_cartItem.html +++ b/src/main/resources/templates/imprimelibros/cart/_cartItem.html @@ -8,7 +8,7 @@
    -
    - - -
    + + + ${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'} @@ -327,19 +327,19 @@ $(() => { } async function getTipoEnvio() { - const { value: checkValue } = await Swal.fire({ + const { isConfirmed, value } = await Swal.fire({ title: window.languageBundle['cart.shipping.tipo-envio'] || 'Tipo de envío', html: ` -
    - - +
    + + ${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'} + + `, focusConfirm: false, showCancelButton: true, buttonsStyling: false, @@ -350,17 +350,18 @@ $(() => { confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar', cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar', preConfirm: () => { - const isPalets = document.getElementById('swal-input-palets').checked; - return isPalets; + const popup = Swal.getPopup(); + const chk = popup.querySelector('#swal-input-palets'); + // Devuelve un OBJETO (siempre truthy) con el booleano dentro + return { isPalets: !!chk?.checked }; } }); - if (checkValue !== undefined) { - return checkValue; // boolean - } - return null; // Si se cancela el Swal + if (!isConfirmed) return null; // cancelado + return value.isPalets; // true / false } + function checkTotalUnits(container, tirada) { const totalUnits = container.find('.direccion-card').toArray().reduce((acc, el) => { diff --git a/src/main/resources/templates/imprimelibros/cart/_cartContent.html b/src/main/resources/templates/imprimelibros/cart/_cartContent.html index 7421730..f724aad 100644 --- a/src/main/resources/templates/imprimelibros/cart/_cartContent.html +++ b/src/main/resources/templates/imprimelibros/cart/_cartContent.html @@ -10,6 +10,7 @@
    + diff --git a/src/main/resources/templates/imprimelibros/cart/_cartItem.html b/src/main/resources/templates/imprimelibros/cart/_cartItem.html index 6920352..bcb1f1c 100644 --- a/src/main/resources/templates/imprimelibros/cart/_cartItem.html +++ b/src/main/resources/templates/imprimelibros/cart/_cartItem.html @@ -4,11 +4,12 @@ data-base=${item.base}">
    - +
    -
      +
    @@ -149,7 +165,22 @@ -
    +
    + + +
    + +
    +
    +
    +
    diff --git a/src/main/resources/templates/imprimelibros/cart/_cartSummary.html b/src/main/resources/templates/imprimelibros/cart/_cartSummary.html index 3caadb7..85fd642 100644 --- a/src/main/resources/templates/imprimelibros/cart/_cartSummary.html +++ b/src/main/resources/templates/imprimelibros/cart/_cartSummary.html @@ -1,4 +1,4 @@ -
    +
    @@ -12,6 +12,10 @@ + + + + : @@ -28,8 +32,12 @@ - +
    + + + +
    diff --git a/src/main/resources/templates/imprimelibros/direcciones/direccionCard.html b/src/main/resources/templates/imprimelibros/direcciones/direccionCard.html index f603077..329b10b 100644 --- a/src/main/resources/templates/imprimelibros/direcciones/direccionCard.html +++ b/src/main/resources/templates/imprimelibros/direcciones/direccionCard.html @@ -1,4 +1,4 @@ -
    diff --git a/src/test/java/com/imprimelibros/erp/redsys/RedsysServiceTest.java b/src/test/java/com/imprimelibros/erp/redsys/RedsysServiceTest.java deleted file mode 100644 index 7c353ba..0000000 --- a/src/test/java/com/imprimelibros/erp/redsys/RedsysServiceTest.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.imprimelibros.erp.redsys; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.junit.jupiter.api.condition.EnabledIfSystemProperty; - -import java.lang.reflect.Field; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -import sis.redsys.api.Signature; - -/** - * Tests de integración "locales" contra tu RedsysService - * usando el jar 'apiSha512V2.jar' (sis.redsys.api.*). - * - * Para que el test sea significativo: - * - Define la clave en entorno: REDSYS_SECRET_B64=tu_clave_base64 - * - O en propiedad de sistema: -Dredsys.secret.b64=tu_clave_base64 - */ -public class RedsysServiceTest { - - private RedsysService service; - - private static String readSecretFromEnvOrProp() { - String env = System.getenv("REDSYS_SECRET_B64"); - if (env != null && !env.isBlank()) - return env.trim(); - String prop = System.getProperty("redsys.secret.b64"); - if (prop != null && !prop.isBlank()) - return prop.trim(); - return ""; - } - - private static void setPrivate(Object target, String field, Object value) { - try { - Field f = target.getClass().getDeclaredField(field); - f.setAccessible(true); - f.set(target, value); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @BeforeEach - void setup() { - service = new RedsysService(); - - // ---- Config mínima para el test ---- - setPrivate(service, "merchantCode", "124760810"); // FUC de ejemplo (sandbox) - setPrivate(service, "terminal", "1"); - setPrivate(service, "currency", "978"); - setPrivate(service, "txType", "0"); - setPrivate(service, "urlOk", "http://localhost:8080/pagos/redsys/ok"); - setPrivate(service, "urlKo", "http://localhost:8080/pagos/redsys/ko"); - setPrivate(service, "urlNotify", "http://localhost:8080/pagos/redsys/notify"); - setPrivate(service, "env", "test"); - - // Clave: del entorno o propiedad. Si queda vacía, los tests se auto-saltan. - setPrivate(service, "secretKeyBase64", readSecretFromEnvOrProp()); - } - - private boolean secretPresent() { - try { - Field f = service.getClass().getDeclaredField("secretKeyBase64"); - f.setAccessible(true); - String key = (String) f.get(service); - return key != null && !key.isBlank(); - } catch (Exception e) { - return false; - } - } - - @Test - void buildRedirectForm_generates_signature_and_params() throws Exception { - if (!secretPresent()) { - System.out.println("SKIP: define REDSYS_SECRET_B64 o -Dredsys.secret.b64 para ejecutar este test."); - return; - } - - // Pedido de ejemplo (usa uno único por intento) - String order = "T" + System.currentTimeMillis(); // p.ej. T1699999999999 - long amountCents = 1234L; - - var req = new RedsysService.PaymentRequest(order, amountCents, "Test compra"); - var form = service.buildRedirectForm(req); - - assertNotNull(form); - assertEquals("HMAC_SHA512_V1", form.signatureVersion()); - assertNotNull(form.merchantParameters()); - assertNotNull(form.signature()); - assertTrue(form.action().contains("sis"), "Action debe ser endpoint de Redsys"); - - // Decodificamos los parámetros para comprobar que incluyen nuestro pedido e - // importe - String json = new String(Base64.getDecoder().decode(form.merchantParameters()), StandardCharsets.UTF_8); - assertTrue(json.contains("\"DS_MERCHANT_ORDER\":\"" + order + "\"")); - assertTrue(json.contains("\"DS_MERCHANT_AMOUNT\":\"" + amountCents + "\"")); - - // Recomputamos firma con el mismo jar y comparamos - String recomputed = Signature.createMerchantSignature( - readSecretFromEnvOrProp(), order, form.merchantParameters()); - assertEquals(form.signature(), recomputed, "La firma recomputada debe coincidir"); - } - - @Test - void validateAndParseNotification_roundtrip_ok() throws Exception { - if (!secretPresent()) { - System.out.println("SKIP: define REDSYS_SECRET_B64 o -Dredsys.secret.b64 para ejecutar este test."); - return; - } - - // 1) Simula un pedido real - String order = "N" + System.currentTimeMillis(); - long amountCents = 2500L; // 25,00 € - - // 2) Construye el JSON de NOTIFICACIÓN (vuelta) con claves Ds_* - Map notifJson = Map.of( - "Ds_Order", order, - "Ds_Amount", String.valueOf(amountCents), - "Ds_Currency", "978", - "Ds_Response", "0" // autorizado - // añade lo que quieras: Ds_AuthorisationCode, etc. - ); - - // 3) Base64 de ese JSON (exactamente lo que recibirías en - // Ds_MerchantParameters) - String notifJsonStr = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(notifJson); - String dsParams = java.util.Base64.getEncoder().encodeToString( - notifJsonStr.getBytes(java.nio.charset.StandardCharsets.UTF_8)); - - // 4) Firma de NOTIFICACIÓN (usa la misma API y clave que Redsys) - String dsSignature = sis.redsys.api.Signature.createMerchantSignature( - readSecretFromEnvOrProp(), order, dsParams); - - // 5) Llama a tu servicio como lo haría el webhook - RedsysService.RedsysNotification notif = service.validateAndParseNotification(dsSignature, dsParams); - - // 6) Asserts - assertEquals(order, notif.order); - assertEquals(amountCents, notif.amountCents); - assertEquals("978", notif.currency); - assertTrue(notif.authorized()); // porque Ds_Response="0" - } - -} From 90c191d8f8ea6b93b33ec9157695e0fb60e46a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Fri, 31 Oct 2025 08:25:13 +0100 Subject: [PATCH 10/11] falta mover la cesta a cliente --- .../imprimelibros/erp/cart/CartService.java | 148 ++++++++++++++---- .../erp/direcciones/DireccionService.java | 18 ++- .../erp/pedido/PedidoService.java | 8 +- src/main/resources/i18n/cart_es.properties | 18 ++- .../js/pages/imprimelibros/cart/cart.js | 35 +---- .../imprimelibros/cart/_cartSummary.html | 28 +++- 6 files changed, 166 insertions(+), 89 deletions(-) diff --git a/src/main/java/com/imprimelibros/erp/cart/CartService.java b/src/main/java/com/imprimelibros/erp/cart/CartService.java index 280dfc8..00082ce 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartService.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartService.java @@ -20,6 +20,7 @@ import com.imprimelibros.erp.cart.dto.UpdateCartRequest; import com.imprimelibros.erp.common.Utils; import com.imprimelibros.erp.direcciones.DireccionService; import com.imprimelibros.erp.externalApi.skApiClient; +import com.imprimelibros.erp.pedido.PedidoService; import com.imprimelibros.erp.presupuesto.PresupuestoRepository; @Service @@ -32,11 +33,12 @@ public class CartService { private final Utils utils; private final DireccionService direccionService; private final skApiClient skApiClient; + private final PedidoService pedidoService; public CartService(CartRepository cartRepo, CartItemRepository itemRepo, MessageSource messageSource, PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo, Utils utils, DireccionService direccionService, - skApiClient skApiClient) { + skApiClient skApiClient, PedidoService pedidoService) { this.cartRepo = cartRepo; this.itemRepo = itemRepo; this.messageSource = messageSource; @@ -44,6 +46,7 @@ public class CartService { this.utils = utils; this.direccionService = direccionService; this.skApiClient = skApiClient; + this.pedidoService = pedidoService; } /** Devuelve el carrito activo o lo crea si no existe. */ @@ -180,6 +183,7 @@ public class CartService { for (CartItem item : items) { Presupuesto p = item.getPresupuesto(); + Double peso = p.getPeso() != null ? p.getPeso().doubleValue() : 0.0; base += p.getBaseImponible().doubleValue(); iva4 += p.getIvaImporte4().doubleValue(); iva21 += p.getIvaImporte21().doubleValue(); @@ -190,43 +194,79 @@ public class CartService { Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(), cd.getDireccion().getPaisCode3()) && !cd.getIsPalets(); if (!freeShipment) { - try { - Map data = Map.of( - "cp", cd.getDireccion().getCp(), - "pais_code3", cd.getDireccion().getPaisCode3(), - "peso", p.getPeso() != null ? p.getPeso() : 0, - "unidades", p.getSelectedTirada(), - "palets", cd.getIsPalets() ? 1 : 0); - var shipmentCost = skApiClient.getCosteEnvio(data, locale); - if (shipmentCost != null && shipmentCost.get("data") != null) { - shipment += (Double) shipmentCost.get("data"); - iva21 += ((Double) shipmentCost.get("data")) * 0.21; - } else { - errorShipementCost = true; - } - } catch (Exception e) { + Integer unidades = p.getSelectedTirada(); + Map res = getShippingCost(cd, peso, unidades, locale); + if (res.get("success").equals(Boolean.FALSE)) { errorShipementCost = true; } + else{ + shipment += (Double) res.get("shipment"); + iva21 += (Double) res.get("iva21"); + } + } // si tiene prueba de envio, hay que añadir el coste if (p.getServiciosJson() != null && p.getServiciosJson().contains("ejemplar-prueba")) { - try { - Map data = Map.of( - "cp", cd.getDireccion().getCp(), - "pais_code3", cd.getDireccion().getPaisCode3(), - "peso", p.getPeso() != null ? p.getPeso() : 0, - "unidades", 1, - "palets", cd.getIsPalets() ? 1 : 0); - var shipmentCost = skApiClient.getCosteEnvio(data, locale); - if (shipmentCost != null && shipmentCost.get("data") != null) { - shipment += (Double) shipmentCost.get("data"); - iva21 += ((Double) shipmentCost.get("data")) * 0.21; - } else { - errorShipementCost = true; - } - } catch (Exception e) { + + Map res = getShippingCost(cd, peso, 1, locale); + if (res.get("success").equals(Boolean.FALSE)) { errorShipementCost = true; } + else{ + shipment += (Double) res.get("shipment"); + iva21 += (Double) res.get("iva21"); + } + } + } + } else { + // envio por cada presupuesto + // buscar la direccion asignada a este presupuesto + if (direcciones == null) + continue; + List cd_presupuesto = direcciones.stream() + .filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(p.getId()) + && d.getUnidades() != null && d.getUnidades() != null && d.getUnidades() > 0) + .toList(); + Boolean firstDirection = true; + for (CartDireccion cd : cd_presupuesto) { + Integer unidades = cd.getUnidades(); + if (firstDirection) { + Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(), + cd.getDireccion().getPaisCode3()) && !cd.getIsPalets(); + if (!freeShipment && unidades != null && unidades > 0) { + Map res = getShippingCost(cd, peso, unidades, locale); + if (res.get("success").equals(Boolean.FALSE)) { + errorShipementCost = true; + } else { + shipment += (Double) res.get("shipment"); + iva21 += (Double) res.get("iva21"); + } + } + firstDirection = false; + } else { + Map res = getShippingCost(cd, peso, unidades, locale); + if (res.get("success").equals(Boolean.FALSE)) { + errorShipementCost = true; + } else { + shipment += (Double) res.get("shipment"); + iva21 += (Double) res.get("iva21"); + } + } + } + // ejemplar de prueba + CartDireccion cd_prueba = direcciones.stream() + .filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(p.getId()) + && d.getUnidades() == null) + .findFirst().orElse(null); + if (cd_prueba != null) { + + Map res = getShippingCost(cd_prueba, peso, 1, locale); + if (res.get("success").equals(Boolean.FALSE)) { + errorShipementCost = true; + } + else{ + shipment += (Double) res.get("shipment"); + iva21 += (Double) res.get("iva21"); } } } @@ -234,11 +274,17 @@ public class CartService { double total = base + iva4 + iva21 + shipment; + int fidelizacion = pedidoService.getDescuentoFidelizacion(); + double descuento = (total) * fidelizacion / 100.0; + total -= descuento; + Map summary = new HashMap<>(); summary.put("base", Utils.formatCurrency(base, locale)); summary.put("iva4", Utils.formatCurrency(iva4, locale)); summary.put("iva21", Utils.formatCurrency(iva21, locale)); summary.put("shipment", Utils.formatCurrency(shipment, locale)); + summary.put("fidelizacion", fidelizacion + "%"); + summary.put("descuento", Utils.formatCurrency(-descuento, locale)); summary.put("total", Utils.formatCurrency(total, locale)); summary.put("errorShipmentCost", errorShipementCost); @@ -308,4 +354,44 @@ public class CartService { return false; } } + + private Map getShippingCost( + CartDireccion cd, + Double peso, + Integer unidades, + Locale locale) { + + Map result = new HashMap<>(); + + try { + Map data = Map.of( + "cp", cd.getDireccion().getCp(), + "pais_code3", cd.getDireccion().getPaisCode3(), + "peso", peso != null ? peso : 0.0, + "unidades", unidades, + "palets", Boolean.TRUE.equals(cd.getIsPalets()) ? 1 : 0); + + var shipmentCost = skApiClient.getCosteEnvio(data, locale); + + if (shipmentCost != null && shipmentCost.get("data") != null) { + Number n = (Number) shipmentCost.get("data"); + double cost = n.doubleValue(); + + result.put("success", true); + result.put("shipment", cost); + result.put("iva21", cost * 0.21); + } else { + result.put("success", false); + result.put("shipment", 0.0); + result.put("iva21", 0.0); + } + } catch (Exception e) { + result.put("success", false); + result.put("shipment", 0.0); + result.put("iva21", 0.0); + } + + return result; + } + } diff --git a/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java index c3dd36f..1f1d4d5 100644 --- a/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java +++ b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java @@ -61,7 +61,8 @@ public class DireccionService { String alias = opt.get("alias").toLowerCase(); String text = opt.get("text").toLowerCase(); String direccion = opt.get("direccion").toLowerCase(); - return text.contains(q) || cp.contains(q) || ciudad.contains(q) || att.contains(q) || alias.contains(q) || direccion.contains(q); + return text.contains(q) || cp.contains(q) || ciudad.contains(q) || att.contains(q) + || alias.contains(q) || direccion.contains(q); }) .sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance())) .collect(Collectors.toList()); @@ -81,15 +82,16 @@ public class DireccionService { } public Boolean checkFreeShipment(Integer cp, String paisCode3) { - if (paisCode3 != null && paisCode3.equals("ESP") && cp != null) { - // Excluir Canarias (35xxx y 38xxx), Baleares (07xxx), Ceuta (51xxx), Melilla (52xxx) - int provincia = cp / 1000; + if (paisCode3 != null && paisCode3.toLowerCase().equals("esp") && cp != null) { + // Excluir Canarias (35xxx y 38xxx), Baleares (07xxx), Ceuta (51xxx), Melilla + // (52xxx) + int provincia = cp / 1000; - if (provincia != 7 && provincia != 35 && provincia != 38 && provincia != 51 && provincia != 52) { - return true; // España peninsular + if (provincia != 7 && provincia != 35 && provincia != 38 && provincia != 51 && provincia != 52) { + return true; // España peninsular + } } + return false; } - return false; -} } diff --git a/src/main/java/com/imprimelibros/erp/pedido/PedidoService.java b/src/main/java/com/imprimelibros/erp/pedido/PedidoService.java index 5372c06..5655b27 100644 --- a/src/main/java/com/imprimelibros/erp/pedido/PedidoService.java +++ b/src/main/java/com/imprimelibros/erp/pedido/PedidoService.java @@ -4,8 +4,8 @@ import org.springframework.stereotype.Service; @Service public class PedidoService { - - public int hasDescuentoFidelidad() { + + public int getDescuentoFidelizacion() { // descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el ultimo año) double totalGastado = 1600.0; // Ejemplo, deberías obtenerlo del historial del cliente if(totalGastado < 1200) { @@ -18,10 +18,8 @@ public class PedidoService { return 3; } else if(totalGastado >= 4000 && totalGastado < 4999) { return 4; - } else if(totalGastado >= 5000 && totalGastado < 9999) { + } else if(totalGastado >= 5000) { return 5; - } else if(totalGastado >= 10000) { - return 6; } return 0; } diff --git a/src/main/resources/i18n/cart_es.properties b/src/main/resources/i18n/cart_es.properties index ab73deb..c867ff5 100644 --- a/src/main/resources/i18n/cart_es.properties +++ b/src/main/resources/i18n/cart_es.properties @@ -30,14 +30,20 @@ cart.shipping.errors.noAddressSelected=Debe seleccionar una dirección de envío cart.shipping.errors.fillAddressesItems=Debe seleccionar una dirección de envío para cada artículo de la cesta. cart.resumen.title=Resumen de la cesta -cart.resumen.base=Base imponible: -cart.resumen.envio=Coste de envío: -cart.resumen.iva-4=IVA 4%: -cart.resumen.iva-21=IVA 21%: -cart.resumen.total=Total cesta: +cart.resumen.base=Base imponible +cart.resumen.envio=Coste de envío +cart.resumen.iva-4=IVA 4% +cart.resumen.iva-21=IVA 21% +cart.resumen.descuento=Descuento fidelización +cart.resumen.total=Total cesta cart.resumen.tramitar=Tramitar pedido -cart.resumen.fidelizacion=Si tiene descuento por fidelización, se aplicará al tramitar el pedido. +cart.pass-to.customer=Mover cesta a cliente +cart.pass-to.customer.info=Puede mover la cesta actual al cliente seleccionado. Esto eliminará la cesta del usuario actual y la asociará al cliente seleccionado. +cart.pass-to.customer.warning=Advertencia: Esta acción no se puede deshacer y sobrescribirá la cesta del cliente seleccionado. Asegúrese de que el cliente seleccionado es correcto. +cart.pass-to.select-customer=Seleccione un cliente +cart.pass-to.button=Mover cesta +cart.pass-to.success=Cesta movida correctamente al cliente {0}. cart.errors.update-cart=Error al actualizar la cesta de la compra: {0} cart.errors.shipping=No se puede calcular el coste del envío para alguna de las direcciones seleccionadas. Por favor, póngase en contacto con el servicio de atención al cliente. \ No newline at end of file diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/cart/cart.js b/src/main/resources/static/assets/js/pages/imprimelibros/cart/cart.js index 7f73011..9a3fd32 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/cart/cart.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/cart/cart.js @@ -31,11 +31,11 @@ $(() => { }).always(() => { hideLoader(); }); - updateTotal(); + checkAddressesForItems(); }); - updateTotal(); + checkAddressesForItems(); function checkAddressesForItems(){ if($('#onlyOneShipment').is(':checked')){ @@ -89,37 +89,6 @@ $(() => { } } - function updateTotal() { - /*const items = $(".product"); - let iva4 = 0; - let iva21 = 0; - let base = 0; - for (let i = 0; i < items.length; i++) { - const item = $(items[i]); - const b = item.data("base"); - const i4 = item.data("iva-4"); - const i21 = item.data("iva-21"); - base += parseFloat(b) || 0; - iva4 += parseFloat(i4) || 0; - iva21 += parseFloat(i21) || 0; - } - $("#base-cesta").text(formateaMoneda(base)); - if (iva4 > 0) { - $("#iva-4-cesta").text(formateaMoneda(iva4)); - $("#tr-iva-4").show(); - } else { - $("#tr-iva-4").hide(); - } - if (iva21 > 0) { - $("#iva-21-cesta").text(formateaMoneda(iva21)); - $("#tr-iva-21").show(); - } else { - $("#tr-iva-21").hide(); - } - const total = base + iva4 + iva21; - $("#total-cesta").text(formateaMoneda(total));*/ - } - $(document).on("click", ".delete-item", async function (event) { event.preventDefault(); diff --git a/src/main/resources/templates/imprimelibros/cart/_cartSummary.html b/src/main/resources/templates/imprimelibros/cart/_cartSummary.html index 85fd642..c03024e 100644 --- a/src/main/resources/templates/imprimelibros/cart/_cartSummary.html +++ b/src/main/resources/templates/imprimelibros/cart/_cartSummary.html @@ -24,6 +24,10 @@ : + + : + + : @@ -35,7 +39,7 @@
    -
    @@ -43,13 +47,25 @@
    -
    - -
    +
    -
    -
    - - -
    - + +
    + + +
    +
    + +
    - +
    diff --git a/src/main/resources/templates/imprimelibros/cart/cart.html b/src/main/resources/templates/imprimelibros/cart/cart.html index 0170d5b..7498643 100644 --- a/src/main/resources/templates/imprimelibros/cart/cart.html +++ b/src/main/resources/templates/imprimelibros/cart/cart.html @@ -36,6 +36,10 @@
    +
    + +
    +