From 167c136dca031d8ee4156843d86baad4770899f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Thu, 30 Oct 2025 19:48:26 +0100 Subject: [PATCH] falta actualizar bien el resumen --- pom.xml | 8 +- .../java/com/imprimelibros/erp/cart/Cart.java | 23 +++ .../erp/cart/CartController.java | 18 ++- .../imprimelibros/erp/cart/CartDireccion.java | 77 ++++++--- .../com/imprimelibros/erp/cart/CartItem.java | 11 +- .../erp/cart/CartRepository.java | 13 ++ .../imprimelibros/erp/cart/CartService.java | 134 +++++++++++++--- .../erp/cart/dto/DireccionCardDTO.java | 39 +++++ .../erp/checkout/CheckoutController.java | 9 -- .../erp/direcciones/DireccionService.java | 17 +- .../erp/externalApi/skApiClient.java | 8 +- .../imprimelibros/erp/pdf/PdfRenderer.java | 2 - .../erp/redsys/RedsysController.java | 30 +++- .../erp/redsys/RedsysService.java | 145 +++++++++-------- src/main/resources/application.properties | 16 +- .../0005-add-carts-onlyoneshipment.yml | 9 ++ .../changesets/0006-add-cart-direcciones.yml | 7 +- src/main/resources/i18n/cart_es.properties | 4 +- src/main/resources/i18n/pedidos_es.properties | 5 +- src/main/resources/lib/apiSha256.jar | Bin 0 -> 13169 bytes src/main/resources/lib/apiSha512V2.jar | Bin 5479 -> 0 bytes .../js/pages/imprimelibros/cart/cart.js | 5 +- .../pages/imprimelibros/cart/shipping-cart.js | 51 +++--- .../imprimelibros/cart/_cartContent.html | 24 ++- .../imprimelibros/cart/_cartItem.html | 39 ++++- .../imprimelibros/cart/_cartSummary.html | 14 +- .../direcciones/direccionCard.html | 2 +- .../erp/redsys/RedsysServiceTest.java | 150 ------------------ 28 files changed, 518 insertions(+), 342 deletions(-) create mode 100644 src/main/java/com/imprimelibros/erp/cart/dto/DireccionCardDTO.java create mode 100644 src/main/resources/lib/apiSha256.jar delete mode 100644 src/main/resources/lib/apiSha512V2.jar delete mode 100644 src/test/java/com/imprimelibros/erp/redsys/RedsysServiceTest.java diff --git a/pom.xml b/pom.xml index a660368..404dffd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.6 + 3.5.7 com.imprimelibros @@ -154,10 +154,10 @@ sis.redsys - apiSha512V2 - 2.0 + apiSha256 + 1.0 system - ${project.basedir}/src/main/resources/lib/apiSha512V2.jar + ${project.basedir}/src/main/resources/lib/apiSha256.jar diff --git a/src/main/java/com/imprimelibros/erp/cart/Cart.java b/src/main/java/com/imprimelibros/erp/cart/Cart.java index 82dea7f..557636f 100644 --- a/src/main/java/com/imprimelibros/erp/cart/Cart.java +++ b/src/main/java/com/imprimelibros/erp/cart/Cart.java @@ -1,6 +1,8 @@ package com.imprimelibros.erp.cart; import jakarta.persistence.*; + +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -43,6 +45,9 @@ public class Cart { @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List direcciones = new ArrayList<>(); + @Column(name = "total", nullable = false) + private BigDecimal total = BigDecimal.ZERO; + @PreUpdate public void preUpdate() { this.updatedAt = LocalDateTime.now(); @@ -85,6 +90,14 @@ public class Cart { this.onlyOneShipment = onlyOneShipment; } + public BigDecimal getTotal() { + return total; + } + + public void setTotal(BigDecimal total) { + this.total = total; + } + public LocalDateTime getCreatedAt() { return createdAt; } @@ -108,4 +121,14 @@ public class Cart { public void setDirecciones(List direcciones) { this.direcciones = direcciones; } + + public void addDireccion(CartDireccion d) { + direcciones.add(d); + d.setCart(this); + } + + public void removeDireccion(CartDireccion d) { + direcciones.remove(d); + d.setCart(null); + } } diff --git a/src/main/java/com/imprimelibros/erp/cart/CartController.java b/src/main/java/com/imprimelibros/erp/cart/CartController.java index f97dc2b..da93051 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartController.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartController.java @@ -72,8 +72,18 @@ public class CartController { var items = service.listItems(userId, locale); model.addAttribute("items", items); + Map direcciones = service.getCartDirecciones(cart.getId(), locale); + if(direcciones != null && direcciones.containsKey("mainDir")) + model.addAttribute("mainDir", direcciones.get("mainDir")); + else if(direcciones != null && direcciones.containsKey("direcciones")) + model.addAttribute("direcciones", direcciones.get("direcciones")); + var summary = service.getCartSummary(cart, locale); model.addAttribute("cartSummary", summary); + if(summary.get("errorShipmentCost") != null && (Boolean)summary.get("errorShipmentCost")) + model.addAttribute("errorEnvio", true); + else + model.addAttribute("errorEnvio", false); model.addAttribute("cart", cart); return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple) @@ -148,7 +158,7 @@ public class CartController { } @PostMapping(value = "/update/{id}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) - public String updateCart(@PathVariable Long id, UpdateCartRequest updateRequest, Model model, Locale locale) { + public String updateCart(@PathVariable Long id, UpdateCartRequest updateRequest, Model model, Locale locale, Principal principal) { try { service.updateCart(id, updateRequest); @@ -159,8 +169,10 @@ public class CartController { } catch (Exception e) { - model.addAttribute("errorMessage", messageSource.getMessage("cart.errors.update-cart", new Object[]{e.getMessage()}, locale)); - return "/cart"; // templates/error/500.html + // redirect to cart with error message + String errorMessage = messageSource.getMessage("cart.update.error", null, "Error updating cart", locale); + model.addAttribute("errorMessage", errorMessage); + return "redirect:/cart"; } } diff --git a/src/main/java/com/imprimelibros/erp/cart/CartDireccion.java b/src/main/java/com/imprimelibros/erp/cart/CartDireccion.java index 8edda9e..dd94623 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartDireccion.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartDireccion.java @@ -1,7 +1,9 @@ package com.imprimelibros.erp.cart; -import java.math.BigDecimal; +import java.util.Locale; +import org.springframework.context.MessageSource; +import com.imprimelibros.erp.cart.dto.DireccionCardDTO; import com.imprimelibros.erp.direcciones.Direccion; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; @@ -33,28 +35,67 @@ public class CartDireccion { @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 Long getId() { + return id; + } - public Cart getCart() { return cart; } - public void setCart(Cart cart) { this.cart = cart; } + public void setId(Long id) { + this.id = id; + } - public Direccion getDireccion() { return direccion; } - public void setDireccion(Direccion direccion) { this.direccion = direccion; } + public Cart getCart() { + return cart; + } - public Presupuesto getPresupuesto() { return presupuesto; } - public void setPresupuesto(Presupuesto presupuesto) { this.presupuesto = presupuesto; } + public void setCart(Cart cart) { + this.cart = cart; + } - public Integer getUnidades() { return unidades; } - public void setUnidades(Integer unidades) { this.unidades = unidades; } + public Direccion getDireccion() { + return direccion; + } - public BigDecimal getBase() { return base; } - public void setBase(BigDecimal base) { this.base = base; } + 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 Boolean getIsPalets() { + return isPalets; + } + + public void setIsPalets(Boolean isPalets) { + this.isPalets = isPalets; + } + + public DireccionCardDTO toDireccionCard(MessageSource messageSource, Locale locale) { + + String pais = messageSource.getMessage("paises." + this.direccion.getPais().getKeyword(), null, + this.direccion.getPais().getKeyword(), locale); + + return new DireccionCardDTO( + this.direccion, + this.presupuesto != null ? this.presupuesto.getId() : null, + this.unidades, + this.isPalets, + pais + ); + } - public Boolean getIsPalets() { return isPalets; } - public void setIsPalets(Boolean isPalets) { this.isPalets = isPalets; } } diff --git a/src/main/java/com/imprimelibros/erp/cart/CartItem.java b/src/main/java/com/imprimelibros/erp/cart/CartItem.java index 853214c..7579201 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartItem.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartItem.java @@ -3,6 +3,8 @@ package com.imprimelibros.erp.cart; import jakarta.persistence.*; import java.time.LocalDateTime; +import com.imprimelibros.erp.presupuesto.dto.Presupuesto; + @Entity @Table( name = "cart_items", @@ -17,8 +19,9 @@ public class CartItem { @JoinColumn(name = "cart_id", nullable = false) private Cart cart; - @Column(name = "presupuesto_id", nullable = false) - private Long presupuestoId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "presupuesto_id", nullable = false) + private Presupuesto presupuesto; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt = LocalDateTime.now(); @@ -29,8 +32,8 @@ public class CartItem { public Cart getCart() { return cart; } public void setCart(Cart cart) { this.cart = cart; } - public Long getPresupuestoId() { return presupuestoId; } - public void setPresupuestoId(Long presupuestoId) { this.presupuestoId = presupuestoId; } + public Presupuesto getPresupuesto() { return presupuesto; } + public void setPresupuesto(Presupuesto presupuesto) { this.presupuesto = presupuesto; } public LocalDateTime getCreatedAt() { return createdAt; } } diff --git a/src/main/java/com/imprimelibros/erp/cart/CartRepository.java b/src/main/java/com/imprimelibros/erp/cart/CartRepository.java index d23d60a..2f1ad0c 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartRepository.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartRepository.java @@ -1,9 +1,22 @@ package com.imprimelibros.erp.cart; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; public interface CartRepository extends JpaRepository { Optional findByUserIdAndStatus(Long userId, Cart.Status status); + + @Query(""" + select distinct c from Cart c + left join fetch c.direcciones cd + left join fetch cd.direccion d + left join fetch d.pais p + left join fetch cd.presupuesto pr + where c.id = :id + """) + Optional findByIdFetchAll(@Param("id") Long id); + } diff --git a/src/main/java/com/imprimelibros/erp/cart/CartService.java b/src/main/java/com/imprimelibros/erp/cart/CartService.java index 8617535..280dfc8 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartService.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartService.java @@ -1,6 +1,6 @@ package com.imprimelibros.erp.cart; -import jakarta.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional; import org.springframework.context.MessageSource; import org.springframework.stereotype.Service; @@ -10,9 +10,12 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; +import com.imprimelibros.erp.cart.dto.DireccionCardDTO; +import com.imprimelibros.erp.cart.dto.DireccionShipment; import com.imprimelibros.erp.cart.dto.UpdateCartRequest; import com.imprimelibros.erp.common.Utils; import com.imprimelibros.erp.direcciones.DireccionService; @@ -68,9 +71,7 @@ public class CartService { List items = itemRepo.findByCartId(cart.getId()); for (CartItem item : items) { - Presupuesto p = presupuestoRepo.findById(item.getPresupuestoId()) - .orElseThrow( - () -> new IllegalStateException("Presupuesto no encontrado: " + item.getPresupuestoId())); + Presupuesto p = item.getPresupuesto(); Map elemento = getElementoCart(p, locale); elemento.put("cartItemId", item.getId()); @@ -88,7 +89,8 @@ public class CartService { if (!exists) { CartItem ci = new CartItem(); ci.setCart(cart); - ci.setPresupuestoId(presupuestoId); + ci.setPresupuesto(presupuestoRepo.findById(presupuestoId) + .orElseThrow(() -> new IllegalArgumentException("Presupuesto no encontrado"))); itemRepo.save(ci); } } @@ -165,58 +167,140 @@ public class CartService { } public Map getCartSummary(Cart cart, Locale locale) { - + double base = 0.0; double iva4 = 0.0; double iva21 = 0.0; + double shipment = 0.0; + + Boolean errorShipementCost = false; 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())); + Presupuesto p = item.getPresupuesto(); base += p.getBaseImponible().doubleValue(); iva4 += p.getIvaImporte4().doubleValue(); iva21 += p.getIvaImporte21().doubleValue(); - if(cart.getOnlyOneShipment() != null && cart.getOnlyOneShipment()) { + 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) { + 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); - + 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) { + errorShipementCost = true; + } + } + // 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) { + errorShipementCost = true; + } } } } } - double total = base + iva4 + iva21; + double total = base + iva4 + iva21 + shipment; 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("total", Utils.formatCurrency(total, locale)); + summary.put("errorShipmentCost", errorShipementCost); return summary; } + @Transactional(readOnly = true) + public Map getCartDirecciones(Long cartId, Locale locale) { + Cart cart = cartRepo.findByIdFetchAll(cartId) + .orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado")); + + Map result = new HashMap<>(); + List direcciones = cart.getDirecciones(); + + if (cart.getOnlyOneShipment() && !direcciones.isEmpty()) { + result.put("mainDir", direcciones.get(0).toDireccionCard(messageSource, locale)); + } else { + List dirCards = cart.getDirecciones().stream() + .filter(Objects::nonNull) + .map(cd -> cd.toDireccionCard(messageSource, locale)) + .filter(Objects::nonNull) + .toList(); + result.put("direcciones", dirCards); + } + + return result; + } + @Transactional public Boolean updateCart(Long cartId, UpdateCartRequest request) { - try{ - Cart cart = cartRepo.findById(cartId).orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado")); + try { + Cart cart = cartRepo.findById(cartId) + .orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado")); cart.setOnlyOneShipment(request.isOnlyOneShipment()); + // Borramos todas las direcciones actuales de la bbdd + // Opcional (limpieza): romper backref antes de clear + for (CartDireccion d : cart.getDirecciones()) { + d.setCart(null); + } + cart.getDirecciones().clear(); + // Guardamos las direcciones + List direcciones = request.getDirecciones(); + if (direcciones != null && direcciones.size() > 0) { + for (DireccionShipment dir : direcciones) { + // Crear una nueva CartDireccion por cada item + CartDireccion cd = new CartDireccion(); + cd.setCart(cart); + cd.setDireccion(dir.getId() != null ? direccionService.findById(dir.getId()) + .orElseThrow(() -> new IllegalArgumentException("Dirección no encontrada")) : null); + cd.setIsPalets(dir.getIsPalets() != null ? dir.getIsPalets() : false); + cd.setUnidades(dir.getUnidades() != null ? dir.getUnidades() : null); + if (dir.getPresupuestoId() != null) { + Presupuesto p = presupuestoRepo.findById(dir.getPresupuestoId()) + .orElse(null); + cd.setPresupuesto(p); + } + cart.addDireccion(cd); + } + } else { + + } cartRepo.save(cart); return true; } catch (Exception e) { diff --git a/src/main/java/com/imprimelibros/erp/cart/dto/DireccionCardDTO.java b/src/main/java/com/imprimelibros/erp/cart/dto/DireccionCardDTO.java new file mode 100644 index 0000000..fb3da47 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/dto/DireccionCardDTO.java @@ -0,0 +1,39 @@ +package com.imprimelibros.erp.cart.dto; + +import com.imprimelibros.erp.direcciones.Direccion; + +public class DireccionCardDTO { + private final Direccion direccion; + private final Long presupuestoId; + private final Integer unidades; + private final Boolean isPalets; + private final String pais; + + public DireccionCardDTO(Direccion direccion, Long presupuestoId, Integer unidades, Boolean isPalets, String pais) { + this.direccion = direccion; + this.presupuestoId = presupuestoId; + this.unidades = unidades; + this.isPalets = isPalets; + this.pais = pais; + } + + public Direccion getDireccion() { + return direccion; + } + + public Long getPresupuestoId() { + return presupuestoId; + } + + public Integer getUnidades() { + return unidades; + } + + public Boolean getIsPalets() { + return isPalets; + } + + public String getPais() { + return pais; + } +} diff --git a/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java index c8a3810..00474fc 100644 --- a/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java +++ b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java @@ -6,24 +6,15 @@ 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; diff --git a/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java index fc339c0..c3dd36f 100644 --- a/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java +++ b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java @@ -4,14 +4,11 @@ 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 { @@ -84,13 +81,15 @@ public class DireccionService { } 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; - } + if (paisCode3 != null && paisCode3.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 } - return false; } + return false; +} } diff --git a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java index 26bbd76..e586f1f 100644 --- a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java +++ b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java @@ -19,6 +19,7 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta; import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion; import java.util.Map; +import java.util.Optional; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.URI; @@ -253,7 +254,12 @@ public class skApiClient { if (error != null && error) { return Map.of("error", messageSource.getMessage("direcciones.error.noShippingCost", null, locale)); } else { - Double total = (Double) responseBody.get("data"); + Double total = Optional.ofNullable(responseBody.get("data")) + .filter(Number.class::isInstance) + .map(Number.class::cast) + .map(Number::doubleValue) + .orElse(0.0); + return Map.of("data", total); } } catch (JsonProcessingException e) { diff --git a/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java b/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java index 79c2db5..079bab3 100644 --- a/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java +++ b/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java @@ -1,8 +1,6 @@ package com.imprimelibros.erp.pdf; import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder; -import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; -import org.springframework.core.io.Resource; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java index 032ad84..921b224 100644 --- a/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java +++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java @@ -1,6 +1,5 @@ package com.imprimelibros.erp.redsys; -import com.imprimelibros.erp.redsys.RedsysService; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -28,7 +27,7 @@ public class RedsysController { model.addAttribute("signatureVersion", form.signatureVersion()); model.addAttribute("merchantParameters", form.merchantParameters()); model.addAttribute("signature", form.signature()); - return "payments/redsys-redirect"; + return "imprimelibros/payments/redsys-redirect"; } @PostMapping("/notify") @@ -54,4 +53,31 @@ public class RedsysController { } } + @PostMapping("/ok") + public String okReturn(@RequestParam("Ds_Signature") String dsSignature, + @RequestParam("Ds_MerchantParameters") String dsMerchantParameters, + Model model) { + try { + RedsysService.RedsysNotification notif = service.validateAndParseNotification(dsSignature, dsMerchantParameters); + // Aquí puedes validar importe/pedido/moneda con tu base de datos y marcar como + // pagado + model.addAttribute("authorized", notif.authorized()); + //model.addAttribute("order", notif.order()); + //model.addAttribute("amountCents", notif.amountCents()); + return "imprimelibros/payments/redsys-ok"; + } catch (Exception e) { + model.addAttribute("error", "No se pudo validar la respuesta de Redsys."); + return "imprimelibros/payments/redsys-ko"; + } + } + + @PostMapping("/ko") + public String koReturn(@RequestParam(value = "Ds_Signature", required = false) String dsSignature, + @RequestParam(value = "Ds_MerchantParameters", required = false) String dsMerchantParameters, + Model model) { + // Suele venir cuando el usuario cancela o hay error + model.addAttribute("error", "Operación cancelada o rechazada."); + return "imprimelibros/payments/redsys-ko"; + } + } diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java index c3e66ff..df8c484 100644 --- a/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java +++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java @@ -3,8 +3,7 @@ package com.imprimelibros.erp.redsys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import sis.redsys.api.Signature; -import sis.redsys.api.Utils; +import sis.redsys.api.ApiMacSha256; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -12,7 +11,6 @@ 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; @@ -20,48 +18,54 @@ import java.util.Objects; 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 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; + @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 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 record PaymentRequest(String order, long amountCents, String description) { + } + + public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) { + } // ---------- 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); + ApiMacSha256 api = new ApiMacSha256(); - // JSON -> Base64 - String json = new ObjectMapper().writeValueAsString(params); - String merchantParametersB64 = Base64.getEncoder() - .encodeToString(json.getBytes(StandardCharsets.UTF_8)); + api.setParameter("DS_MERCHANT_AMOUNT", String.valueOf(req.amountCents())); + api.setParameter("DS_MERCHANT_ORDER", req.order()); // Usa 12 dígitos con ceros si puedes + api.setParameter("DS_MERCHANT_MERCHANTCODE", merchantCode); + api.setParameter("DS_MERCHANT_CURRENCY", currency); + api.setParameter("DS_MERCHANT_TRANSACTIONTYPE", txType); + api.setParameter("DS_MERCHANT_TERMINAL", terminal); + api.setParameter("DS_MERCHANT_MERCHANTURL", urlNotify); + api.setParameter("DS_MERCHANT_URLOK", urlOk); + api.setParameter("DS_MERCHANT_URLKO", urlKo); - // Firma SHA-512 (tu JAR) - String signature = Signature.createMerchantSignature(secretKeyBase64, req.order(), merchantParametersB64); + String merchantParameters = api.createMerchantParameters(); + String signature = api.createMerchantSignature(secretKeyBase64); String action = "test".equalsIgnoreCase(env) ? "https://sis-t.redsys.es:25443/sis/realizarPago" : "https://sis.redsys.es/sis/realizarPago"; - return new FormPayload(action, "HMAC_SHA512_V1", merchantParametersB64, signature); + return new FormPayload(action, "HMAC_SHA256_V1", merchantParameters, signature); } // ---------- STEP 3: Decodificar Ds_MerchantParameters ---------- @@ -69,40 +73,42 @@ public class RedsysService { 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>() {}); + return MAPPER.readValue(json, new TypeReference<>() { + }); + } catch (Exception e) { + throw new IllegalArgumentException("No se pudo decodificar Ds_MerchantParameters", e); } } // ---------- STEP 4: Validar notificación ---------- - public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64) throws Exception { - Map mp = decodeMerchantParametersToMap(dsMerchantParametersB64); - RedsysNotification notif = new RedsysNotification(mp); + 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"); - } - - String expected = Signature.createMerchantSignature( - secretKeyBase64, notif.order, dsMerchantParametersB64 - ); - - if (!safeEqualsB64(dsSignature, expected)) { - throw new SecurityException("Firma Redsys no válida"); - } - - return notif; + if (notif.order == null || notif.order.isBlank()) { + throw new IllegalArgumentException("Falta Ds_Order en Ds_MerchantParameters"); } + ApiMacSha256 api = new ApiMacSha256(); + api.setParameter("Ds_MerchantParameters", dsMerchantParametersB64); + + String expected = api.createMerchantSignatureNotif(secretKeyBase64, api.decodeMerchantParameters(dsMerchantParametersB64)); // ✅ SOLO UN PARÁMETRO + + 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; + if (Objects.equals(a, b)) + return true; try { String na = normalizeB64(a); String nb = normalizeB64(b); @@ -115,12 +121,16 @@ public class RedsysService { } private static String normalizeB64(String s) { - if (s == null) return ""; + 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 += "==="; + if (mod == 2) + n += "=="; + else if (mod == 3) + n += "="; + else if (mod == 1) + n += "==="; return n; } @@ -144,12 +154,21 @@ public class RedsysService { try { int r = Integer.parseInt(response); return r >= 0 && r <= 99; - } catch (Exception e) { return false; } + } catch (Exception e) { + return false; + } + } + + private static String str(Object o) { + return o == null ? null : String.valueOf(o); } - 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; } + try { + return Long.parseLong(String.valueOf(o)); + } catch (Exception e) { + return 0L; + } } } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f089b65..d280e5d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -28,8 +28,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 @@ -114,7 +114,13 @@ redsys.terminal=1 redsys.currency=978 redsys.transaction-type=0 redsys.secret-key=sq7HjrUOBfKmC576ILgskD5srU870gJ7 -redsys.urls.ok=https://localhost:8080/pagos/redsys/ok -redsys.urls.ko=https://localhost:8080/pagos/redsys/ko -redsys.urls.notify=https://localhost:8080/pagos/redsys/notify +redsys.urls.ok=http://localhost:8080/pagos/redsys/ok +redsys.urls.ko=http://localhost:8080/pagos/redsys/ko +redsys.urls.notify=http://localhost:8080/pagos/redsys/notify + +# Mensajes de error mas cortos +# Oculta el stack trace en los errores del servidor +server.error.include-stacktrace=never +# No mostrar el mensaje completo de excepción en la respuesta +server.error.include-message=always \ No newline at end of file diff --git a/src/main/resources/db/changelog/changesets/0005-add-carts-onlyoneshipment.yml b/src/main/resources/db/changelog/changesets/0005-add-carts-onlyoneshipment.yml index deb12ec..4dae461 100644 --- a/src/main/resources/db/changelog/changesets/0005-add-carts-onlyoneshipment.yml +++ b/src/main/resources/db/changelog/changesets/0005-add-carts-onlyoneshipment.yml @@ -22,6 +22,15 @@ databaseChangeLog: constraints: nullable: false + - column: + name: total + type: DECIMAL(19,6) + defaultValueNumeric: 0 + remarks: "Total del carrito" + afterColumn: only_one_shipment + constraints: + nullable: false + rollback: - dropColumn: tableName: carts 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 86bae3f..41a1177 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 @@ -50,12 +50,7 @@ databaseChangeLog: constraints: nullable: false defaultValue: false - - - column: - name: base - type: DECIMAL(12, 2) - - + - createIndex: indexName: idx_cart_dir_direccion_id tableName: cart_direcciones diff --git a/src/main/resources/i18n/cart_es.properties b/src/main/resources/i18n/cart_es.properties index 66390f3..ab73deb 100644 --- a/src/main/resources/i18n/cart_es.properties +++ b/src/main/resources/i18n/cart_es.properties @@ -31,6 +31,7 @@ cart.shipping.errors.fillAddressesItems=Debe seleccionar una dirección de enví 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: @@ -38,4 +39,5 @@ cart.resumen.tramitar=Tramitar pedido 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 +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/i18n/pedidos_es.properties b/src/main/resources/i18n/pedidos_es.properties index 76db42f..a56dd01 100644 --- a/src/main/resources/i18n/pedidos_es.properties +++ b/src/main/resources/i18n/pedidos_es.properties @@ -11,4 +11,7 @@ checkout.shipping.onlyOneShipment=Todo el pedido se envía a una única direcci checkout.summary.presupuesto=#Presupuesto checkout.summary.titulo=Título -checkout.summary.base=Base \ No newline at end of file +checkout.summary.base=Base +checkout.summary.iva-4=IVA 4% +checkout.summary.iva-21=IVA 21% +checkout.summary.envio=Envío \ No newline at end of file diff --git a/src/main/resources/lib/apiSha256.jar b/src/main/resources/lib/apiSha256.jar new file mode 100644 index 0000000000000000000000000000000000000000..83fbf6c21f3d77039813f57e7964bf54eb00f2b0 GIT binary patch literal 13169 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 J6fOYZe*lq;=(7L- 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 374e92f..7f73011 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 @@ -24,7 +24,10 @@ $(() => { $(this).find('.item-tirada').attr('name', 'direcciones[' + i + '].unidades'); }); $.post(form.attr('action'), form.serialize(), (response) => { - // handle response + // if success and received html, replace container summary + if (response) { + $('.cart-summary-container').replaceWith(response); + } }).always(() => { hideLoader(); }); 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 4242916..ab7edfe 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 @@ -10,7 +10,7 @@ $(() => { $('.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)).removeClass('d-none'); + if (detailsBtn) new bootstrap.Tab(detailsBtn).show(); }); $('#shippingAddressesContainer').empty().removeClass('d-none'); $('.shipping-addresses-item').toArray().forEach(element => { @@ -235,7 +235,7 @@ $(() => { return false; } } - else if(presupuestoId == null){ // caso para todas los envios a la misma direccion + else if (presupuestoId == null && direccionId) { // caso para todas los envios a la misma direccion const isPaletsValue = await getTipoEnvio(); if (isPaletsValue !== null) { isPalets = isPaletsValue ? 1 : 0; @@ -285,13 +285,13 @@ $(() => { value="${tirada}" class="form-control text-center">

- - -
+ + + ${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" - } - -}