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