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 0000000..83fbf6c Binary files /dev/null and b/src/main/resources/lib/apiSha256.jar differ diff --git a/src/main/resources/lib/apiSha512V2.jar b/src/main/resources/lib/apiSha512V2.jar deleted file mode 100644 index 5858b1d..0000000 Binary files a/src/main/resources/lib/apiSha512V2.jar and /dev/null differ 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" - } - -}