mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 00:48:49 +00:00
falta actualizar bien el resumen
This commit is contained in:
8
pom.xml
8
pom.xml
@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.5.6</version>
|
<version>3.5.7</version>
|
||||||
<relativePath /> <!-- lookup parent from repository -->
|
<relativePath /> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.imprimelibros</groupId>
|
<groupId>com.imprimelibros</groupId>
|
||||||
@ -154,10 +154,10 @@
|
|||||||
<!-- Redsys -->
|
<!-- Redsys -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>sis.redsys</groupId>
|
<groupId>sis.redsys</groupId>
|
||||||
<artifactId>apiSha512V2</artifactId>
|
<artifactId>apiSha256</artifactId>
|
||||||
<version>2.0</version>
|
<version>1.0</version>
|
||||||
<scope>system</scope>
|
<scope>system</scope>
|
||||||
<systemPath>${project.basedir}/src/main/resources/lib/apiSha512V2.jar</systemPath>
|
<systemPath>${project.basedir}/src/main/resources/lib/apiSha256.jar</systemPath>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Dependencias locales incluidas en el ZIP -->
|
<!-- Dependencias locales incluidas en el ZIP -->
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package com.imprimelibros.erp.cart;
|
package com.imprimelibros.erp.cart;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -43,6 +45,9 @@ public class Cart {
|
|||||||
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
private List<CartDireccion> direcciones = new ArrayList<>();
|
private List<CartDireccion> direcciones = new ArrayList<>();
|
||||||
|
|
||||||
|
@Column(name = "total", nullable = false)
|
||||||
|
private BigDecimal total = BigDecimal.ZERO;
|
||||||
|
|
||||||
@PreUpdate
|
@PreUpdate
|
||||||
public void preUpdate() {
|
public void preUpdate() {
|
||||||
this.updatedAt = LocalDateTime.now();
|
this.updatedAt = LocalDateTime.now();
|
||||||
@ -85,6 +90,14 @@ public class Cart {
|
|||||||
this.onlyOneShipment = onlyOneShipment;
|
this.onlyOneShipment = onlyOneShipment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTotal() {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotal(BigDecimal total) {
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
@ -108,4 +121,14 @@ public class Cart {
|
|||||||
public void setDirecciones(List<CartDireccion> direcciones) {
|
public void setDirecciones(List<CartDireccion> direcciones) {
|
||||||
this.direcciones = 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,8 +72,18 @@ public class CartController {
|
|||||||
var items = service.listItems(userId, locale);
|
var items = service.listItems(userId, locale);
|
||||||
model.addAttribute("items", items);
|
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);
|
var summary = service.getCartSummary(cart, locale);
|
||||||
model.addAttribute("cartSummary", summary);
|
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);
|
model.addAttribute("cart", cart);
|
||||||
return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple)
|
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)
|
@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 {
|
try {
|
||||||
service.updateCart(id, updateRequest);
|
service.updateCart(id, updateRequest);
|
||||||
@ -159,8 +169,10 @@ public class CartController {
|
|||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
||||||
model.addAttribute("errorMessage", messageSource.getMessage("cart.errors.update-cart", new Object[]{e.getMessage()}, locale));
|
// redirect to cart with error message
|
||||||
return "/cart"; // templates/error/500.html
|
String errorMessage = messageSource.getMessage("cart.update.error", null, "Error updating cart", locale);
|
||||||
|
model.addAttribute("errorMessage", errorMessage);
|
||||||
|
return "redirect:/cart";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
package com.imprimelibros.erp.cart;
|
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.direcciones.Direccion;
|
||||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
|
|
||||||
@ -33,28 +35,67 @@ public class CartDireccion {
|
|||||||
@Column(name = "isPalets", nullable = false)
|
@Column(name = "isPalets", nullable = false)
|
||||||
private Boolean isPalets;
|
private Boolean isPalets;
|
||||||
|
|
||||||
@Column(name = "base", precision = 12, scale = 2)
|
|
||||||
private BigDecimal base;
|
|
||||||
|
|
||||||
// --- Getters & Setters ---
|
// --- Getters & Setters ---
|
||||||
public Long getId() { return id; }
|
public Long getId() {
|
||||||
public void setId(Long id) { this.id = id; }
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
public Cart getCart() { return cart; }
|
public void setId(Long id) {
|
||||||
public void setCart(Cart cart) { this.cart = cart; }
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
public Direccion getDireccion() { return direccion; }
|
public Cart getCart() {
|
||||||
public void setDireccion(Direccion direccion) { this.direccion = direccion; }
|
return cart;
|
||||||
|
}
|
||||||
|
|
||||||
public Presupuesto getPresupuesto() { return presupuesto; }
|
public void setCart(Cart cart) {
|
||||||
public void setPresupuesto(Presupuesto presupuesto) { this.presupuesto = presupuesto; }
|
this.cart = cart;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getUnidades() { return unidades; }
|
public Direccion getDireccion() {
|
||||||
public void setUnidades(Integer unidades) { this.unidades = unidades; }
|
return direccion;
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getBase() { return base; }
|
public void setDireccion(Direccion direccion) {
|
||||||
public void setBase(BigDecimal base) { this.base = base; }
|
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; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package com.imprimelibros.erp.cart;
|
|||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = "cart_items",
|
name = "cart_items",
|
||||||
@ -17,8 +19,9 @@ public class CartItem {
|
|||||||
@JoinColumn(name = "cart_id", nullable = false)
|
@JoinColumn(name = "cart_id", nullable = false)
|
||||||
private Cart cart;
|
private Cart cart;
|
||||||
|
|
||||||
@Column(name = "presupuesto_id", nullable = false)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
private Long presupuestoId;
|
@JoinColumn(name = "presupuesto_id", nullable = false)
|
||||||
|
private Presupuesto presupuesto;
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private LocalDateTime createdAt = LocalDateTime.now();
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
@ -29,8 +32,8 @@ public class CartItem {
|
|||||||
public Cart getCart() { return cart; }
|
public Cart getCart() { return cart; }
|
||||||
public void setCart(Cart cart) { this.cart = cart; }
|
public void setCart(Cart cart) { this.cart = cart; }
|
||||||
|
|
||||||
public Long getPresupuestoId() { return presupuestoId; }
|
public Presupuesto getPresupuesto() { return presupuesto; }
|
||||||
public void setPresupuestoId(Long presupuestoId) { this.presupuestoId = presupuestoId; }
|
public void setPresupuesto(Presupuesto presupuesto) { this.presupuesto = presupuesto; }
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,22 @@
|
|||||||
package com.imprimelibros.erp.cart;
|
package com.imprimelibros.erp.cart;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface CartRepository extends JpaRepository<Cart, Long> {
|
public interface CartRepository extends JpaRepository<Cart, Long> {
|
||||||
Optional<Cart> findByUserIdAndStatus(Long userId, Cart.Status status);
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.imprimelibros.erp.cart;
|
package com.imprimelibros.erp.cart;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -10,9 +10,12 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
||||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
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.cart.dto.UpdateCartRequest;
|
||||||
import com.imprimelibros.erp.common.Utils;
|
import com.imprimelibros.erp.common.Utils;
|
||||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||||
@ -68,9 +71,7 @@ public class CartService {
|
|||||||
List<CartItem> items = itemRepo.findByCartId(cart.getId());
|
List<CartItem> items = itemRepo.findByCartId(cart.getId());
|
||||||
for (CartItem item : items) {
|
for (CartItem item : items) {
|
||||||
|
|
||||||
Presupuesto p = presupuestoRepo.findById(item.getPresupuestoId())
|
Presupuesto p = item.getPresupuesto();
|
||||||
.orElseThrow(
|
|
||||||
() -> new IllegalStateException("Presupuesto no encontrado: " + item.getPresupuestoId()));
|
|
||||||
|
|
||||||
Map<String, Object> elemento = getElementoCart(p, locale);
|
Map<String, Object> elemento = getElementoCart(p, locale);
|
||||||
elemento.put("cartItemId", item.getId());
|
elemento.put("cartItemId", item.getId());
|
||||||
@ -88,7 +89,8 @@ public class CartService {
|
|||||||
if (!exists) {
|
if (!exists) {
|
||||||
CartItem ci = new CartItem();
|
CartItem ci = new CartItem();
|
||||||
ci.setCart(cart);
|
ci.setCart(cart);
|
||||||
ci.setPresupuestoId(presupuestoId);
|
ci.setPresupuesto(presupuestoRepo.findById(presupuestoId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Presupuesto no encontrado")));
|
||||||
itemRepo.save(ci);
|
itemRepo.save(ci);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,58 +167,140 @@ public class CartService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> getCartSummary(Cart cart, Locale locale) {
|
public Map<String, Object> getCartSummary(Cart cart, Locale locale) {
|
||||||
|
|
||||||
double base = 0.0;
|
double base = 0.0;
|
||||||
double iva4 = 0.0;
|
double iva4 = 0.0;
|
||||||
double iva21 = 0.0;
|
double iva21 = 0.0;
|
||||||
|
double shipment = 0.0;
|
||||||
|
|
||||||
|
Boolean errorShipementCost = false;
|
||||||
|
|
||||||
List<CartItem> items = cart.getItems();
|
List<CartItem> items = cart.getItems();
|
||||||
List<CartDireccion> direcciones = cart.getDirecciones();
|
List<CartDireccion> direcciones = cart.getDirecciones();
|
||||||
|
|
||||||
for (CartItem item : items) {
|
for (CartItem item : items) {
|
||||||
Presupuesto p = presupuestoRepo.findById(item.getPresupuestoId())
|
Presupuesto p = item.getPresupuesto();
|
||||||
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + item.getPresupuestoId()));
|
|
||||||
base += p.getBaseImponible().doubleValue();
|
base += p.getBaseImponible().doubleValue();
|
||||||
iva4 += p.getIvaImporte4().doubleValue();
|
iva4 += p.getIvaImporte4().doubleValue();
|
||||||
iva21 += p.getIvaImporte21().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
|
// 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);
|
CartDireccion cd = direcciones.get(0);
|
||||||
Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(), cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
|
Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(),
|
||||||
if(!freeShipment) {
|
cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
|
||||||
Map<String, Object> data =
|
if (!freeShipment) {
|
||||||
Map.of(
|
try {
|
||||||
"cp", cd.getDireccion().getCp(),
|
Map<String, Object> data = Map.of(
|
||||||
"pais_code3", cd.getDireccion().getPaisCode3(),
|
"cp", cd.getDireccion().getCp(),
|
||||||
"peso", p.getPeso() != null ? p.getPeso() : 0,
|
"pais_code3", cd.getDireccion().getPaisCode3(),
|
||||||
"unidades", cd.getUnidades(),
|
"peso", p.getPeso() != null ? p.getPeso() : 0,
|
||||||
"palets", cd.getIsPalets() ? 1 : 0
|
"unidades", p.getSelectedTirada(),
|
||||||
);
|
"palets", cd.getIsPalets() ? 1 : 0);
|
||||||
var shipmentCost = skApiClient.getCosteEnvio(data, locale);
|
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<>();
|
Map<String, Object> summary = new HashMap<>();
|
||||||
summary.put("base", Utils.formatCurrency(base, locale));
|
summary.put("base", Utils.formatCurrency(base, locale));
|
||||||
summary.put("iva4", Utils.formatCurrency(iva4, locale));
|
summary.put("iva4", Utils.formatCurrency(iva4, locale));
|
||||||
summary.put("iva21", Utils.formatCurrency(iva21, locale));
|
summary.put("iva21", Utils.formatCurrency(iva21, locale));
|
||||||
|
summary.put("shipment", Utils.formatCurrency(shipment, locale));
|
||||||
summary.put("total", Utils.formatCurrency(total, locale));
|
summary.put("total", Utils.formatCurrency(total, locale));
|
||||||
|
summary.put("errorShipmentCost", errorShipementCost);
|
||||||
|
|
||||||
return summary;
|
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
|
@Transactional
|
||||||
public Boolean updateCart(Long cartId, UpdateCartRequest request) {
|
public Boolean updateCart(Long cartId, UpdateCartRequest request) {
|
||||||
|
|
||||||
try{
|
try {
|
||||||
Cart cart = cartRepo.findById(cartId).orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
Cart cart = cartRepo.findById(cartId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||||
cart.setOnlyOneShipment(request.isOnlyOneShipment());
|
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);
|
cartRepo.save(cart);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,24 +6,15 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.validation.BindingResult;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
|
|
||||||
import com.imprimelibros.erp.common.Utils;
|
import com.imprimelibros.erp.common.Utils;
|
||||||
import com.imprimelibros.erp.direcciones.Direccion;
|
|
||||||
import com.imprimelibros.erp.i18n.TranslationService;
|
import com.imprimelibros.erp.i18n.TranslationService;
|
||||||
import com.imprimelibros.erp.paises.PaisesService;
|
import com.imprimelibros.erp.paises.PaisesService;
|
||||||
|
|
||||||
import jakarta.mail.Message;
|
|
||||||
|
|
||||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||||
|
|
||||||
import com.imprimelibros.erp.cart.CartService;
|
import com.imprimelibros.erp.cart.CartService;
|
||||||
|
|||||||
@ -4,14 +4,11 @@ import java.text.Collator;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.imprimelibros.erp.direcciones.DireccionRepository;
|
|
||||||
import com.imprimelibros.erp.paises.Paises;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class DireccionService {
|
public class DireccionService {
|
||||||
@ -84,13 +81,15 @@ public class DireccionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Boolean checkFreeShipment(Integer cp, String paisCode3) {
|
public Boolean checkFreeShipment(Integer cp, String paisCode3) {
|
||||||
if(paisCode3.equals("ESP")) {
|
if (paisCode3 != null && paisCode3.equals("ESP") && cp != null) {
|
||||||
// España peninsular y baleares
|
// Excluir Canarias (35xxx y 38xxx), Baleares (07xxx), Ceuta (51xxx), Melilla (52xxx)
|
||||||
if(cp != null && cp < 35000 && cp >= 35999) {
|
int provincia = cp / 1000;
|
||||||
return true;
|
|
||||||
}
|
if (provincia != 7 && provincia != 35 && provincia != 38 && provincia != 51 && provincia != 52) {
|
||||||
|
return true; // España peninsular
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
|||||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@ -253,7 +254,12 @@ public class skApiClient {
|
|||||||
if (error != null && error) {
|
if (error != null && error) {
|
||||||
return Map.of("error", messageSource.getMessage("direcciones.error.noShippingCost", null, locale));
|
return Map.of("error", messageSource.getMessage("direcciones.error.noShippingCost", null, locale));
|
||||||
} else {
|
} 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);
|
return Map.of("data", total);
|
||||||
}
|
}
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package com.imprimelibros.erp.pdf;
|
package com.imprimelibros.erp.pdf;
|
||||||
|
|
||||||
import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder;
|
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.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package com.imprimelibros.erp.redsys;
|
package com.imprimelibros.erp.redsys;
|
||||||
|
|
||||||
import com.imprimelibros.erp.redsys.RedsysService;
|
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
@ -28,7 +27,7 @@ public class RedsysController {
|
|||||||
model.addAttribute("signatureVersion", form.signatureVersion());
|
model.addAttribute("signatureVersion", form.signatureVersion());
|
||||||
model.addAttribute("merchantParameters", form.merchantParameters());
|
model.addAttribute("merchantParameters", form.merchantParameters());
|
||||||
model.addAttribute("signature", form.signature());
|
model.addAttribute("signature", form.signature());
|
||||||
return "payments/redsys-redirect";
|
return "imprimelibros/payments/redsys-redirect";
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/notify")
|
@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";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,7 @@ package com.imprimelibros.erp.redsys;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import sis.redsys.api.Signature;
|
import sis.redsys.api.ApiMacSha256;
|
||||||
import sis.redsys.api.Utils;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@ -12,7 +11,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -20,48 +18,54 @@ import java.util.Objects;
|
|||||||
public class RedsysService {
|
public class RedsysService {
|
||||||
|
|
||||||
// ---------- CONFIG ----------
|
// ---------- CONFIG ----------
|
||||||
@Value("${redsys.merchant-code}") private String merchantCode;
|
@Value("${redsys.merchant-code}")
|
||||||
@Value("${redsys.terminal}") private String terminal;
|
private String merchantCode;
|
||||||
@Value("${redsys.currency}") private String currency;
|
@Value("${redsys.terminal}")
|
||||||
@Value("${redsys.transaction-type}") private String txType;
|
private String terminal;
|
||||||
@Value("${redsys.secret-key}") private String secretKeyBase64;
|
@Value("${redsys.currency}")
|
||||||
@Value("${redsys.urls.ok}") private String urlOk;
|
private String currency;
|
||||||
@Value("${redsys.urls.ko}") private String urlKo;
|
@Value("${redsys.transaction-type}")
|
||||||
@Value("${redsys.urls.notify}") private String urlNotify;
|
private String txType;
|
||||||
@Value("${redsys.environment}") private String env;
|
@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 ----------
|
// ---------- RECORDS ----------
|
||||||
public record PaymentRequest(String order, long amountCents, String description) {}
|
public record PaymentRequest(String order, long amountCents, String description) {
|
||||||
public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) {}
|
}
|
||||||
|
|
||||||
|
public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) {
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- MÉTODO PRINCIPAL ----------
|
// ---------- MÉTODO PRINCIPAL ----------
|
||||||
public FormPayload buildRedirectForm(PaymentRequest req) throws Exception {
|
public FormPayload buildRedirectForm(PaymentRequest req) throws Exception {
|
||||||
Map<String, Object> params = new HashMap<>();
|
ApiMacSha256 api = new ApiMacSha256();
|
||||||
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);
|
|
||||||
|
|
||||||
// JSON -> Base64
|
api.setParameter("DS_MERCHANT_AMOUNT", String.valueOf(req.amountCents()));
|
||||||
String json = new ObjectMapper().writeValueAsString(params);
|
api.setParameter("DS_MERCHANT_ORDER", req.order()); // Usa 12 dígitos con ceros si puedes
|
||||||
String merchantParametersB64 = Base64.getEncoder()
|
api.setParameter("DS_MERCHANT_MERCHANTCODE", merchantCode);
|
||||||
.encodeToString(json.getBytes(StandardCharsets.UTF_8));
|
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 merchantParameters = api.createMerchantParameters();
|
||||||
String signature = Signature.createMerchantSignature(secretKeyBase64, req.order(), merchantParametersB64);
|
String signature = api.createMerchantSignature(secretKeyBase64);
|
||||||
|
|
||||||
String action = "test".equalsIgnoreCase(env)
|
String action = "test".equalsIgnoreCase(env)
|
||||||
? "https://sis-t.redsys.es:25443/sis/realizarPago"
|
? "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||||
: "https://sis.redsys.es/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 ----------
|
// ---------- STEP 3: Decodificar Ds_MerchantParameters ----------
|
||||||
@ -69,40 +73,42 @@ public class RedsysService {
|
|||||||
|
|
||||||
public Map<String, Object> decodeMerchantParametersToMap(String dsMerchantParametersB64) throws Exception {
|
public Map<String, Object> decodeMerchantParametersToMap(String dsMerchantParametersB64) throws Exception {
|
||||||
try {
|
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);
|
byte[] decoded = Base64.getDecoder().decode(dsMerchantParametersB64);
|
||||||
String json = new String(decoded, StandardCharsets.UTF_8);
|
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 ----------
|
// ---------- STEP 4: Validar notificación ----------
|
||||||
public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64) throws Exception {
|
public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64)
|
||||||
Map<String, Object> mp = decodeMerchantParametersToMap(dsMerchantParametersB64);
|
throws Exception {
|
||||||
RedsysNotification notif = new RedsysNotification(mp);
|
Map<String, Object> mp = decodeMerchantParametersToMap(dsMerchantParametersB64);
|
||||||
|
RedsysNotification notif = new RedsysNotification(mp);
|
||||||
|
|
||||||
if (notif.order == null || notif.order.isBlank()) {
|
if (notif.order == null || notif.order.isBlank()) {
|
||||||
throw new IllegalArgumentException("Falta Ds_Order en Ds_MerchantParameters");
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ----------
|
// ---------- HELPERS ----------
|
||||||
private static boolean safeEqualsB64(String a, String b) {
|
private static boolean safeEqualsB64(String a, String b) {
|
||||||
if (Objects.equals(a, b)) return true;
|
if (Objects.equals(a, b))
|
||||||
|
return true;
|
||||||
try {
|
try {
|
||||||
String na = normalizeB64(a);
|
String na = normalizeB64(a);
|
||||||
String nb = normalizeB64(b);
|
String nb = normalizeB64(b);
|
||||||
@ -115,12 +121,16 @@ public class RedsysService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String normalizeB64(String s) {
|
private static String normalizeB64(String s) {
|
||||||
if (s == null) return "";
|
if (s == null)
|
||||||
|
return "";
|
||||||
String n = s.replace('-', '+').replace('_', '/');
|
String n = s.replace('-', '+').replace('_', '/');
|
||||||
int mod = n.length() % 4;
|
int mod = n.length() % 4;
|
||||||
if (mod == 2) n += "==";
|
if (mod == 2)
|
||||||
else if (mod == 3) n += "=";
|
n += "==";
|
||||||
else if (mod == 1) n += "===";
|
else if (mod == 3)
|
||||||
|
n += "=";
|
||||||
|
else if (mod == 1)
|
||||||
|
n += "===";
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,12 +154,21 @@ public class RedsysService {
|
|||||||
try {
|
try {
|
||||||
int r = Integer.parseInt(response);
|
int r = Integer.parseInt(response);
|
||||||
return r >= 0 && r <= 99;
|
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) {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,8 +28,8 @@ spring.jpa.show-sql=false
|
|||||||
#
|
#
|
||||||
# Safekat API Configuration
|
# Safekat API Configuration
|
||||||
#
|
#
|
||||||
#safekat.api.url=http://localhost:8000/
|
safekat.api.url=http://localhost:8000/
|
||||||
safekat.api.url=https://erp-dev.safekat.es/
|
#safekat.api.url=https://erp-dev.safekat.es/
|
||||||
safekat.api.email=imnavajas@coit.es
|
safekat.api.email=imnavajas@coit.es
|
||||||
safekat.api.password=Safekat2024
|
safekat.api.password=Safekat2024
|
||||||
|
|
||||||
@ -114,7 +114,13 @@ redsys.terminal=1
|
|||||||
redsys.currency=978
|
redsys.currency=978
|
||||||
redsys.transaction-type=0
|
redsys.transaction-type=0
|
||||||
redsys.secret-key=sq7HjrUOBfKmC576ILgskD5srU870gJ7
|
redsys.secret-key=sq7HjrUOBfKmC576ILgskD5srU870gJ7
|
||||||
redsys.urls.ok=https://localhost:8080/pagos/redsys/ok
|
redsys.urls.ok=http://localhost:8080/pagos/redsys/ok
|
||||||
redsys.urls.ko=https://localhost:8080/pagos/redsys/ko
|
redsys.urls.ko=http://localhost:8080/pagos/redsys/ko
|
||||||
redsys.urls.notify=https://localhost:8080/pagos/redsys/notify
|
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
|
||||||
@ -22,6 +22,15 @@ databaseChangeLog:
|
|||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: total
|
||||||
|
type: DECIMAL(19,6)
|
||||||
|
defaultValueNumeric: 0
|
||||||
|
remarks: "Total del carrito"
|
||||||
|
afterColumn: only_one_shipment
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
rollback:
|
rollback:
|
||||||
- dropColumn:
|
- dropColumn:
|
||||||
tableName: carts
|
tableName: carts
|
||||||
|
|||||||
@ -50,12 +50,7 @@ databaseChangeLog:
|
|||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
|
|
||||||
- column:
|
|
||||||
name: base
|
|
||||||
type: DECIMAL(12, 2)
|
|
||||||
|
|
||||||
|
|
||||||
- createIndex:
|
- createIndex:
|
||||||
indexName: idx_cart_dir_direccion_id
|
indexName: idx_cart_dir_direccion_id
|
||||||
tableName: cart_direcciones
|
tableName: cart_direcciones
|
||||||
|
|||||||
@ -31,6 +31,7 @@ cart.shipping.errors.fillAddressesItems=Debe seleccionar una dirección de enví
|
|||||||
|
|
||||||
cart.resumen.title=Resumen de la cesta
|
cart.resumen.title=Resumen de la cesta
|
||||||
cart.resumen.base=Base imponible:
|
cart.resumen.base=Base imponible:
|
||||||
|
cart.resumen.envio=Coste de envío:
|
||||||
cart.resumen.iva-4=IVA 4%:
|
cart.resumen.iva-4=IVA 4%:
|
||||||
cart.resumen.iva-21=IVA 21%:
|
cart.resumen.iva-21=IVA 21%:
|
||||||
cart.resumen.total=Total cesta:
|
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.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}
|
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.
|
||||||
@ -11,4 +11,7 @@ checkout.shipping.onlyOneShipment=Todo el pedido se envía a una única direcci
|
|||||||
|
|
||||||
checkout.summary.presupuesto=#Presupuesto
|
checkout.summary.presupuesto=#Presupuesto
|
||||||
checkout.summary.titulo=Título
|
checkout.summary.titulo=Título
|
||||||
checkout.summary.base=Base
|
checkout.summary.base=Base
|
||||||
|
checkout.summary.iva-4=IVA 4%
|
||||||
|
checkout.summary.iva-21=IVA 21%
|
||||||
|
checkout.summary.envio=Envío
|
||||||
BIN
src/main/resources/lib/apiSha256.jar
Normal file
BIN
src/main/resources/lib/apiSha256.jar
Normal file
Binary file not shown.
Binary file not shown.
@ -24,7 +24,10 @@ $(() => {
|
|||||||
$(this).find('.item-tirada').attr('name', 'direcciones[' + i + '].unidades');
|
$(this).find('.item-tirada').attr('name', 'direcciones[' + i + '].unidades');
|
||||||
});
|
});
|
||||||
$.post(form.attr('action'), form.serialize(), (response) => {
|
$.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(() => {
|
}).always(() => {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,7 +10,7 @@ $(() => {
|
|||||||
$('.nav-product').addClass('d-none');
|
$('.nav-product').addClass('d-none');
|
||||||
document.querySelectorAll('.card.product').forEach(card => {
|
document.querySelectorAll('.card.product').forEach(card => {
|
||||||
const detailsBtn = card.querySelector('.nav-link[id^="pills-details-"][id$="-tab"]');
|
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');
|
$('#shippingAddressesContainer').empty().removeClass('d-none');
|
||||||
$('.shipping-addresses-item').toArray().forEach(element => {
|
$('.shipping-addresses-item').toArray().forEach(element => {
|
||||||
@ -235,7 +235,7 @@ $(() => {
|
|||||||
return false;
|
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();
|
const isPaletsValue = await getTipoEnvio();
|
||||||
if (isPaletsValue !== null) {
|
if (isPaletsValue !== null) {
|
||||||
isPalets = isPaletsValue ? 1 : 0;
|
isPalets = isPaletsValue ? 1 : 0;
|
||||||
@ -285,13 +285,13 @@ $(() => {
|
|||||||
value="${tirada}" class="form-control text-center">
|
value="${tirada}" class="form-control text-center">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-switch form-switch-custom mb-3 d-flex align-items-center justify-content-center ps-0">
|
<div class="form-check form-switch form-switch-custom mb-3 d-flex align-items-center justify-content-center ps-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
id="swal-input-palets"
|
id="swal-input-palets"
|
||||||
class="form-check-input ms-0 me-2 float-none">
|
class="form-check-input ms-0 me-2 float-none">
|
||||||
<label for="swal-input-palets" class="form-check-label mb-0">
|
<label for="swal-input-palets" class="form-check-label mb-0">
|
||||||
${window.languageBundle['cart.shipping.send-in-palets'] || 'Enviar en palets'}
|
${window.languageBundle['cart.shipping.send-in-palets'] || 'Enviar en palets'}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<span class="form-text text-muted">
|
<span class="form-text text-muted">
|
||||||
${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'}
|
${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'}
|
||||||
</span>
|
</span>
|
||||||
@ -327,19 +327,19 @@ $(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getTipoEnvio() {
|
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',
|
title: window.languageBundle['cart.shipping.tipo-envio'] || 'Tipo de envío',
|
||||||
html: `
|
html: `
|
||||||
<div class="form-check form-switch form-switch-custom my-3 d-flex align-items-center justify-content-center gap-2">
|
<div class="form-check form-switch form-switch-custom my-3 d-flex align-items-center justify-content-center gap-2">
|
||||||
<input type="checkbox" class="form-check-input" id="swal-input-palets">
|
<input type="checkbox" class="form-check-input" id="swal-input-palets">
|
||||||
<label for="swal-input-palets" class="form-label mb-0">
|
<label for="swal-input-palets" class="form-label mb-0">
|
||||||
${window.languageBundle['cart.shipping.send-in-palets'] || 'Enviar en palets'}
|
${window.languageBundle['cart.shipping.send-in-palets'] || 'Enviar en palets'}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<span class="form-text text-muted">
|
<span class="form-text text-muted">
|
||||||
${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'}
|
${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'}
|
||||||
</span>
|
</span>
|
||||||
`,
|
`,
|
||||||
focusConfirm: false,
|
focusConfirm: false,
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
buttonsStyling: false,
|
buttonsStyling: false,
|
||||||
@ -350,17 +350,18 @@ $(() => {
|
|||||||
confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar',
|
confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar',
|
||||||
cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar',
|
cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar',
|
||||||
preConfirm: () => {
|
preConfirm: () => {
|
||||||
const isPalets = document.getElementById('swal-input-palets').checked;
|
const popup = Swal.getPopup();
|
||||||
return isPalets;
|
const chk = popup.querySelector('#swal-input-palets');
|
||||||
|
// Devuelve un OBJETO (siempre truthy) con el booleano dentro
|
||||||
|
return { isPalets: !!chk?.checked };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (checkValue !== undefined) {
|
if (!isConfirmed) return null; // cancelado
|
||||||
return checkValue; // boolean
|
return value.isPalets; // true / false
|
||||||
}
|
|
||||||
return null; // Si se cancela el Swal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function checkTotalUnits(container, tirada) {
|
function checkTotalUnits(container, tirada) {
|
||||||
|
|
||||||
const totalUnits = container.find('.direccion-card').toArray().reduce((acc, el) => {
|
const totalUnits = container.find('.direccion-card').toArray().reduce((acc, el) => {
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
<div th:if="${items.isEmpty()}">
|
<div th:if="${items.isEmpty()}">
|
||||||
<div class="alert alert-info" role="alert" th:text="#{cart.empty}"></div>
|
<div class="alert alert-info" role="alert" th:text="#{cart.empty}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="errorEnvio" th:class="${'alert alert-danger' + (errorEnvio ? '' : ' d-none')}" role="alert" th:text="#{cart.errors.shipping}"></div>
|
||||||
<div th:if="${errorMessage}" class="alert alert-danger " role="alert" th:text="${errorMessage}"></div>
|
<div th:if="${errorMessage}" class="alert alert-danger " role="alert" th:text="${errorMessage}"></div>
|
||||||
|
|
||||||
<div class="alert alert-danger alert-shipment d-none" role="alert"
|
<div class="alert alert-danger alert-shipment d-none" role="alert"
|
||||||
@ -24,17 +25,30 @@
|
|||||||
<p th:text="#{cart.shipping.info}"></p>
|
<p th:text="#{cart.shipping.info}"></p>
|
||||||
<div
|
<div
|
||||||
class="form-check form-switch form-switch-custom form-switch-presupuesto mb-3 d-flex align-items-center">
|
class="form-check form-switch form-switch-custom form-switch-presupuesto mb-3 d-flex align-items-center">
|
||||||
|
|
||||||
<input type="checkbox" class="form-check-input datos-generales-data me-2" id="onlyOneShipment"
|
<input type="checkbox" class="form-check-input datos-generales-data me-2" id="onlyOneShipment"
|
||||||
th:field="${cart.onlyOneShipment}"/>
|
th:field="${cart.onlyOneShipment}" />
|
||||||
<label for="onlyOneShipment" class="form-label d-flex align-items-center mb-0">
|
<label for="onlyOneShipment" class="form-label d-flex align-items-center mb-0">
|
||||||
<span th:text="#{cart.shipping.onlyOneShipment}" class="me-2"></span>
|
<span th:text="#{cart.shipping.onlyOneShipment}" class="me-2"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" th:class="${'btn btn-secondary' + (!cart.onlyOneShipment or mainDir?.size > 0 ? ' d-none' : '')}" id="addOrderAddress"
|
<button type="button"
|
||||||
th:text="#{cart.shipping.add}">Añadir dirección</button>
|
th:class="${'btn btn-secondary' + (!cart.onlyOneShipment or #lists.size(mainDir ?: {}) > 0 ? ' d-none' : '')}"
|
||||||
|
id="addOrderAddress" th:text="#{cart.shipping.add}">Añadir dirección</button>
|
||||||
|
|
||||||
<div id="shippingAddressesContainer" class="shipping-order-address d-flex flex-wrap gap-3 mt-4"></div>
|
<div id="shippingAddressesContainer" class="shipping-order-address d-flex flex-wrap gap-3 mt-4">
|
||||||
|
<div th:replace="${cart.onlyOneShipment and mainDir != null}
|
||||||
|
? ~{imprimelibros/direcciones/direccionCard :: direccionCard(
|
||||||
|
${mainDir.direccion},
|
||||||
|
${mainDir.pais},
|
||||||
|
${mainDir.presupuestoId},
|
||||||
|
${mainDir.unidades},
|
||||||
|
${mainDir.isPalets}
|
||||||
|
)}
|
||||||
|
: ~{}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,11 +4,12 @@
|
|||||||
data-base=${item.base}">
|
data-base=${item.base}">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<input type="hidden" class="item-presupuesto-id" th:value="${item.cartItemId}" />
|
<input type="hidden" class="item-presupuesto-id" th:value="${item.presupuestoId}" />
|
||||||
<input type="hidden" class="item-tirada" th:value="${item.tirada}" />
|
<input type="hidden" class="item-tirada" th:value="${item.tirada}" />
|
||||||
|
|
||||||
<div class="step-arrow-nav mt-n3 mx-n3 mb-3">
|
<div class="step-arrow-nav mt-n3 mx-n3 mb-3">
|
||||||
<ul th:class="${'nav nav-pills nav-justified custom-nav nav-product' + (cart.onlyOneShipment ? ' d-none' : '')}" role="tablist">
|
<ul th:class="${'nav nav-pills nav-justified custom-nav nav-product' + (cart.onlyOneShipment ? ' d-none' : '')}"
|
||||||
|
role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link fs-15 active" th:id="${'pills-details-' + item.cartItemId + '-tab'}"
|
<button class="nav-link fs-15 active" th:id="${'pills-details-' + item.cartItemId + '-tab'}"
|
||||||
th:data-bs-target="${'#pills-details-' + item.cartItemId}" type="button" role="tab"
|
th:data-bs-target="${'#pills-details-' + item.cartItemId}" type="button" role="tab"
|
||||||
@ -131,7 +132,22 @@
|
|||||||
<button type="button" class="btn btn-secondary btn-add-shipping"
|
<button type="button" class="btn btn-secondary btn-add-shipping"
|
||||||
th:text="#{cart.shipping.add}">Añadir dirección</button>
|
th:text="#{cart.shipping.add}">Añadir dirección</button>
|
||||||
|
|
||||||
<div class="shipping-addresses-item d-flex flex-wrap gap-3 mt-4"></div>
|
<div class="shipping-addresses-item d-flex flex-wrap gap-3 mt-4">
|
||||||
|
<th:block th:each="dir : ${direcciones}">
|
||||||
|
<th:block
|
||||||
|
th:if="${dir != null and dir.unidades != null and dir.unidades > 0 and dir.presupuestoId == item.presupuestoId}">
|
||||||
|
<div th:replace="~{imprimelibros/direcciones/direccionCard ::
|
||||||
|
direccionCard(
|
||||||
|
direccion=${dir.direccion},
|
||||||
|
pais=${dir.pais},
|
||||||
|
presupuestoId=${dir.presupuestoId},
|
||||||
|
unidades=${dir.unidades},
|
||||||
|
isPalets=${dir.isPalets} )}">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -149,7 +165,22 @@
|
|||||||
<button type="button" class="btn btn-secondary btn-add-shipping-sample"
|
<button type="button" class="btn btn-secondary btn-add-shipping-sample"
|
||||||
th:text="#{cart.shipping.add}">Añadir dirección</button>
|
th:text="#{cart.shipping.add}">Añadir dirección</button>
|
||||||
|
|
||||||
<div class="shipping-addresses-sample d-flex flex-wrap gap-3 mt-4"></div>
|
<div class="shipping-addresses-sample d-flex flex-wrap gap-3 mt-4">
|
||||||
|
<th:block th:each="dir : ${direcciones}">
|
||||||
|
<th:block
|
||||||
|
th:if="${dir != null and dir.unidades == null and dir.presupuestoId == item.presupuestoId}">
|
||||||
|
<div th:replace="~{imprimelibros/direcciones/direccionCard ::
|
||||||
|
direccionCard(
|
||||||
|
direccion=${dir.direccion},
|
||||||
|
pais=${dir.pais},
|
||||||
|
presupuestoId=${dir.presupuestoId},
|
||||||
|
unidades=${dir.unidades},
|
||||||
|
isPalets=${dir.isPalets} )}">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<div th:fragment="cartSummary(summary)" class="col-xl-4">
|
<div th:fragment="cartSummary(summary)" class="col-xl-4 cart-summary-container">
|
||||||
<div class="sticky-side-div">
|
<div class="sticky-side-div">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header border-bottom-dashed">
|
<div class="card-header border-bottom-dashed">
|
||||||
@ -12,6 +12,10 @@
|
|||||||
<td><span th:text="#{cart.resumen.base}"></span></td>
|
<td><span th:text="#{cart.resumen.base}"></span></td>
|
||||||
<td class="text-end" id="base-cesta" th:text="${summary.base}"></td>
|
<td class="text-end" id="base-cesta" th:text="${summary.base}"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span th:text="#{cart.resumen.envio}"></span></td>
|
||||||
|
<td class="text-end" id="envio-cesta" th:text="${summary.shipment}"></td>
|
||||||
|
</tr>
|
||||||
<tr id="tr-iva-4">
|
<tr id="tr-iva-4">
|
||||||
<td><span th:text="#{cart.resumen.iva-4}"></span> : </td>
|
<td><span th:text="#{cart.resumen.iva-4}"></span> : </td>
|
||||||
<td class="text-end" id="iva-4-cesta" th:text="${summary.iva4}"></td>
|
<td class="text-end" id="iva-4-cesta" th:text="${summary.iva4}"></td>
|
||||||
@ -28,8 +32,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button type="button" class="btn btn-secondary w-100 mt-2" id="btn-checkout"
|
<form th:action="@{/pagos/redsys/crear}" method="post">
|
||||||
th:onclick="location.href='/checkout'" th:text="#{cart.resumen.tramitar}">Checkout</button>
|
<input type="hidden" name="order" value="123456789012"/>
|
||||||
|
<input type="hidden" name="amountCents" value="12525"/>
|
||||||
|
<button type="submit" class="btn btn-secondary w-100 mt-2"
|
||||||
|
th:text="#{cart.resumen.tramitar}">Checkout</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<!-- end table-responsive -->
|
<!-- end table-responsive -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<div th:fragment="direccionCard(direccion)" name="direccion"
|
<div th:fragment="direccionCard(direccion, pais, presupuestoId, unidades, isPalets)" name="direccion"
|
||||||
class="card card border mb-3 direccion-card bg-light w-100 mx-2">
|
class="card card border mb-3 direccion-card bg-light w-100 mx-2">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
|||||||
@ -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<String, Object> 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"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user