From feff9ee94a2a8648829763e244603a3db2395ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Wed, 29 Oct 2025 23:30:33 +0100 Subject: [PATCH] cargando carrito desde backend --- pom.xml | 2 +- .../java/com/imprimelibros/erp/cart/Cart.java | 87 +++++++-- .../erp/cart/CartController.java | 42 +++-- .../imprimelibros/erp/cart/CartDireccion.java | 60 ++++++ .../imprimelibros/erp/cart/CartService.java | 71 ++++++-- .../erp/cart/dto/CartDireccionRepository.java | 15 ++ .../erp/cart/dto/DireccionShipment.java | 60 ++++++ .../erp/cart/dto/UpdateCartRequest.java | 26 +++ .../erp/direcciones/DireccionController.java | 13 +- .../erp/direcciones/DireccionService.java | 10 + .../erp/presupuesto/dto/Presupuesto.java | 24 +++ .../erp/redsys/RedsysController.java | 56 +++--- .../erp/redsys/RedsysService.java | 162 ++++++++++++----- .../changesets/0006-add-cart-direcciones.yml | 18 ++ src/main/resources/i18n/cart_es.properties | 8 +- .../static/assets/css/imprimelibros.css | 10 + .../js/pages/imprimelibros/cart/cart.js | 2 +- .../pages/imprimelibros/cart/shipping-cart.js | 172 +++++++++++++----- .../imprimelibros/cart/_cartContent.html | 10 +- .../imprimelibros/cart/_cartItem.html | 2 +- .../direcciones/direccionCard.html | 24 ++- .../payments/redsys-redirect.html | 7 + .../erp/redsys/RedsysServiceTest.java | 150 +++++++++++++++ 23 files changed, 848 insertions(+), 183 deletions(-) create mode 100644 src/main/java/com/imprimelibros/erp/cart/CartDireccion.java create mode 100644 src/main/java/com/imprimelibros/erp/cart/dto/CartDireccionRepository.java create mode 100644 src/main/java/com/imprimelibros/erp/cart/dto/DireccionShipment.java create mode 100644 src/main/java/com/imprimelibros/erp/cart/dto/UpdateCartRequest.java create mode 100644 src/main/resources/templates/imprimelibros/payments/redsys-redirect.html create mode 100644 src/test/java/com/imprimelibros/erp/redsys/RedsysServiceTest.java diff --git a/pom.xml b/pom.xml index f882da2..a660368 100644 --- a/pom.xml +++ b/pom.xml @@ -153,7 +153,7 @@ - com.redsys + sis.redsys apiSha512V2 2.0 system diff --git a/src/main/java/com/imprimelibros/erp/cart/Cart.java b/src/main/java/com/imprimelibros/erp/cart/Cart.java index ff3b6ec..82dea7f 100644 --- a/src/main/java/com/imprimelibros/erp/cart/Cart.java +++ b/src/main/java/com/imprimelibros/erp/cart/Cart.java @@ -6,13 +6,16 @@ import java.util.ArrayList; import java.util.List; @Entity -@Table(name = "carts", - uniqueConstraints = @UniqueConstraint(name="uq_carts_user_active", columnNames={"user_id","status"})) +@Table(name = "carts", uniqueConstraints = @UniqueConstraint(name = "uq_carts_user_active", columnNames = { "user_id", + "status" })) public class Cart { - public enum Status { ACTIVE, LOCKED, ABANDONED } + public enum Status { + ACTIVE, LOCKED, ABANDONED + } - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "user_id", nullable = false) @@ -25,6 +28,9 @@ public class Cart { @Column(nullable = false, length = 3) private String currency = "EUR"; + @Column(name = "only_one_shipment", nullable = false) + private Boolean onlyOneShipment = true; + @Column(name = "created_at", nullable = false) private LocalDateTime createdAt = LocalDateTime.now(); @@ -34,23 +40,72 @@ public class Cart { @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List items = new ArrayList<>(); + @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private List direcciones = new ArrayList<>(); + @PreUpdate - public void preUpdate() { this.updatedAt = LocalDateTime.now(); } + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } // Getters & Setters - public Long getId() { return id; } - public Long getUserId() { return userId; } - public void setUserId(Long userId) { this.userId = userId; } + public Long getId() { + return id; + } - public Status getStatus() { return status; } - public void setStatus(Status status) { this.status = status; } + public Long getUserId() { + return userId; + } - public String getCurrency() { return currency; } - public void setCurrency(String currency) { this.currency = currency; } + public void setUserId(Long userId) { + this.userId = userId; + } - public LocalDateTime getCreatedAt() { return createdAt; } - public LocalDateTime getUpdatedAt() { return updatedAt; } + public Status getStatus() { + return status; + } - public List getItems() { return items; } - public void setItems(List items) { this.items = items; } + public void setStatus(Status status) { + this.status = status; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public Boolean getOnlyOneShipment() { + return onlyOneShipment; + } + + public void setOnlyOneShipment(Boolean onlyOneShipment) { + this.onlyOneShipment = onlyOneShipment; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public List getDirecciones() { + return direcciones; + } + + public void setDirecciones(List direcciones) { + this.direcciones = direcciones; + } } diff --git a/src/main/java/com/imprimelibros/erp/cart/CartController.java b/src/main/java/com/imprimelibros/erp/cart/CartController.java index d72dbd1..f97dc2b 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartController.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartController.java @@ -9,6 +9,7 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.context.MessageSource; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import com.imprimelibros.erp.common.Utils; import com.imprimelibros.erp.direcciones.Direccion; @@ -19,9 +20,8 @@ import java.security.Principal; import java.util.List; import java.util.Locale; import java.util.Map; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +import com.imprimelibros.erp.cart.dto.UpdateCartRequest; @Controller @RequestMapping("/cart") @@ -56,6 +56,9 @@ public class CartController { "cart.shipping.errors.units-error", "cart.shipping.ud", "cart.shipping.uds", + "cart.shipping.send-in-palets", + "cart.shipping.send-in-palets.info", + "cart.shipping.tipo-envio", "app.yes", "app.aceptar", "app.cancelar"); @@ -63,12 +66,16 @@ public class CartController { Map translations = translationService.getTranslations(locale, keys); model.addAttribute("languageBundle", translations); - var items = service.listItems(Utils.currentUserId(principal), locale); + Long userId = Utils.currentUserId(principal); + Cart cart = service.getOrCreateActiveCart(userId); + + var items = service.listItems(userId, locale); model.addAttribute("items", items); - var summary = service.getCartSummary(items, locale); + + var summary = service.getCartSummary(cart, locale); model.addAttribute("cartSummary", summary); - model.addAttribute("cartId", service.getOrCreateActiveCart(Utils.currentUserId(principal))); + model.addAttribute("cart", cart); return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple) } @@ -126,6 +133,7 @@ public class CartController { @GetMapping("/get-address/{id}") public String getDireccionCard(@PathVariable Long id, @RequestParam(required = false) Long presupuestoId, @RequestParam(required = false) Integer unidades, + @RequestParam(required = false) Integer isPalets, Model model, Locale locale) { Direccion dir = direccionService.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); @@ -133,18 +141,28 @@ public class CartController { dir.getPais().getKeyword(), locale)); model.addAttribute("presupuestoId", presupuestoId); model.addAttribute("unidades", unidades); + model.addAttribute("isPalets", isPalets); model.addAttribute("direccion", dir); return "imprimelibros/direcciones/direccionCard :: direccionCard(direccion=${direccion})"; } - @PostMapping("/update/{id}") - public String postMethodName(@PathVariable Long id, @RequestBody String entity) { - - + @PostMapping(value = "/update/{id}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public String updateCart(@PathVariable Long id, UpdateCartRequest updateRequest, Model model, Locale locale) { + + try { + service.updateCart(id, updateRequest); + var cartSummary = service.getCartSummary(service.getCartById(id), locale); + model.addAttribute("cartSummary", cartSummary); + + return "imprimelibros/cart/_cartSummary :: cartSummary(summary=${cartSummary})"; + + } catch (Exception e) { + + model.addAttribute("errorMessage", messageSource.getMessage("cart.errors.update-cart", new Object[]{e.getMessage()}, locale)); + return "/cart"; // templates/error/500.html + } - - return entity; } - + } diff --git a/src/main/java/com/imprimelibros/erp/cart/CartDireccion.java b/src/main/java/com/imprimelibros/erp/cart/CartDireccion.java new file mode 100644 index 0000000..8edda9e --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/CartDireccion.java @@ -0,0 +1,60 @@ +package com.imprimelibros.erp.cart; + +import java.math.BigDecimal; + +import com.imprimelibros.erp.direcciones.Direccion; +import com.imprimelibros.erp.presupuesto.dto.Presupuesto; + +import jakarta.persistence.*; + +@Entity +@Table(name = "cart_direcciones") +public class CartDireccion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cart_id", nullable = false) + private Cart cart; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "direccion_id", nullable = false) + private Direccion direccion; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "presupuesto_id") + private Presupuesto presupuesto; + + @Column(name = "unidades") + private Integer unidades; + + @Column(name = "isPalets", nullable = false) + private Boolean isPalets; + + @Column(name = "base", precision = 12, scale = 2) + private BigDecimal base; + + // --- Getters & Setters --- + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Cart getCart() { return cart; } + public void setCart(Cart cart) { this.cart = cart; } + + public Direccion getDireccion() { return direccion; } + public void setDireccion(Direccion direccion) { this.direccion = direccion; } + + public Presupuesto getPresupuesto() { return presupuesto; } + public void setPresupuesto(Presupuesto presupuesto) { this.presupuesto = presupuesto; } + + public Integer getUnidades() { return unidades; } + public void setUnidades(Integer unidades) { this.unidades = unidades; } + + public BigDecimal getBase() { return base; } + public void setBase(BigDecimal base) { this.base = base; } + + public Boolean getIsPalets() { return isPalets; } + public void setIsPalets(Boolean isPalets) { this.isPalets = isPalets; } +} diff --git a/src/main/java/com/imprimelibros/erp/cart/CartService.java b/src/main/java/com/imprimelibros/erp/cart/CartService.java index 9b2db65..8617535 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartService.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartService.java @@ -13,10 +13,12 @@ import java.util.Map; import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; +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.presupuesto.PresupuestoRepository; - @Service public class CartService { @@ -25,15 +27,20 @@ public class CartService { private final MessageSource messageSource; private final PresupuestoRepository presupuestoRepo; private final Utils utils; + private final DireccionService direccionService; + private final skApiClient skApiClient; public CartService(CartRepository cartRepo, CartItemRepository itemRepo, MessageSource messageSource, PresupuestoFormatter presupuestoFormatter, - PresupuestoRepository presupuestoRepo, Utils utils) { + PresupuestoRepository presupuestoRepo, Utils utils, DireccionService direccionService, + skApiClient skApiClient) { this.cartRepo = cartRepo; this.itemRepo = itemRepo; this.messageSource = messageSource; this.presupuestoRepo = presupuestoRepo; this.utils = utils; + this.direccionService = direccionService; + this.skApiClient = skApiClient; } /** Devuelve el carrito activo o lo crea si no existe. */ @@ -48,6 +55,11 @@ public class CartService { }); } + public Cart getCartById(Long cartId) { + return cartRepo.findById(cartId) + .orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado")); + } + /** Lista items (presupuestos) del carrito activo del usuario. */ @Transactional public List> listItems(Long userId, Locale locale) { @@ -57,13 +69,14 @@ public class CartService { for (CartItem item : items) { Presupuesto p = presupuestoRepo.findById(item.getPresupuestoId()) - .orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + item.getPresupuestoId())); + .orElseThrow( + () -> new IllegalStateException("Presupuesto no encontrado: " + item.getPresupuestoId())); Map elemento = getElementoCart(p, locale); elemento.put("cartItemId", item.getId()); resultados.add(elemento); } - //System.out.println("Cart items: " + resultados); + // System.out.println("Cart items: " + resultados); return resultados; } @@ -120,9 +133,9 @@ public class CartService { } private Map getElementoCart(Presupuesto presupuesto, Locale locale) { - + Map resumen = new HashMap<>(); - + resumen.put("titulo", presupuesto.getTitulo()); resumen.put("imagen", @@ -132,7 +145,7 @@ public class CartService { resumen.put("presupuestoId", presupuesto.getId()); - if(presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("ejemplar-prueba")) { + if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("ejemplar-prueba")) { resumen.put("hasSample", true); } else { resumen.put("hasSample", false); @@ -151,18 +164,40 @@ public class CartService { return resumen; } - public Map getCartSummary(List> cartItems, Locale locale) { + public Map getCartSummary(Cart cart, Locale locale) { double base = 0.0; double iva4 = 0.0; double iva21 = 0.0; - for (Map item : cartItems) { - Presupuesto p = presupuestoRepo.findById((Long) item.get("presupuestoId")) - .orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + item.get("presupuestoId"))); + List items = cart.getItems(); + List direcciones = cart.getDirecciones(); + + for (CartItem item : items) { + Presupuesto p = presupuestoRepo.findById(item.getPresupuestoId()) + .orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + item.getPresupuestoId())); base += p.getBaseImponible().doubleValue(); iva4 += p.getIvaImporte4().doubleValue(); iva21 += p.getIvaImporte21().doubleValue(); + if(cart.getOnlyOneShipment() != null && cart.getOnlyOneShipment()) { + // Si es envío único, que es a españa y no ha canarias + if(direcciones != null && direcciones.size() > 0) { + CartDireccion cd = direcciones.get(0); + Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(), cd.getDireccion().getPaisCode3()) && !cd.getIsPalets(); + if(!freeShipment) { + Map data = + Map.of( + "cp", cd.getDireccion().getCp(), + "pais_code3", cd.getDireccion().getPaisCode3(), + "peso", p.getPeso() != null ? p.getPeso() : 0, + "unidades", cd.getUnidades(), + "palets", cd.getIsPalets() ? 1 : 0 + ); + var shipmentCost = skApiClient.getCosteEnvio(data, locale); + + } + } + } } double total = base + iva4 + iva21; @@ -175,4 +210,18 @@ public class CartService { return summary; } + + @Transactional + public Boolean updateCart(Long cartId, UpdateCartRequest request) { + + try{ + Cart cart = cartRepo.findById(cartId).orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado")); + cart.setOnlyOneShipment(request.isOnlyOneShipment()); + cartRepo.save(cart); + return true; + } catch (Exception e) { + // Manejo de excepciones + return false; + } + } } diff --git a/src/main/java/com/imprimelibros/erp/cart/dto/CartDireccionRepository.java b/src/main/java/com/imprimelibros/erp/cart/dto/CartDireccionRepository.java new file mode 100644 index 0000000..ba1e740 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/dto/CartDireccionRepository.java @@ -0,0 +1,15 @@ +package com.imprimelibros.erp.cart.dto; + +import com.imprimelibros.erp.cart.CartDireccion; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CartDireccionRepository extends JpaRepository { + + // Borrado masivo por cart_id + void deleteByCartId(Long cartId); + + // Lectura por cart_id (útil para componer respuestas) + List findByCartId(Long cartId); +} diff --git a/src/main/java/com/imprimelibros/erp/cart/dto/DireccionShipment.java b/src/main/java/com/imprimelibros/erp/cart/dto/DireccionShipment.java new file mode 100644 index 0000000..a6c5fea --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/dto/DireccionShipment.java @@ -0,0 +1,60 @@ +package com.imprimelibros.erp.cart.dto; + +public class DireccionShipment { + + private Long id; // puede no venir → null + private String cp; // puede no venir → null + private String paisCode3; // puede no venir → null + private Long presupuestoId; // puede no venir → null + private Integer unidades; // puede no venir → null + private Boolean isPalets; // puede no venir → null + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCp() { + return cp; + } + + public void setCp(String cp) { + this.cp = cp; + } + + public String getPaisCode3() { + return paisCode3; + } + + public void setPaisCode3(String paisCode3) { + this.paisCode3 = paisCode3; + } + + public Long getPresupuestoId() { + return presupuestoId; + } + + public void setPresupuestoId(Long presupuestoId) { + this.presupuestoId = presupuestoId; + } + + public Integer getUnidades() { + return unidades; + } + + public void setUnidades(Integer unidades) { + this.unidades = unidades; + } + + public Boolean getIsPalets() { + return isPalets; + } + + public void setIsPalets(Boolean isPalets) { + this.isPalets = isPalets; + } +} + diff --git a/src/main/java/com/imprimelibros/erp/cart/dto/UpdateCartRequest.java b/src/main/java/com/imprimelibros/erp/cart/dto/UpdateCartRequest.java new file mode 100644 index 0000000..515afc9 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/dto/UpdateCartRequest.java @@ -0,0 +1,26 @@ +package com.imprimelibros.erp.cart.dto; + +import java.util.ArrayList; +import java.util.List; + +public class UpdateCartRequest { + private Boolean onlyOneShipment = Boolean.FALSE; // default: false + private List direcciones = new ArrayList<>(); + + public boolean isOnlyOneShipment() { // boolean-style getter + return Boolean.TRUE.equals(onlyOneShipment); + } + + public void setOnlyOneShipment(Boolean onlyOneShipment) { + this.onlyOneShipment = onlyOneShipment; + } + + public List getDirecciones() { + return direcciones; + } + + public void setDirecciones(List direcciones) { + this.direcciones = (direcciones != null) ? direcciones : new ArrayList<>(); + } +} + diff --git a/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java b/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java index cf05771..28d2bad 100644 --- a/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java +++ b/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java @@ -369,7 +369,7 @@ public class DireccionController { Locale locale) { User current = userRepo.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(auth.getName()).orElse(null); - direccion.setUser(current); + direccion.setUser(current); if (binding.hasErrors()) { response.setStatus(422); @@ -480,16 +480,19 @@ public class DireccionController { public Map getSelect2( @RequestParam(value = "q", required = false) String q1, @RequestParam(value = "term", required = false) String q2, + @RequestParam(value = "presupuestoId", required = false) Long presupuestoId, 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); + if (!isAdmin) { + 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); diff --git a/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java index 116d3e0..fc339c0 100644 --- a/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java +++ b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java @@ -83,4 +83,14 @@ public class DireccionService { return repo.findById(id); } + public Boolean checkFreeShipment(Integer cp, String paisCode3) { + if(paisCode3.equals("ESP")) { + // España peninsular y baleares + if(cp != null && cp < 35000 && cp >= 35999) { + return true; + } + } + return false; + } + } diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java b/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java index 65942fc..12db785 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java @@ -920,4 +920,28 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable { public void setId(Long id){ this.id = id; } + + public Double getPeso(){ + // get peso from first element of pricingSnapshotJson (need to parse JSON) + // pricingSnapshotJson = {"xxx":{"peso":0.5,...}} is a String + if (this.pricingSnapshotJson != null && !this.pricingSnapshotJson.isEmpty()) { + try { + String json = this.pricingSnapshotJson.trim(); + int pesoIndex = json.indexOf("\"peso\":"); + if (pesoIndex != -1) { + int startIndex = pesoIndex + 7; + int endIndex = json.indexOf(",", startIndex); + if (endIndex == -1) { + endIndex = json.indexOf("}", startIndex); + } + String pesoStr = json.substring(startIndex, endIndex).trim(); + return Double.parseDouble(pesoStr); + } + } catch (Exception e) { + // log error + e.printStackTrace(); + } + } + return null; + } } diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java index 6a7b43b..032ad84 100644 --- a/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java +++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java @@ -1,53 +1,57 @@ package com.imprimelibros.erp.redsys; +import com.imprimelibros.erp.redsys.RedsysService; + import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.*; @Controller @RequestMapping("/pagos/redsys") public class RedsysController { private final RedsysService service; - public RedsysController(RedsysService service) { this.service = service; } + + public RedsysController(RedsysService service) { + this.service = service; + } @PostMapping("/crear") public String crearPago(@RequestParam String order, - @RequestParam long amountCents, - Model model) { - var payReq = new RedsysService.PaymentRequest(order, amountCents, "Compra en ImprimeLibros"); - var form = service.buildRedirectForm(payReq); + @RequestParam long amountCents, + Model model) throws Exception { + + var req = new RedsysService.PaymentRequest(order, amountCents, "Compra en ImprimeLibros"); + var form = service.buildRedirectForm(req); model.addAttribute("action", form.action()); model.addAttribute("signatureVersion", form.signatureVersion()); model.addAttribute("merchantParameters", form.merchantParameters()); model.addAttribute("signature", form.signature()); - return "payments/redsys-redirect"; // Thymeleaf + return "payments/redsys-redirect"; } @PostMapping("/notify") @ResponseBody - public ResponseEntity notifyRedsys(@RequestParam("Ds_Signature") String dsSig, - @RequestParam("Ds_SignatureVersion") String dsSigVer, - @RequestParam("Ds_MerchantParameters") String dsParams) throws Exception { - var notif = service.validateAndParse(dsSig, dsSigVer, dsParams); + public ResponseEntity notifyRedsys( + @RequestParam("Ds_Signature") String dsSignature, + @RequestParam("Ds_MerchantParameters") String dsMerchantParameters) { - // 1) Idempotencia: marca el pedido si aún no procesado. - // 2) Verifica importe/moneda/pedido contra tu base de datos. - // 3) Autoriza en tu sistema si notif.authorized() == true. + try { + RedsysService.RedsysNotification notif = service.validateAndParseNotification(dsSignature, + dsMerchantParameters); - return ResponseEntity.ok("OK"); + // 1) Idempotencia: comprueba si el pedido ya fue procesado + // 2) Valida que importe/moneda/pedido coincidan con lo que esperabas + // 3) Marca como pagado si notif.authorized() == true + + return ResponseEntity.ok("OK"); // Redsys espera "OK" + } catch (SecurityException se) { + // Firma incorrecta: NO procesar + return ResponseEntity.status(400).body("BAD SIGNATURE"); + } catch (Exception e) { + return ResponseEntity.status(500).body("ERROR"); + } } - @GetMapping("/ok") - public String ok() { return "payments/success"; } - - @GetMapping("/ko") - public String ko() { return "payments/failure"; } - } - diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java index 33ac9eb..c3e66ff 100644 --- a/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java +++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java @@ -1,89 +1,155 @@ package com.imprimelibros.erp.redsys; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import sis.redsys.api.Signature; +import sis.redsys.api.Utils; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + @Service public class RedsysService { + // ---------- CONFIG ---------- @Value("${redsys.merchant-code}") private String merchantCode; @Value("${redsys.terminal}") private String terminal; @Value("${redsys.currency}") private String currency; @Value("${redsys.transaction-type}") private String txType; - @Value("${redsys.secret-key}") private String secretKey; + @Value("${redsys.secret-key}") private String secretKeyBase64; @Value("${redsys.urls.ok}") private String urlOk; @Value("${redsys.urls.ko}") private String urlKo; @Value("${redsys.urls.notify}") private String urlNotify; @Value("${redsys.environment}") private String env; + // ---------- RECORDS ---------- public record PaymentRequest(String order, long amountCents, String description) {} - public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) {} - public FormPayload buildRedirectForm(PaymentRequest req) { - // RedsysAPI proviene del JAR oficial - com.redsys.api.RedsysAPI api = new com.redsys.api.RedsysAPI(); + // ---------- MÉTODO PRINCIPAL ---------- + public FormPayload buildRedirectForm(PaymentRequest req) throws Exception { + Map params = new HashMap<>(); + params.put("DS_MERCHANT_AMOUNT", String.valueOf(req.amountCents())); + params.put("DS_MERCHANT_ORDER", req.order()); + params.put("DS_MERCHANT_MERCHANTCODE", merchantCode); + params.put("DS_MERCHANT_CURRENCY", currency); + params.put("DS_MERCHANT_TRANSACTIONTYPE", txType); + params.put("DS_MERCHANT_TERMINAL", terminal); + params.put("DS_MERCHANT_MERCHANTNAME", "ImprimeLibros"); + params.put("DS_MERCHANT_PRODUCTDESCRIPTION", req.description()); + params.put("DS_MERCHANT_URLOK", urlOk); + params.put("DS_MERCHANT_URLKO", urlKo); + params.put("DS_MERCHANT_MERCHANTURL", urlNotify); - Map mp = new HashMap<>(); - mp.put("DS_MERCHANT_AMOUNT", String.valueOf(req.amountCents())); - mp.put("DS_MERCHANT_ORDER", req.order()); - mp.put("DS_MERCHANT_MERCHANTCODE", merchantCode); - mp.put("DS_MERCHANT_CURRENCY", currency); - mp.put("DS_MERCHANT_TRANSACTIONTYPE", txType); - mp.put("DS_MERCHANT_TERMINAL", terminal); - mp.put("DS_MERCHANT_MERCHANTNAME", "Tu Comercio"); - mp.put("DS_MERCHANT_PRODUCTDESCRIPTION", req.description()); - mp.put("DS_MERCHANT_URLOK", urlOk); - mp.put("DS_MERCHANT_URLKO", urlKo); - mp.put("DS_MERCHANT_MERCHANTURL", urlNotify); + // JSON -> Base64 + String json = new ObjectMapper().writeValueAsString(params); + String merchantParametersB64 = Base64.getEncoder() + .encodeToString(json.getBytes(StandardCharsets.UTF_8)); - String merchantParameters = api.createMerchantParameters(mp); - String signature = api.createMerchantSignature(secretKey); + // Firma SHA-512 (tu JAR) + String signature = Signature.createMerchantSignature(secretKeyBase64, req.order(), merchantParametersB64); String action = "test".equalsIgnoreCase(env) ? "https://sis-t.redsys.es:25443/sis/realizarPago" : "https://sis.redsys.es/sis/realizarPago"; - return new FormPayload(action, "HMAC_SHA256_V1", merchantParameters, signature); + return new FormPayload(action, "HMAC_SHA512_V1", merchantParametersB64, signature); } - // Validación de la notificación on-line (webhook). - public RedsysNotification validateAndParse(String dsSignature, String dsSignatureVersion, String dsMerchantParametersB64) { - com.redsys.api.RedsysAPI api = new com.redsys.api.RedsysAPI(); + // ---------- STEP 3: Decodificar Ds_MerchantParameters ---------- + private static final ObjectMapper MAPPER = new ObjectMapper(); - // 1) Validar firma - String calc = api.createMerchantSignatureNotif(secretKey, dsMerchantParametersB64); - if (!Objects.equals(calc, dsSignature)) { - throw new IllegalArgumentException("Firma Redsys no válida"); + public Map decodeMerchantParametersToMap(String dsMerchantParametersB64) throws Exception { + try { + String json = Utils.decodeB64UrlSafeString( + dsMerchantParametersB64.getBytes(StandardCharsets.UTF_8) + ); + return MAPPER.readValue(json, new TypeReference>() {}); + } catch (Exception ignore) { + byte[] decoded = Base64.getDecoder().decode(dsMerchantParametersB64); + String json = new String(decoded, StandardCharsets.UTF_8); + return MAPPER.readValue(json, new TypeReference>() {}); } - - // 2) Decodificar parámetros - String json = api.decodeMerchantParameters(dsMerchantParametersB64); - Map params = new com.fasterxml.jackson.databind.ObjectMapper() - .readValue(json, new com.fasterxml.jackson.core.type.TypeReference<>() {}); - // Campos típicos: Ds_Order, Ds_Amount, Ds_Currency, Ds_Response, etc. - return RedsysNotification.from(params); } - public static record RedsysNotification(String order, String dsResponse, long amountCents, String currency) { - static RedsysNotification from(Map p) { - String order = (String) p.get("Ds_Order"); - String resp = String.valueOf(p.get("Ds_Response")); - long amount = Long.parseLong((String) p.get("Ds_Amount")); - String curr = String.valueOf(p.get("Ds_Currency")); - return new RedsysNotification(order, resp, amount, curr); + // ---------- STEP 4: Validar notificación ---------- + public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64) throws Exception { + Map mp = decodeMerchantParametersToMap(dsMerchantParametersB64); + RedsysNotification notif = new RedsysNotification(mp); + + if (notif.order == null || notif.order.isBlank()) { + throw new IllegalArgumentException("Falta Ds_Order en Ds_MerchantParameters"); } - // Éxito si 0–99. + + String expected = Signature.createMerchantSignature( + secretKeyBase64, notif.order, dsMerchantParametersB64 + ); + + if (!safeEqualsB64(dsSignature, expected)) { + throw new SecurityException("Firma Redsys no válida"); + } + + return notif; + } + + // ---------- HELPERS ---------- + private static boolean safeEqualsB64(String a, String b) { + if (Objects.equals(a, b)) return true; + try { + String na = normalizeB64(a); + String nb = normalizeB64(b); + byte[] da = Base64.getDecoder().decode(na); + byte[] db = Base64.getDecoder().decode(nb); + return MessageDigest.isEqual(da, db); + } catch (Exception e) { + return false; + } + } + + private static String normalizeB64(String s) { + if (s == null) return ""; + String n = s.replace('-', '+').replace('_', '/'); + int mod = n.length() % 4; + if (mod == 2) n += "=="; + else if (mod == 3) n += "="; + else if (mod == 1) n += "==="; + return n; + } + + // ---------- MODELO DE NOTIFICACIÓN ---------- + public static final class RedsysNotification { + public final Map raw; + public final String order; + public final String response; + public final long amountCents; + public final String currency; + + public RedsysNotification(Map raw) { + this.raw = raw; + this.order = str(raw.get("Ds_Order")); + this.response = str(raw.get("Ds_Response")); + this.currency = str(raw.get("Ds_Currency")); + this.amountCents = parseLongSafe(raw.get("Ds_Amount")); + } + public boolean authorized() { try { - int r = Integer.parseInt(dsResponse); + int r = Integer.parseInt(response); return r >= 0 && r <= 99; } catch (Exception e) { return false; } } + + private static String str(Object o) { return o == null ? null : String.valueOf(o); } + private static long parseLongSafe(Object o) { + try { return Long.parseLong(String.valueOf(o)); } catch (Exception e) { return 0L; } + } } - } diff --git a/src/main/resources/db/changelog/changesets/0006-add-cart-direcciones.yml b/src/main/resources/db/changelog/changesets/0006-add-cart-direcciones.yml index 91d4f5c..86bae3f 100644 --- a/src/main/resources/db/changelog/changesets/0006-add-cart-direcciones.yml +++ b/src/main/resources/db/changelog/changesets/0006-add-cart-direcciones.yml @@ -20,6 +20,12 @@ databaseChangeLog: primaryKey: true primaryKeyName: pk_cart_direcciones + - column: + name: cart_id + type: BIGINT + constraints: + nullable: false + - column: name: direccion_id type: BIGINT @@ -37,6 +43,18 @@ databaseChangeLog: type: INT constraints: nullable: true + + - column: + name: is_palets + type: TINYINT(1) + constraints: + nullable: false + defaultValue: false + + - column: + name: base + type: DECIMAL(12, 2) + - createIndex: indexName: idx_cart_dir_direccion_id diff --git a/src/main/resources/i18n/cart_es.properties b/src/main/resources/i18n/cart_es.properties index ffe04b7..66390f3 100644 --- a/src/main/resources/i18n/cart_es.properties +++ b/src/main/resources/i18n/cart_es.properties @@ -21,7 +21,9 @@ cart.shipping.ud=ud. cart.shipping.uds=uds. cart.shipping.enter-units=Introduzca el número de unidades para esta dirección: cart.shipping.units-label=Número de unidades (máximo {max}) - +cart.shipping.send-in-palets=Enviar en palets +cart.shipping.send-in-palets.info=Marque esta opción si desea que el envío se realice en palets (sólo para tiradas grandes). La entrega se realizará a pie de calle. +cart.shipping.tipo-envio=Tipo de envío: cart.shipping.errors.units-error=Por favor, introduzca un número válido entre 1 y {max}. cart.shipping.errors.noAddressSelected=Debe seleccionar una dirección de envío para el pedido. @@ -34,4 +36,6 @@ cart.resumen.iva-21=IVA 21%: 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. \ No newline at end of file +cart.resumen.fidelizacion=Si tiene descuento por fidelización, se aplicará al tramitar el pedido. + +cart.errors.update-cart=Error al actualizar la cesta de la compra: {0} \ No newline at end of file diff --git a/src/main/resources/static/assets/css/imprimelibros.css b/src/main/resources/static/assets/css/imprimelibros.css index aafd10a..b426ded 100644 --- a/src/main/resources/static/assets/css/imprimelibros.css +++ b/src/main/resources/static/assets/css/imprimelibros.css @@ -29,3 +29,13 @@ body { color: #fff; border-color: #92b2a7; } + +/* Solo dentro del modal */ +.swal2-popup .form-switch-custom { + font-size: 1rem; /* clave: fija el tamaño base del switch */ + line-height: 1.5; +} +.swal2-popup .form-switch-custom .form-check-input { + float: none; /* por si acaso */ + margin: 0; +} 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 40aa6ce..374e92f 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 @@ -21,7 +21,7 @@ $(() => { $(this).find('.presupuesto-id').attr('name', 'direcciones[' + i + '].presupuestoId'); if($(this).find('.item-tirada').length > 0 && $(this).find('.item-tirada').val() !== null && $(this).find('.item-tirada').val() !== "") - $(this).find('.item-tirada').attr('name', 'direcciones[' + i + '].tirada'); + $(this).find('.item-tirada').attr('name', 'direcciones[' + i + '].unidades'); }); $.post(form.attr('action'), form.serialize(), (response) => { // handle response 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 6af9089..4242916 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 @@ -7,23 +7,23 @@ $(() => { $("#onlyOneShipment").on('change', function () { if ($(this).is(':checked')) { - $('.nav-product').hide(); + $('.nav-product').addClass('d-none'); document.querySelectorAll('.card.product').forEach(card => { const detailsBtn = card.querySelector('.nav-link[id^="pills-details-"][id$="-tab"]'); - if (detailsBtn) new bootstrap.Tab(detailsBtn).show(); + if (detailsBtn) $(new bootstrap.Tab(detailsBtn)).removeClass('d-none'); }); - $('#shippingAddressesContainer').empty().show(); + $('#shippingAddressesContainer').empty().removeClass('d-none'); $('.shipping-addresses-item').toArray().forEach(element => { - $(element).empty().hide(); + $(element).empty().addClass('d-none'); }); - $('#addOrderAddress').show(); + $('#addOrderAddress').removeClass('d-none'); } else { - $('.nav-product').show(); - $('#shippingAddressesContainer').empty().hide(); + $('.nav-product').removeClass('d-none'); + $('#shippingAddressesContainer').empty().addClass('d-none'); $('.shipping-addresses-item').toArray().forEach(element => { - $(element).empty().show(); + $(element).empty().removeClass('d-none'); }); - $('#addOrderAddress').hide(); + $('#addOrderAddress').addClass('d-none'); } $(document).trigger('updateCart'); }); @@ -34,7 +34,7 @@ $(() => { const $div = $card.parent(); $card.remove(); if ($div.hasClass('shipping-order-address')) { - $('#addOrderAddress').show(); + $('#addOrderAddress').removeClass('d-none'); } else { $div.trigger('direcciones:actualizadas'); @@ -50,19 +50,27 @@ $(() => { const tirada = $(this).closest('.product').find('.item-tirada').val(); const totalTirada = container.find('.item-tirada').toArray().reduce((acc, el) => acc + parseInt($(el).val() || 0), 0); const remainingTirada = parseInt(tirada) - parseInt(totalTirada) + parseInt($card.find('.item-tirada').val() || 0); - const units = getUnitsFromUser(remainingTirada); - units.then(unidades => { - if (unidades) { - $card.find('.item-tirada').val(unidades); + const data = getUnitsFromUser(remainingTirada); + data.then(data => { + if (data.unidades) { + $card.find('.item-tirada').val(data.unidades); $card.find('#units-text').each(function () { - if (unidades == 1) { - $(this).text(`${unidades} ${window.languageBundle['cart.shipping.ud'] || 'unidad'}`); + if (data.unidades == 1) { + $(this).text(`${data.unidades} ${window.languageBundle['cart.shipping.ud'] || 'unidad'}`); } else { - $(this).text(`${unidades} ${window.languageBundle['cart.shipping.uds'] || 'unidades'}`); + $(this).text(`${data.unidades} ${window.languageBundle['cart.shipping.uds'] || 'unidades'}`); } }); container.trigger('direcciones:actualizadas'); $(document).trigger('updateCart'); + $card.find('.is-palets').val(data.isPalets ? 'true' : 'false'); + $card.find('.icon-shipment').each(function () { + if (data.isPalets) { + $(this).removeClass('la-box').addClass('la-pallet'); + } else { + $(this).removeClass('la-pallet').addClass('la-box'); + } + }); } }); @@ -87,7 +95,7 @@ $(() => { $('#addOrderAddress').on('click', async () => { if ($('#onlyOneShipment').is(':checked')) { if (await seleccionarDireccionEnvio()) { - $('#addOrderAddress').hide(); + $('#addOrderAddress').addClass('d-none'); } } }); @@ -210,16 +218,27 @@ $(() => { $('#direccionFormModalBody').html(html); const title = $('#direccionFormModalBody #direccionForm').data('add'); $('#direccionFormModal .modal-title').text(title); - modal.show(); + modal.removeClass('d-none'); }); } let unidades = null; + let isPalets = 0; if (tirada !== null && tirada >= 1 && direccionId) { - const unidadesValue = await getUnitsFromUser(tirada); - if (unidadesValue) { - unidades = parseInt(unidadesValue); + const data = await getUnitsFromUser(tirada); + if (data && data.unidades) { + unidades = parseInt(data.unidades); + isPalets = data.isPalets ? 1 : 0; + } else { + // Si el usuario cancela, salir de la función + return false; + } + } + else if(presupuestoId == null){ // caso para todas los envios a la misma direccion + const isPaletsValue = await getTipoEnvio(); + if (isPaletsValue !== null) { + isPalets = isPaletsValue ? 1 : 0; } else { // Si el usuario cancela, salir de la función return false; @@ -228,9 +247,9 @@ $(() => { if (direccionId) { // Obtén el objeto completo seleccionado showLoader(); - let uri = `/cart/get-address/${direccionId}`; + let uri = `/cart/get-address/${direccionId}?isPalets=${isPalets}`; if (presupuestoId !== null) { - uri += `?presupuestoId=${presupuestoId}`; + uri += `&presupuestoId=${presupuestoId}`; if (tirada !== null) { uri += `&unidades=${unidades}`; } @@ -255,18 +274,29 @@ $(() => { } async function getUnitsFromUser(tirada) { - - // Swal preguntando numero de unidades a asignar con máximo de tirada, necesito guardar el valor - const { value: unidadesValue } = await Swal.fire({ + const { value: formValues } = await Swal.fire({ title: window.languageBundle['cart.shipping.enter-units'] || 'Introduzca el número de unidades para esta dirección', - input: 'number', - inputLabel: window.languageBundle['cart.shipping.units-label']?.replace('{max}', tirada) || `Número de unidades (máximo ${tirada})`, - inputAttributes: { - min: 1, - max: tirada, - step: 1, - }, - inputValue: tirada, + 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, customClass: { @@ -275,14 +305,60 @@ $(() => { }, confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar', cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar', - inputValidator: (value) => { - if (!value || isNaN(value) || value < 1 || value > tirada) { - return window.languageBundle['cart.shipping.errors.units-error']?.replace('{max}', tirada) || `Por favor, introduzca un número válido entre 1 y ${tirada}.`; + preConfirm: () => { + const unidades = parseInt(document.getElementById('swal-input-unidades').value, 10); + const isPalets = document.getElementById('swal-input-palets').checked; + + if (!unidades || isNaN(unidades) || unidades < 1 || unidades > tirada) { + Swal.showValidationMessage( + window.languageBundle['cart.shipping.errors.units-error']?.replace('{max}', tirada) + || `Por favor, introduzca un número válido entre 1 y ${tirada}.` + ); + return false; } - return null; + return { unidades, isPalets }; } }); - return unidadesValue; + + if (formValues) { + return formValues; // { unidades: number, isPalets: boolean } + } + return null; // Si se cancela el Swal + } + + async function getTipoEnvio() { + const { value: checkValue } = 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, + customClass: { + confirmButton: 'btn btn-secondary me-2', + cancelButton: 'btn btn-light', + }, + confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar', + cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar', + preConfirm: () => { + const isPalets = document.getElementById('swal-input-palets').checked; + return isPalets; + } + }); + + if (checkValue !== undefined) { + return checkValue; // boolean + } + return null; // Si se cancela el Swal } function checkTotalUnits(container, tirada) { @@ -294,8 +370,8 @@ $(() => { if (totalUnits < tirada) { return false; } - if(container.find('.product').closest('.shipping-addresses-sample')){ - if(container.find('.direccion-card').toArray().length === 0){ + if (container.find('.product').closest('.shipping-addresses-sample')) { + if (container.find('.direccion-card').toArray().length === 0) { return false; } } @@ -308,21 +384,21 @@ $(() => { const container = $(this); if (!checkTotalUnits(container, tirada)) { - container.closest('.px-2').find('.btn-add-shipping').show(); + container.closest('.px-2').find('.btn-add-shipping').removeClass('d-none'); } else { - container.closest('.px-2').find('.btn-add-shipping').hide(); + container.closest('.px-2').find('.btn-add-shipping').addClass('d-none'); } }); $(document).on('direcciones:actualizadas', '.shipping-addresses-sample', function (e) { - + const container = $(this); if (container.find('.direccion-card').toArray().length === 0) { - container.closest('.px-2').find('.btn-add-shipping-sample').show(); + container.closest('.px-2').find('.btn-add-shipping-sample').removeClass('d-none'); } else { - container.closest('.px-2').find('.btn-add-shipping-sample').hide(); + container.closest('.px-2').find('.btn-add-shipping-sample').addClass('d-none'); } }); @@ -345,7 +421,7 @@ $(() => { return; } // Éxito real: cerrar y recargar tabla - modal.hide(); + modal.addClass('d-none'); seleccionarDireccionEnvio(); }, error: function (xhr) { diff --git a/src/main/resources/templates/imprimelibros/cart/_cartContent.html b/src/main/resources/templates/imprimelibros/cart/_cartContent.html index 61b7862..7421730 100644 --- a/src/main/resources/templates/imprimelibros/cart/_cartContent.html +++ b/src/main/resources/templates/imprimelibros/cart/_cartContent.html @@ -10,26 +10,28 @@
+ -
+ - +

+ + th:field="${cart.onlyOneShipment}"/>
-
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 @@
-