falta actualizar bien el resumen

This commit is contained in:
2025-10-30 19:48:26 +01:00
parent feff9ee94a
commit 167c136dca
28 changed files with 518 additions and 342 deletions

View File

@ -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<CartDireccion> 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<CartDireccion> 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);
}
}

View File

@ -72,8 +72,18 @@ public class CartController {
var items = service.listItems(userId, locale);
model.addAttribute("items", items);
Map<String, Object> 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";
}
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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<Cart, Long> {
Optional<Cart> 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<Cart> findByIdFetchAll(@Param("id") Long id);
}

View File

@ -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<CartItem> 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<String, Object> 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<String, Object> 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<CartItem> items = cart.getItems();
List<CartDireccion> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> getCartDirecciones(Long cartId, Locale locale) {
Cart cart = cartRepo.findByIdFetchAll(cartId)
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
Map<String, Object> result = new HashMap<>();
List<CartDireccion> direcciones = cart.getDirecciones();
if (cart.getOnlyOneShipment() && !direcciones.isEmpty()) {
result.put("mainDir", direcciones.get(0).toDireccionCard(messageSource, locale));
} else {
List<DireccionCardDTO> 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<DireccionShipment> 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) {

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -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";
}
}

View File

@ -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<String, Object> 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<String, Object> decodeMerchantParametersToMap(String dsMerchantParametersB64) throws Exception {
try {
String json = Utils.decodeB64UrlSafeString(
dsMerchantParametersB64.getBytes(StandardCharsets.UTF_8)
);
return MAPPER.readValue(json, new TypeReference<Map<String, Object>>() {});
} catch (Exception ignore) {
byte[] decoded = Base64.getDecoder().decode(dsMerchantParametersB64);
String json = new String(decoded, StandardCharsets.UTF_8);
return MAPPER.readValue(json, new TypeReference<Map<String, Object>>() {});
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<String, Object> mp = decodeMerchantParametersToMap(dsMerchantParametersB64);
RedsysNotification notif = new RedsysNotification(mp);
public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64)
throws Exception {
Map<String, Object> 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;
}
}
}
}