diff --git a/pom.xml b/pom.xml
index 2e5351a..404dffd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.5.6
+ 3.5.7
com.imprimelibros
@@ -151,6 +151,41 @@
${liquibase.version}
+
+
+ sis.redsys
+ apiSha256
+ 1.0
+ system
+ ${project.basedir}/src/main/resources/lib/apiSha256.jar
+
+
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+ 1.47
+ system
+ ${project.basedir}/src/main/resources/lib/bcprov-jdk15on-1.4.7.jar
+
+
+
+ commons-codec
+ commons-codec
+ 1.3
+ system
+ ${project.basedir}/src/main/resources/lib/commons-codec-1.3.jar
+
+
+
+ org.json
+ json
+ 1.0
+ system
+ ${project.basedir}/src/main/resources/lib/org.json.jar
+
+
+
diff --git a/src/main/java/com/imprimelibros/erp/cart/Cart.java b/src/main/java/com/imprimelibros/erp/cart/Cart.java
index ff3b6ec..557636f 100644
--- a/src/main/java/com/imprimelibros/erp/cart/Cart.java
+++ b/src/main/java/com/imprimelibros/erp/cart/Cart.java
@@ -1,18 +1,23 @@
package com.imprimelibros.erp.cart;
import jakarta.persistence.*;
+
+import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
-@Table(name = "carts",
- uniqueConstraints = @UniqueConstraint(name="uq_carts_user_active", columnNames={"user_id","status"}))
+@Table(name = "carts", uniqueConstraints = @UniqueConstraint(name = "uq_carts_user_active", columnNames = { "user_id",
+ "status" }))
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;
@Column(name = "user_id", nullable = false)
@@ -25,6 +30,9 @@ public class Cart {
@Column(nullable = false, length = 3)
private String currency = "EUR";
+ @Column(name = "only_one_shipment", nullable = false)
+ private Boolean onlyOneShipment = true;
+
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@@ -34,23 +42,93 @@ public class Cart {
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List items = new ArrayList<>();
+ @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
+ private List direcciones = new ArrayList<>();
+
+ @Column(name = "total", nullable = false)
+ private BigDecimal total = BigDecimal.ZERO;
+
@PreUpdate
- public void preUpdate() { this.updatedAt = LocalDateTime.now(); }
+ public void preUpdate() {
+ this.updatedAt = LocalDateTime.now();
+ }
// Getters & Setters
- public Long getId() { return id; }
- public Long getUserId() { return userId; }
- public void setUserId(Long userId) { this.userId = userId; }
+ public Long getId() {
+ return id;
+ }
- public Status getStatus() { return status; }
- public void setStatus(Status status) { this.status = status; }
+ public Long getUserId() {
+ return userId;
+ }
- public String getCurrency() { return currency; }
- public void setCurrency(String currency) { this.currency = currency; }
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
- public LocalDateTime getCreatedAt() { return createdAt; }
- public LocalDateTime getUpdatedAt() { return updatedAt; }
+ public Status getStatus() {
+ return status;
+ }
- public List getItems() { return items; }
- public void setItems(List items) { this.items = items; }
+ public void setStatus(Status status) {
+ 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 BigDecimal getTotal() {
+ return total;
+ }
+
+ public void setTotal(BigDecimal total) {
+ this.total = total;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public LocalDateTime getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public List getItems() {
+ return items;
+ }
+
+ public void setItems(List items) {
+ this.items = items;
+ }
+
+ public List getDirecciones() {
+ return direcciones;
+ }
+
+ public void setDirecciones(List direcciones) {
+ this.direcciones = direcciones;
+ }
+
+ public void addDireccion(CartDireccion d) {
+ direcciones.add(d);
+ d.setCart(this);
+ }
+
+ public void removeDireccion(CartDireccion d) {
+ direcciones.remove(d);
+ d.setCart(null);
+ }
}
diff --git a/src/main/java/com/imprimelibros/erp/cart/CartController.java b/src/main/java/com/imprimelibros/erp/cart/CartController.java
index 4bb4f78..5fa7504 100644
--- a/src/main/java/com/imprimelibros/erp/cart/CartController.java
+++ b/src/main/java/com/imprimelibros/erp/cart/CartController.java
@@ -3,71 +3,105 @@ package com.imprimelibros.erp.cart;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
-
-import com.imprimelibros.erp.users.UserDetailsImpl;
+import org.springframework.web.server.ResponseStatusException;
import jakarta.servlet.http.HttpServletRequest;
-import com.imprimelibros.erp.users.User;
-
+import org.springframework.context.MessageSource;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
-import org.springframework.security.core.Authentication;
+import com.imprimelibros.erp.common.Utils;
+import com.imprimelibros.erp.direcciones.Direccion;
+import com.imprimelibros.erp.direcciones.DireccionService;
+import com.imprimelibros.erp.i18n.TranslationService;
import java.security.Principal;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
+import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
+
@Controller
@RequestMapping("/cart")
public class CartController {
- private final CartService service;
+ protected final CartService service;
+ protected DireccionService direccionService;
+ protected MessageSource messageSource;
+ protected TranslationService translationService;
- public CartController(CartService service) {
+ public CartController(CartService service, DireccionService direccionService, MessageSource messageSource,
+ TranslationService translationService) {
this.service = service;
- }
-
- /**
- * Obtiene el ID de usuario desde tu seguridad.
- * Adáptalo a tu UserDetails (e.g., SecurityContext con getId())
- */
- private Long currentUserId(Principal principal) {
- if (principal == null) {
- throw new IllegalStateException("Usuario no autenticado");
- }
-
- if (principal instanceof Authentication auth) {
- Object principalObj = auth.getPrincipal();
-
- if (principalObj instanceof UserDetailsImpl udi) {
- return udi.getId();
- } else if (principalObj instanceof User u && u.getId() != null) {
- return u.getId();
- }
- }
-
- throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
+ this.direccionService = direccionService;
+ this.messageSource = messageSource;
+ this.translationService = translationService;
}
/** Vista del carrito */
@GetMapping
public String viewCart(Model model, Principal principal, Locale locale) {
- var items = service.listItems(currentUserId(principal), locale);
+
+ List keys = List.of(
+ "app.cancelar",
+ "app.seleccionar",
+ "cart.shipping.add.title",
+ "cart.shipping.select-placeholder",
+ "cart.shipping.new-address",
+ "cart.shipping.errors.noAddressSelected",
+ "cart.shipping.enter-units",
+ "cart.shipping.units-label",
+ "cart.shipping.errors.units-error",
+ "cart.shipping.ud",
+ "cart.shipping.uds",
+ "cart.shipping.send-in-palets",
+ "cart.shipping.send-in-palets.info",
+ "cart.shipping.tipo-envio",
+ "cart.pass-to.customer.error",
+ "cart.pass-to.customer.error-move",
+ "app.yes",
+ "app.aceptar",
+ "app.cancelar");
+
+ Map translations = translationService.getTranslations(locale, keys);
+ model.addAttribute("languageBundle", translations);
+
+ Long userId = Utils.currentUserId(principal);
+ Cart cart = service.getOrCreateActiveCart(userId);
+
+ var items = service.listItems(userId, locale);
model.addAttribute("items", items);
+
+ Map direcciones = service.getCartDirecciones(cart.getId(), locale);
+ if (direcciones != null && direcciones.containsKey("mainDir"))
+ model.addAttribute("mainDir", direcciones.get("mainDir"));
+ else if (direcciones != null && direcciones.containsKey("direcciones"))
+ model.addAttribute("direcciones", direcciones.get("direcciones"));
+
+ var summary = service.getCartSummary(cart, locale);
+ model.addAttribute("cartSummary", summary);
+ if (summary.get("errorShipmentCost") != null && (Boolean) summary.get("errorShipmentCost"))
+ model.addAttribute("errorEnvio", true);
+ else
+ model.addAttribute("errorEnvio", false);
+
+ model.addAttribute("cart", cart);
return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple)
}
/** Añadir presupuesto via POST form */
@PostMapping("/add")
public String add(@PathVariable(name = "presupuestoId", required = true) Long presupuestoId, Principal principal) {
- service.addPresupuesto(currentUserId(principal), presupuestoId);
+ service.addPresupuesto(Utils.currentUserId(principal), presupuestoId);
return "redirect:/cart";
}
/** Añadir presupuesto con ruta REST (opcional) */
@PostMapping("/add/{presupuestoId}")
public Object addPath(@PathVariable Long presupuestoId, Principal principal, HttpServletRequest request) {
- service.addPresupuesto(currentUserId(principal), presupuestoId);
+ service.addPresupuesto(Utils.currentUserId(principal), presupuestoId);
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
if (isAjax) {
// Responder 200 con la URL a la que quieres ir
@@ -83,13 +117,13 @@ public class CartController {
public long getCount(Principal principal) {
if (principal == null)
return 0;
- return service.countItems(currentUserId(principal));
+ return service.countItems(Utils.currentUserId(principal));
}
/** Eliminar línea por ID de item */
@DeleteMapping("/{itemId}/remove")
public String remove(@PathVariable Long itemId, Principal principal) {
- service.removeItem(currentUserId(principal), itemId);
+ service.removeItem(Utils.currentUserId(principal), itemId);
return "redirect:/cart";
}
@@ -97,14 +131,72 @@ public class CartController {
@DeleteMapping("/delete/item/{presupuestoId}")
@ResponseBody
public String removeByPresupuesto(@PathVariable Long presupuestoId, Principal principal) {
- service.removeByPresupuesto(currentUserId(principal), presupuestoId);
+ service.removeByPresupuesto(Utils.currentUserId(principal), presupuestoId);
return "redirect:/cart";
}
/** Vaciar carrito completo */
@DeleteMapping("/clear")
public String clear(Principal principal) {
- service.clear(currentUserId(principal));
+ service.clear(Utils.currentUserId(principal));
return "redirect:/cart";
}
+
+ @GetMapping("/get-address/{id}")
+ public String getDireccionCard(@PathVariable Long id, @RequestParam(required = false) Long presupuestoId,
+ @RequestParam(required = false) Integer unidades,
+ @RequestParam(required = false) Integer isPalets,
+ Model model, Locale locale) {
+ Direccion dir = direccionService.findById(id)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ model.addAttribute("pais", messageSource.getMessage("paises." + dir.getPais().getKeyword(), null,
+ dir.getPais().getKeyword(), locale));
+ model.addAttribute("presupuestoId", presupuestoId);
+ model.addAttribute("unidades", unidades);
+ model.addAttribute("isPalets", isPalets);
+ model.addAttribute("direccion", dir);
+
+ return "imprimelibros/direcciones/direccionCard :: direccionCard(direccion=${direccion})";
+ }
+
+ @PostMapping(value = "/update/{id}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+ public String updateCart(@PathVariable Long id, UpdateCartRequest updateRequest, Model model, Locale locale,
+ Principal principal) {
+
+ 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) {
+
+ // redirect to cart with error message
+ String errorMessage = messageSource.getMessage("cart.update.error", null, "Error updating cart", locale);
+ model.addAttribute("errorMessage", errorMessage);
+ return "redirect:/cart";
+ }
+ }
+
+ @PostMapping(value = "/pass-to-customer/{customerId}")
+ public ResponseEntity> moveToCustomer(
+ @PathVariable Long customerId,
+ Principal principal) {
+
+ if(!Utils.isCurrentUserAdmin()) {
+ return ResponseEntity.status(403).body(Map.of("error", "Forbidden"));
+ }
+
+ Long userId = Utils.currentUserId(principal);
+ Cart cart = service.getOrCreateActiveCart(userId);
+
+ boolean ok = service.moveCartToCustomer(cart.getId(), customerId);
+
+ if (ok)
+ return ResponseEntity.ok().build();
+ return ResponseEntity.status(400).body(Map.of("error", "cart.errors.move-cart"));
+ }
+
+
}
diff --git a/src/main/java/com/imprimelibros/erp/cart/CartDireccion.java b/src/main/java/com/imprimelibros/erp/cart/CartDireccion.java
new file mode 100644
index 0000000..dd94623
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/cart/CartDireccion.java
@@ -0,0 +1,101 @@
+package com.imprimelibros.erp.cart;
+
+import java.util.Locale;
+
+import org.springframework.context.MessageSource;
+import com.imprimelibros.erp.cart.dto.DireccionCardDTO;
+import com.imprimelibros.erp.direcciones.Direccion;
+import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
+
+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;
+
+ // --- 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 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
+ );
+ }
+
+}
diff --git a/src/main/java/com/imprimelibros/erp/cart/CartItem.java b/src/main/java/com/imprimelibros/erp/cart/CartItem.java
index 853214c..7579201 100644
--- a/src/main/java/com/imprimelibros/erp/cart/CartItem.java
+++ b/src/main/java/com/imprimelibros/erp/cart/CartItem.java
@@ -3,6 +3,8 @@ package com.imprimelibros.erp.cart;
import jakarta.persistence.*;
import java.time.LocalDateTime;
+import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
+
@Entity
@Table(
name = "cart_items",
@@ -17,8 +19,9 @@ public class CartItem {
@JoinColumn(name = "cart_id", nullable = false)
private Cart cart;
- @Column(name = "presupuesto_id", nullable = false)
- private Long presupuestoId;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "presupuesto_id", nullable = false)
+ private Presupuesto presupuesto;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@@ -29,8 +32,8 @@ public class CartItem {
public Cart getCart() { return cart; }
public void setCart(Cart cart) { this.cart = cart; }
- public Long getPresupuestoId() { return presupuestoId; }
- public void setPresupuestoId(Long presupuestoId) { this.presupuestoId = presupuestoId; }
+ public Presupuesto getPresupuesto() { return presupuesto; }
+ public void setPresupuesto(Presupuesto presupuesto) { this.presupuesto = presupuesto; }
public LocalDateTime getCreatedAt() { return createdAt; }
}
diff --git a/src/main/java/com/imprimelibros/erp/cart/CartRepository.java b/src/main/java/com/imprimelibros/erp/cart/CartRepository.java
index d23d60a..2f1ad0c 100644
--- a/src/main/java/com/imprimelibros/erp/cart/CartRepository.java
+++ b/src/main/java/com/imprimelibros/erp/cart/CartRepository.java
@@ -1,9 +1,22 @@
package com.imprimelibros.erp.cart;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
import java.util.Optional;
public interface CartRepository extends JpaRepository {
Optional findByUserIdAndStatus(Long userId, Cart.Status status);
+
+ @Query("""
+ select distinct c from Cart c
+ left join fetch c.direcciones cd
+ left join fetch cd.direccion d
+ left join fetch d.pais p
+ left join fetch cd.presupuesto pr
+ where c.id = :id
+ """)
+ Optional findByIdFetchAll(@Param("id") Long id);
+
}
diff --git a/src/main/java/com/imprimelibros/erp/cart/CartService.java b/src/main/java/com/imprimelibros/erp/cart/CartService.java
index d04b349..1980f4d 100644
--- a/src/main/java/com/imprimelibros/erp/cart/CartService.java
+++ b/src/main/java/com/imprimelibros/erp/cart/CartService.java
@@ -1,6 +1,6 @@
package com.imprimelibros.erp.cart;
-import jakarta.transaction.Transactional;
+import org.springframework.transaction.annotation.Transactional;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;
@@ -10,32 +10,43 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
+import com.imprimelibros.erp.cart.dto.DireccionCardDTO;
+import com.imprimelibros.erp.cart.dto.DireccionShipment;
+import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
import com.imprimelibros.erp.common.Utils;
+import com.imprimelibros.erp.direcciones.DireccionService;
+import com.imprimelibros.erp.externalApi.skApiClient;
+import com.imprimelibros.erp.pedido.PedidoService;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
-
@Service
public class CartService {
private final CartRepository cartRepo;
private final CartItemRepository itemRepo;
private final MessageSource messageSource;
- private final PresupuestoFormatter presupuestoFormatter;
private final PresupuestoRepository presupuestoRepo;
private final Utils utils;
+ private final DireccionService direccionService;
+ private final skApiClient skApiClient;
+ private final PedidoService pedidoService;
public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
MessageSource messageSource, PresupuestoFormatter presupuestoFormatter,
- PresupuestoRepository presupuestoRepo, Utils utils) {
+ PresupuestoRepository presupuestoRepo, Utils utils, DireccionService direccionService,
+ skApiClient skApiClient, PedidoService pedidoService) {
this.cartRepo = cartRepo;
this.itemRepo = itemRepo;
this.messageSource = messageSource;
- this.presupuestoFormatter = presupuestoFormatter;
this.presupuestoRepo = presupuestoRepo;
this.utils = utils;
+ this.direccionService = direccionService;
+ this.skApiClient = skApiClient;
+ this.pedidoService = pedidoService;
}
/** Devuelve el carrito activo o lo crea si no existe. */
@@ -50,6 +61,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. */
@Transactional
public List> listItems(Long userId, Locale locale) {
@@ -58,14 +74,13 @@ public class CartService {
List items = itemRepo.findByCartId(cart.getId());
for (CartItem item : items) {
- Presupuesto p = presupuestoRepo.findById(item.getPresupuestoId())
- .orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + item.getPresupuestoId()));
+ Presupuesto p = item.getPresupuesto();
Map elemento = getElementoCart(p, locale);
elemento.put("cartItemId", item.getId());
resultados.add(elemento);
}
- //System.out.println("Cart items: " + resultados);
+ // System.out.println("Cart items: " + resultados);
return resultados;
}
@@ -77,7 +92,8 @@ public class CartService {
if (!exists) {
CartItem ci = new CartItem();
ci.setCart(cart);
- ci.setPresupuestoId(presupuestoId);
+ ci.setPresupuesto(presupuestoRepo.findById(presupuestoId)
+ .orElseThrow(() -> new IllegalArgumentException("Presupuesto no encontrado")));
itemRepo.save(ci);
}
}
@@ -96,8 +112,9 @@ public class CartService {
@Transactional
public void removeByPresupuesto(Long userId, Long presupuestoId) {
Cart cart = getOrCreateActiveCart(userId);
- itemRepo.findByCartIdAndPresupuestoId(cart.getId(), presupuestoId)
- .ifPresent(itemRepo::delete);
+ CartItem item = itemRepo.findByCartIdAndPresupuestoId(cart.getId(), presupuestoId)
+ .orElseThrow(() -> new IllegalArgumentException("Item no encontrado"));
+ itemRepo.deleteById(item.getId());
}
/** Vacía todo el carrito activo. */
@@ -122,9 +139,9 @@ public class CartService {
}
private Map getElementoCart(Presupuesto presupuesto, Locale locale) {
-
+
Map resumen = new HashMap<>();
-
+
resumen.put("titulo", presupuesto.getTitulo());
resumen.put("imagen",
@@ -134,8 +151,15 @@ public class CartService {
resumen.put("presupuestoId", presupuesto.getId());
+ if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
+ resumen.put("hasSample", true);
+ } else {
+ resumen.put("hasSample", false);
+ }
Map detalles = utils.getTextoPresupuesto(presupuesto, locale);
+ resumen.put("tirada", presupuesto.getSelectedTirada());
+
resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale));
resumen.put("base", presupuesto.getBaseImponible());
resumen.put("iva4", presupuesto.getIvaImporte4());
@@ -145,4 +169,257 @@ public class CartService {
return resumen;
}
+
+ public Map getCartSummary(Cart cart, Locale locale) {
+
+ double base = 0.0;
+ double iva4 = 0.0;
+ double iva21 = 0.0;
+ double shipment = 0.0;
+
+ Boolean errorShipementCost = false;
+
+ List items = cart.getItems();
+ List direcciones = cart.getDirecciones();
+
+ for (CartItem item : items) {
+ Presupuesto p = item.getPresupuesto();
+ Double peso = p.getPeso() != null ? p.getPeso().doubleValue() : 0.0;
+ base += p.getBaseImponible().doubleValue();
+ iva4 += p.getIvaImporte4().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) {
+ Integer unidades = p.getSelectedTirada();
+ Map res = getShippingCost(cd, peso, unidades, locale);
+ if (res.get("success").equals(Boolean.FALSE)) {
+ errorShipementCost = true;
+ }
+ else{
+ shipment += (Double) res.get("shipment");
+ iva21 += (Double) res.get("iva21");
+ }
+
+ }
+ // si tiene prueba de envio, hay que añadir el coste
+ if (p.getServiciosJson() != null && p.getServiciosJson().contains("ejemplar-prueba")) {
+
+ Map res = getShippingCost(cd, peso, 1, locale);
+ if (res.get("success").equals(Boolean.FALSE)) {
+ errorShipementCost = true;
+ }
+ else{
+ shipment += (Double) res.get("shipment");
+ iva21 += (Double) res.get("iva21");
+ }
+ }
+ }
+ } else {
+ // envio por cada presupuesto
+ // buscar la direccion asignada a este presupuesto
+ if (direcciones == null)
+ continue;
+ List cd_presupuesto = direcciones.stream()
+ .filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(p.getId())
+ && d.getUnidades() != null && d.getUnidades() != null && d.getUnidades() > 0)
+ .toList();
+ Boolean firstDirection = true;
+ for (CartDireccion cd : cd_presupuesto) {
+ Integer unidades = cd.getUnidades();
+ if (firstDirection) {
+ Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(),
+ cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
+ if (!freeShipment && unidades != null && unidades > 0) {
+ Map res = getShippingCost(cd, peso, unidades, locale);
+ if (res.get("success").equals(Boolean.FALSE)) {
+ errorShipementCost = true;
+ } else {
+ shipment += (Double) res.get("shipment");
+ iva21 += (Double) res.get("iva21");
+ }
+ }
+ firstDirection = false;
+ } else {
+ Map res = getShippingCost(cd, peso, unidades, locale);
+ if (res.get("success").equals(Boolean.FALSE)) {
+ errorShipementCost = true;
+ } else {
+ shipment += (Double) res.get("shipment");
+ iva21 += (Double) res.get("iva21");
+ }
+ }
+ }
+ // ejemplar de prueba
+ CartDireccion cd_prueba = direcciones.stream()
+ .filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(p.getId())
+ && d.getUnidades() == null)
+ .findFirst().orElse(null);
+ if (cd_prueba != null) {
+
+ Map res = getShippingCost(cd_prueba, peso, 1, locale);
+ if (res.get("success").equals(Boolean.FALSE)) {
+ errorShipementCost = true;
+ }
+ else{
+ shipment += (Double) res.get("shipment");
+ iva21 += (Double) res.get("iva21");
+ }
+ }
+ }
+ }
+
+ double total = base + iva4 + iva21 + shipment;
+
+ int fidelizacion = pedidoService.getDescuentoFidelizacion();
+ double descuento = (total) * fidelizacion / 100.0;
+ total -= descuento;
+
+ Map summary = new HashMap<>();
+ summary.put("base", Utils.formatCurrency(base, locale));
+ summary.put("iva4", Utils.formatCurrency(iva4, locale));
+ summary.put("iva21", Utils.formatCurrency(iva21, locale));
+ summary.put("shipment", Utils.formatCurrency(shipment, locale));
+ summary.put("fidelizacion", fidelizacion + "%");
+ summary.put("descuento", Utils.formatCurrency(-descuento, locale));
+ summary.put("total", Utils.formatCurrency(total, locale));
+ summary.put("errorShipmentCost", errorShipementCost);
+
+ return summary;
+ }
+
+ @Transactional(readOnly = true)
+ public Map getCartDirecciones(Long cartId, Locale locale) {
+ Cart cart = cartRepo.findByIdFetchAll(cartId)
+ .orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
+
+ Map result = new HashMap<>();
+ List direcciones = cart.getDirecciones();
+
+ if (cart.getOnlyOneShipment() && !direcciones.isEmpty()) {
+ result.put("mainDir", direcciones.get(0).toDireccionCard(messageSource, locale));
+ } else {
+ List dirCards = cart.getDirecciones().stream()
+ .filter(Objects::nonNull)
+ .map(cd -> cd.toDireccionCard(messageSource, locale))
+ .filter(Objects::nonNull)
+ .toList();
+ result.put("direcciones", dirCards);
+ }
+
+ return result;
+ }
+
+ @Transactional
+ public Boolean updateCart(Long cartId, UpdateCartRequest request) {
+
+ try {
+ Cart cart = cartRepo.findById(cartId)
+ .orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
+ cart.setOnlyOneShipment(request.isOnlyOneShipment());
+ // Borramos todas las direcciones actuales de la bbdd
+ // Opcional (limpieza): romper backref antes de clear
+ for (CartDireccion d : cart.getDirecciones()) {
+ d.setCart(null);
+ }
+ cart.getDirecciones().clear();
+ // Guardamos las direcciones
+ List direcciones = request.getDirecciones();
+ if (direcciones != null && direcciones.size() > 0) {
+ for (DireccionShipment dir : direcciones) {
+ // Crear una nueva CartDireccion por cada item
+ CartDireccion cd = new CartDireccion();
+ cd.setCart(cart);
+ cd.setDireccion(dir.getId() != null ? direccionService.findById(dir.getId())
+ .orElseThrow(() -> new IllegalArgumentException("Dirección no encontrada")) : null);
+ cd.setIsPalets(dir.getIsPalets() != null ? dir.getIsPalets() : false);
+ cd.setUnidades(dir.getUnidades() != null ? dir.getUnidades() : null);
+ if (dir.getPresupuestoId() != null) {
+ Presupuesto p = presupuestoRepo.findById(dir.getPresupuestoId())
+ .orElse(null);
+ cd.setPresupuesto(p);
+ }
+ cart.addDireccion(cd);
+ }
+ } else {
+
+ }
+ cartRepo.save(cart);
+ return true;
+ } catch (Exception e) {
+ // Manejo de excepciones
+ return false;
+ }
+ }
+
+ public Boolean moveCartToCustomer(Long cartId, Long customerId) {
+ try {
+
+ // Remove the cart from the customer if they have one
+ Cart existingCart = cartRepo.findByUserIdAndStatus(customerId, Cart.Status.ACTIVE)
+ .orElse(null);
+ if (existingCart != null) {
+ cartRepo.delete(existingCart);
+ }
+
+ Cart cart = cartRepo.findById(cartId)
+ .orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
+
+ cart.setUserId(customerId);
+ cartRepo.save(cart);
+ return true;
+
+ } catch (Exception e) {
+ // Manejo de excepciones
+ return false;
+ }
+ }
+
+ /***************************************
+ * MÉTODOS PRIVADOS
+ ***************************************/
+
+ private Map getShippingCost(
+ CartDireccion cd,
+ Double peso,
+ Integer unidades,
+ Locale locale) {
+
+ Map result = new HashMap<>();
+
+ try {
+ Map data = Map.of(
+ "cp", cd.getDireccion().getCp(),
+ "pais_code3", cd.getDireccion().getPaisCode3(),
+ "peso", peso != null ? peso : 0.0,
+ "unidades", unidades,
+ "palets", Boolean.TRUE.equals(cd.getIsPalets()) ? 1 : 0);
+
+ var shipmentCost = skApiClient.getCosteEnvio(data, locale);
+
+ if (shipmentCost != null && shipmentCost.get("data") != null) {
+ Number n = (Number) shipmentCost.get("data");
+ double cost = n.doubleValue();
+
+ result.put("success", true);
+ result.put("shipment", cost);
+ result.put("iva21", cost * 0.21);
+ } else {
+ result.put("success", false);
+ result.put("shipment", 0.0);
+ result.put("iva21", 0.0);
+ }
+ } catch (Exception e) {
+ result.put("success", false);
+ result.put("shipment", 0.0);
+ result.put("iva21", 0.0);
+ }
+
+ return result;
+ }
+
}
diff --git a/src/main/java/com/imprimelibros/erp/cart/dto/CartDireccionRepository.java b/src/main/java/com/imprimelibros/erp/cart/dto/CartDireccionRepository.java
new file mode 100644
index 0000000..ba1e740
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/cart/dto/CartDireccionRepository.java
@@ -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 {
+
+ // Borrado masivo por cart_id
+ void deleteByCartId(Long cartId);
+
+ // Lectura por cart_id (útil para componer respuestas)
+ List findByCartId(Long cartId);
+}
diff --git a/src/main/java/com/imprimelibros/erp/cart/dto/DireccionCardDTO.java b/src/main/java/com/imprimelibros/erp/cart/dto/DireccionCardDTO.java
new file mode 100644
index 0000000..fb3da47
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/cart/dto/DireccionCardDTO.java
@@ -0,0 +1,39 @@
+package com.imprimelibros.erp.cart.dto;
+
+import com.imprimelibros.erp.direcciones.Direccion;
+
+public class DireccionCardDTO {
+ private final Direccion direccion;
+ private final Long presupuestoId;
+ private final Integer unidades;
+ private final Boolean isPalets;
+ private final String pais;
+
+ public DireccionCardDTO(Direccion direccion, Long presupuestoId, Integer unidades, Boolean isPalets, String pais) {
+ this.direccion = direccion;
+ this.presupuestoId = presupuestoId;
+ this.unidades = unidades;
+ this.isPalets = isPalets;
+ this.pais = pais;
+ }
+
+ public Direccion getDireccion() {
+ return direccion;
+ }
+
+ public Long getPresupuestoId() {
+ return presupuestoId;
+ }
+
+ public Integer getUnidades() {
+ return unidades;
+ }
+
+ public Boolean getIsPalets() {
+ return isPalets;
+ }
+
+ public String getPais() {
+ return pais;
+ }
+}
diff --git a/src/main/java/com/imprimelibros/erp/cart/dto/DireccionShipment.java b/src/main/java/com/imprimelibros/erp/cart/dto/DireccionShipment.java
new file mode 100644
index 0000000..a6c5fea
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/cart/dto/DireccionShipment.java
@@ -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;
+ }
+}
+
diff --git a/src/main/java/com/imprimelibros/erp/cart/dto/UpdateCartRequest.java b/src/main/java/com/imprimelibros/erp/cart/dto/UpdateCartRequest.java
new file mode 100644
index 0000000..515afc9
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/cart/dto/UpdateCartRequest.java
@@ -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 direcciones = new ArrayList<>();
+
+ public boolean isOnlyOneShipment() { // boolean-style getter
+ return Boolean.TRUE.equals(onlyOneShipment);
+ }
+
+ public void setOnlyOneShipment(Boolean onlyOneShipment) {
+ this.onlyOneShipment = onlyOneShipment;
+ }
+
+ public List getDirecciones() {
+ return direcciones;
+ }
+
+ public void setDirecciones(List direcciones) {
+ this.direcciones = (direcciones != null) ? direcciones : new ArrayList<>();
+ }
+}
+
diff --git a/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java
new file mode 100644
index 0000000..00474fc
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/checkout/CheckoutController.java
@@ -0,0 +1,66 @@
+package com.imprimelibros.erp.checkout;
+
+import java.security.Principal;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.springframework.context.MessageSource;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import com.imprimelibros.erp.common.Utils;
+import com.imprimelibros.erp.i18n.TranslationService;
+import com.imprimelibros.erp.paises.PaisesService;
+
+import com.imprimelibros.erp.direcciones.DireccionService;
+
+import com.imprimelibros.erp.cart.CartService;
+
+@Controller
+@RequestMapping("/checkout")
+public class CheckoutController {
+
+ protected CartService cartService;
+ protected TranslationService translationService;
+ protected PaisesService paisesService;
+ protected DireccionService direccionService;
+ protected MessageSource messageSource;
+
+ public CheckoutController(CartService cartService, TranslationService translationService,
+ PaisesService paisesService, DireccionService direccionService, MessageSource messageSource) {
+ this.cartService = cartService;
+ this.translationService = translationService;
+ this.paisesService = paisesService;
+ this.direccionService = direccionService;
+ this.messageSource = messageSource;
+ }
+
+ @GetMapping
+ public String view(Model model, Principal principal, Locale locale) {
+
+ List keys = List.of(
+ "app.cancelar",
+ "app.seleccionar",
+ "checkout.shipping.add.title",
+ "checkout.shipping.select-placeholder",
+ "checkout.shipping.new-address",
+ "app.yes",
+ "app.cancelar");
+
+ Map translations = translationService.getTranslations(locale, keys);
+ model.addAttribute("languageBundle", translations);
+
+ var items = this.cartService.listItems(Utils.currentUserId(principal), locale);
+ for (var item : items) {
+ if (item.get("hasSample") != null && (Boolean) item.get("hasSample")) {
+ model.addAttribute("hasSample", true);
+ break;
+ }
+ }
+ model.addAttribute("items", items);
+ return "imprimelibros/checkout/checkout"; // crea esta vista si quieres (tabla simple)
+ }
+}
diff --git a/src/main/java/com/imprimelibros/erp/common/Utils.java b/src/main/java/com/imprimelibros/erp/common/Utils.java
index b6b2a79..a5262b7 100644
--- a/src/main/java/com/imprimelibros/erp/common/Utils.java
+++ b/src/main/java/com/imprimelibros/erp/common/Utils.java
@@ -2,6 +2,7 @@ package com.imprimelibros.erp.common;
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.security.Principal;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
@@ -12,6 +13,8 @@ import java.util.Optional;
import java.util.function.BiFunction;
import org.springframework.context.MessageSource;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -22,6 +25,8 @@ import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices;
import com.imprimelibros.erp.presupuesto.marcapaginas.Marcapaginas;
+import com.imprimelibros.erp.users.User;
+import com.imprimelibros.erp.users.UserDetailsImpl;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Path;
@@ -40,6 +45,30 @@ public class Utils {
this.messageSource = messageSource;
}
+ public static boolean isCurrentUserAdmin() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ return auth.getAuthorities().stream()
+ .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN"));
+ }
+
+ public static Long currentUserId(Principal principal) {
+
+ if (principal == null) {
+ throw new IllegalStateException("Usuario no autenticado");
+ }
+
+ if (principal instanceof Authentication auth) {
+ Object principalObj = auth.getPrincipal();
+
+ if (principalObj instanceof UserDetailsImpl udi) {
+ return udi.getId();
+ } else if (principalObj instanceof User u && u.getId() != null) {
+ return u.getId();
+ }
+ }
+ throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
+ }
+
public static String formatCurrency(BigDecimal amount, Locale locale) {
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
return currencyFormatter.format(amount);
diff --git a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java
index 392b416..096488d 100644
--- a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java
+++ b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java
@@ -91,7 +91,6 @@ public class SecurityConfig {
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
.csrf(csrf -> csrf
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/")))
-
// ====== RequestCache: sólo navegaciones HTML reales ======
.requestCache(rc -> {
HttpSessionRequestCache cache = new HttpSessionRequestCache();
diff --git a/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java b/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java
index 8993f40..28d2bad 100644
--- a/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java
+++ b/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java
@@ -1,5 +1,6 @@
package com.imprimelibros.erp.direcciones;
+import java.security.Principal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@@ -43,20 +44,23 @@ import jakarta.validation.Valid;
@RequestMapping("/direcciones")
public class DireccionController {
+ private final DireccionService direccionService;
+
protected final DireccionRepository repo;
protected final PaisesService paisesService;
protected final MessageSource messageSource;
protected final UserDao userRepo;
protected final TranslationService translationService;
-
public DireccionController(DireccionRepository repo, PaisesService paisesService,
- MessageSource messageSource, UserDao userRepo, TranslationService translationService) {
+ MessageSource messageSource, UserDao userRepo, TranslationService translationService,
+ DireccionService direccionService) {
this.repo = repo;
this.paisesService = paisesService;
this.messageSource = messageSource;
this.userRepo = userRepo;
this.translationService = translationService;
+ this.direccionService = direccionService;
}
@GetMapping()
@@ -295,6 +299,33 @@ public class DireccionController {
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
+ @GetMapping("direction-form")
+ public String getForm(@RequestParam(required = false) Long id,
+ Direccion direccion,
+ BindingResult binding,
+ Model model,
+ HttpServletResponse response,
+ Principal principal,
+ Locale locale) {
+
+ model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
+
+ Direccion newDireccion = new Direccion();
+
+ User user = null;
+ if (principal instanceof UserDetailsImpl udi) {
+ user = new User();
+ user.setId(udi.getId());
+ } else if (principal instanceof User u && u.getId() != null) {
+ user = u;
+ }
+ newDireccion.setUser(user);
+ model.addAttribute("dirForm", newDireccion);
+ model.addAttribute("action", "/direcciones/add");
+
+ return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm";
+ }
+
@PostMapping
public String create(
@Valid @ModelAttribute("dirForm") Direccion direccion,
@@ -327,6 +358,34 @@ public class DireccionController {
return null;
}
+ // para el formulario modal en checkout
+ @PostMapping("/add")
+ public String create2(
+ @Valid @ModelAttribute("dirForm") Direccion direccion,
+ BindingResult binding,
+ Model model,
+ HttpServletResponse response,
+ Authentication auth,
+ Locale locale) {
+
+ User current = userRepo.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(auth.getName()).orElse(null);
+ direccion.setUser(current);
+
+ if (binding.hasErrors()) {
+ response.setStatus(422);
+ model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
+ model.addAttribute("action", "/direcciones/add");
+ model.addAttribute("dirForm", direccion);
+ return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm";
+ }
+
+ var data = direccion;
+
+ repo.save(data);
+ response.setStatus(201);
+ return null;
+ }
+
@PostMapping("/{id}")
public String update(
@PathVariable Long id,
@@ -416,12 +475,36 @@ public class DireccionController {
}
}
+ @GetMapping(value = "/select2", produces = "application/json")
+ @ResponseBody
+ public Map getSelect2(
+ @RequestParam(value = "q", required = false) String q1,
+ @RequestParam(value = "term", required = false) String q2,
+ @RequestParam(value = "presupuestoId", required = false) Long presupuestoId,
+ Authentication auth) {
+
+ boolean isAdmin = auth.getAuthorities().stream()
+ .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN"));
+
+ Long currentUserId = null;
+ if (!isAdmin) {
+ if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
+ currentUserId = udi.getId();
+ } else if (auth != null) {
+ currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
+ }
+ }
+
+ return direccionService.getForSelect(q1, q2, isAdmin ? null : currentUserId);
+
+ }
+
private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) {
if (auth == null) {
return false;
}
boolean isAdmin = auth.getAuthorities().stream()
- .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
+ .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN"));
if (isAdmin) {
return true;
}
@@ -434,4 +517,5 @@ public class DireccionController {
}
return currentUserId != null && currentUserId.equals(ownerId);
}
+
}
diff --git a/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java
new file mode 100644
index 0000000..1f1d4d5
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/direcciones/DireccionService.java
@@ -0,0 +1,97 @@
+package com.imprimelibros.erp.direcciones;
+
+import java.text.Collator;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class DireccionService {
+
+ protected DireccionRepository repo;
+
+ public DireccionService(DireccionRepository repo) {
+ this.repo = repo;
+ }
+
+ public Map getForSelect(String q1, String q2, Long userId) {
+ try {
+
+ // Termino de búsqueda (Select2 usa 'q' o 'term' según versión/config)
+ String search = Optional.ofNullable(q1).orElse(q2);
+ if (search != null) {
+ search = search.trim();
+ }
+ final String q = (search == null || search.isEmpty())
+ ? null
+ : search.toLowerCase();
+
+ List all = userId != null ? repo.findByUserId(userId) : repo.findAll();
+
+ // Mapear a opciones id/text con i18n y filtrar por búsqueda si llega
+ List> options = all.stream()
+ .map(cc -> {
+ String id = cc.getId().toString();
+ String alias = cc.getAlias();
+ String direccion = cc.getDireccion();
+ String cp = String.valueOf(cc.getCp());
+ String ciudad = cc.getCiudad();
+ String att = cc.getAtt();
+ Map m = new HashMap<>();
+ m.put("id", id); // lo normal en Select2: id = valor que guardarás (code3)
+ m.put("text", alias); // texto mostrado, i18n con fallback a keyword
+ m.put("cp", cp);
+ m.put("ciudad", ciudad);
+ m.put("att", att);
+ m.put("alias", alias);
+ m.put("direccion", direccion);
+ return m;
+ })
+ .filter(opt -> {
+ if (q == null || q.isEmpty())
+ return true;
+ String cp = opt.get("cp");
+ String ciudad = opt.get("ciudad").toLowerCase();
+ String att = opt.get("att").toLowerCase();
+ String alias = opt.get("alias").toLowerCase();
+ String text = opt.get("text").toLowerCase();
+ String direccion = opt.get("direccion").toLowerCase();
+ return text.contains(q) || cp.contains(q) || ciudad.contains(q) || att.contains(q)
+ || alias.contains(q) || direccion.contains(q);
+ })
+ .sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance()))
+ .collect(Collectors.toList());
+
+ // Estructura Select2
+ Map resp = new HashMap<>();
+ resp.put("results", options);
+ return resp;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return Map.of("results", List.of());
+ }
+ }
+
+ public Optional findById(Long id) {
+ return repo.findById(id);
+ }
+
+ public Boolean checkFreeShipment(Integer cp, String paisCode3) {
+ if (paisCode3 != null && paisCode3.toLowerCase().equals("esp") && cp != null) {
+ // Excluir Canarias (35xxx y 38xxx), Baleares (07xxx), Ceuta (51xxx), Melilla
+ // (52xxx)
+ int provincia = cp / 1000;
+
+ if (provincia != 7 && provincia != 35 && provincia != 38 && provincia != 51 && provincia != 52) {
+ return true; // España peninsular
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java
index 4e8fcb3..e586f1f 100644
--- a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java
+++ b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java
@@ -6,6 +6,7 @@ import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -18,8 +19,10 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
import java.util.Map;
+import java.util.Optional;
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.function.Supplier;
@@ -219,6 +222,54 @@ public class skApiClient {
}
}
+ public Map getCosteEnvio(Map data, Locale locale) {
+
+ return performWithRetryMap(() -> {
+ String url = this.skApiUrl + "api/calcular-envio";
+
+ URI uri = UriComponentsBuilder.fromUriString(url)
+ .queryParam("pais_code3", data.get("pais_code3"))
+ .queryParam("cp", data.get("cp"))
+ .queryParam("peso", data.get("peso"))
+ .queryParam("unidades", data.get("unidades"))
+ .queryParam("palets", data.get("palets"))
+ .build(true) // no re-encode []
+ .toUri();
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(authService.getToken());
+
+
+ ResponseEntity response = restTemplate.exchange(
+ uri,
+ HttpMethod.GET,
+ new HttpEntity<>(headers),
+ String.class);
+
+ try {
+ Map responseBody = new ObjectMapper().readValue(
+ response.getBody(),
+ new TypeReference>() {
+ });
+ Boolean error = (Boolean) responseBody.get("error");
+ if (error != null && error) {
+ return Map.of("error", messageSource.getMessage("direcciones.error.noShippingCost", null, locale));
+ } else {
+ Double total = Optional.ofNullable(responseBody.get("data"))
+ .filter(Number.class::isInstance)
+ .map(Number.class::cast)
+ .map(Number::doubleValue)
+ .orElse(0.0);
+
+ return Map.of("data", total);
+ }
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ return Map.of("error", "Internal Server Error: 1"); // Fallback en caso de error
+ }
+ });
+
+ }
+
/******************
* PRIVATE METHODS
******************/
@@ -236,6 +287,20 @@ public class skApiClient {
}
}
+ private Map performWithRetryMap(Supplier> request) {
+ try {
+ return request.get();
+ } catch (HttpClientErrorException.Unauthorized e) {
+ // Token expirado, renovar y reintentar
+ authService.invalidateToken();
+ try {
+ return request.get(); // segundo intento
+ } catch (HttpClientErrorException ex) {
+ throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex);
+ }
+ }
+ }
+
private static BigDecimal calcularMargen(
BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax,
BigDecimal margenMax, BigDecimal margenMin) {
diff --git a/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java b/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java
index 79c2db5..079bab3 100644
--- a/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java
+++ b/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java
@@ -1,8 +1,6 @@
package com.imprimelibros.erp.pdf;
import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder;
-import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
-import org.springframework.core.io.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/imprimelibros/erp/pedido/PedidoService.java b/src/main/java/com/imprimelibros/erp/pedido/PedidoService.java
new file mode 100644
index 0000000..5655b27
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/pedido/PedidoService.java
@@ -0,0 +1,26 @@
+package com.imprimelibros.erp.pedido;
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class PedidoService {
+
+ public int getDescuentoFidelizacion() {
+ // descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el ultimo año)
+ double totalGastado = 1600.0; // Ejemplo, deberías obtenerlo del historial del cliente
+ if(totalGastado < 1200) {
+ return 0;
+ } else if(totalGastado >= 1200 && totalGastado < 1999) {
+ return 1;
+ } else if(totalGastado >= 2000 && totalGastado < 2999) {
+ return 2;
+ } else if(totalGastado >= 3000 && totalGastado < 3999) {
+ return 3;
+ } else if(totalGastado >= 4000 && totalGastado < 4999) {
+ return 4;
+ } else if(totalGastado >= 5000) {
+ return 5;
+ }
+ return 0;
+ }
+}
diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java
index f0ed404..73b89a8 100644
--- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java
+++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java
@@ -4,6 +4,7 @@ import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
+import java.security.Principal;
import java.time.Instant;
import java.util.HashMap;
import java.util.Locale;
@@ -621,14 +622,14 @@ public class PresupuestoController {
@ResponseBody
public DataTablesResponse> datatable(
HttpServletRequest request, Authentication auth, Locale locale,
- @PathVariable("tipo") String tipo) {
+ @PathVariable("tipo") String tipo, Principal principal) {
DataTablesRequest dt = DataTablesParser.from(request);
if ("anonimos".equals(tipo)) {
- return dtService.datatablePublicos(dt, locale);
+ return dtService.datatablePublicos(dt, locale, principal);
} else if ("clientes".equals(tipo)) {
- return dtService.datatablePrivados(dt, locale);
+ return dtService.datatablePrivados(dt, locale, principal);
} else {
throw new IllegalArgumentException("Tipo de datatable no válido");
}
diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java
index 0555968..d81756a 100644
--- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java
+++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java
@@ -1,15 +1,18 @@
package com.imprimelibros.erp.presupuesto;
import com.imprimelibros.erp.common.Utils;
+import com.imprimelibros.erp.configuracion.margenes_presupuestos.MargenPresupuesto;
import com.imprimelibros.erp.datatables.*;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import jakarta.persistence.criteria.Expression;
import org.springframework.context.MessageSource;
+import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import java.security.Principal;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
@@ -26,18 +29,29 @@ public class PresupuestoDatatableService {
}
@Transactional(readOnly = true)
- public DataTablesResponse> datatablePublicos(DataTablesRequest dt, Locale locale) {
- return commonDataTable(dt, locale, "publico", true);
+ public DataTablesResponse> datatablePublicos(DataTablesRequest dt, Locale locale,
+ Principal principal) {
+ return commonDataTable(dt, locale, "publico", true, principal);
}
@Transactional(readOnly = true)
- public DataTablesResponse> datatablePrivados(DataTablesRequest dt, Locale locale) {
- return commonDataTable(dt, locale, "privado", false);
+ public DataTablesResponse> datatablePrivados(DataTablesRequest dt, Locale locale,
+ Principal principal) {
+ return commonDataTable(dt, locale, "privado", false, principal);
}
private DataTablesResponse> commonDataTable(DataTablesRequest dt, Locale locale, String origen,
- boolean publico) {
- Long count = repo.findAllByOrigen(Presupuesto.Origen.valueOf(origen)).stream().count();
+ boolean publico, Principal principal) {
+
+ Specification base = Specification.allOf(
+ (root, query, cb) -> cb.equal(root.get("origen"), Presupuesto.Origen.valueOf(origen)));
+
+ Boolean isAdmin = Utils.isCurrentUserAdmin();
+ if (!isAdmin) {
+ base = base.and((root, query, cb) -> cb.equal(root.get("user").get("id"), Utils.currentUserId(principal)));
+ }
+
+ Long count = repo.count(base);
List orderable = List.of(
"id", "titulo", "user.fullName", "tipoEncuadernacion", "tipoCubierta", "tipoImpresion",
@@ -74,6 +88,7 @@ public class PresupuestoDatatableService {
.add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale))
.addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "")
.add("actions", this::generarBotones)
+ .where(base)
.toJson(count);
}
diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java b/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java
index 65942fc..12db785 100644
--- a/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java
+++ b/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java
@@ -920,4 +920,28 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
public void setId(Long 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;
+ }
}
diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java
new file mode 100644
index 0000000..921b224
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java
@@ -0,0 +1,83 @@
+package com.imprimelibros.erp.redsys;
+
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+
+@Controller
+@RequestMapping("/pagos/redsys")
+public class RedsysController {
+
+ private final RedsysService service;
+
+ public RedsysController(RedsysService service) {
+ this.service = service;
+ }
+
+ @PostMapping("/crear")
+ public String crearPago(@RequestParam String order,
+ @RequestParam long amountCents,
+ Model model) throws Exception {
+
+ var req = new RedsysService.PaymentRequest(order, amountCents, "Compra en ImprimeLibros");
+ var form = service.buildRedirectForm(req);
+ model.addAttribute("action", form.action());
+ model.addAttribute("signatureVersion", form.signatureVersion());
+ model.addAttribute("merchantParameters", form.merchantParameters());
+ model.addAttribute("signature", form.signature());
+ return "imprimelibros/payments/redsys-redirect";
+ }
+
+ @PostMapping("/notify")
+ @ResponseBody
+ public ResponseEntity notifyRedsys(
+ @RequestParam("Ds_Signature") String dsSignature,
+ @RequestParam("Ds_MerchantParameters") String dsMerchantParameters) {
+
+ try {
+ RedsysService.RedsysNotification notif = service.validateAndParseNotification(dsSignature,
+ dsMerchantParameters);
+
+ // 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");
+ }
+ }
+
+ @PostMapping("/ok")
+ public String okReturn(@RequestParam("Ds_Signature") String dsSignature,
+ @RequestParam("Ds_MerchantParameters") String dsMerchantParameters,
+ Model model) {
+ try {
+ RedsysService.RedsysNotification notif = service.validateAndParseNotification(dsSignature, dsMerchantParameters);
+ // Aquí puedes validar importe/pedido/moneda con tu base de datos y marcar como
+ // pagado
+ model.addAttribute("authorized", notif.authorized());
+ //model.addAttribute("order", notif.order());
+ //model.addAttribute("amountCents", notif.amountCents());
+ return "imprimelibros/payments/redsys-ok";
+ } catch (Exception e) {
+ model.addAttribute("error", "No se pudo validar la respuesta de Redsys.");
+ return "imprimelibros/payments/redsys-ko";
+ }
+ }
+
+ @PostMapping("/ko")
+ public String koReturn(@RequestParam(value = "Ds_Signature", required = false) String dsSignature,
+ @RequestParam(value = "Ds_MerchantParameters", required = false) String dsMerchantParameters,
+ Model model) {
+ // Suele venir cuando el usuario cancela o hay error
+ model.addAttribute("error", "Operación cancelada o rechazada.");
+ return "imprimelibros/payments/redsys-ko";
+ }
+
+}
diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java
new file mode 100644
index 0000000..df8c484
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java
@@ -0,0 +1,174 @@
+package com.imprimelibros.erp.redsys;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import sis.redsys.api.ApiMacSha256;
+
+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.Map;
+import java.util.Objects;
+
+@Service
+public class RedsysService {
+
+ // ---------- CONFIG ----------
+ @Value("${redsys.merchant-code}")
+ private String merchantCode;
+ @Value("${redsys.terminal}")
+ private String terminal;
+ @Value("${redsys.currency}")
+ private String currency;
+ @Value("${redsys.transaction-type}")
+ private String txType;
+ @Value("${redsys.secret-key}")
+ private String secretKeyBase64;
+ @Value("${redsys.urls.ok}")
+ private String urlOk;
+ @Value("${redsys.urls.ko}")
+ private String urlKo;
+ @Value("${redsys.urls.notify}")
+ private String urlNotify;
+ @Value("${redsys.environment}")
+ private String env;
+
+ // ---------- RECORDS ----------
+ public record PaymentRequest(String order, long amountCents, String description) {
+ }
+
+ public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) {
+ }
+
+ // ---------- MÉTODO PRINCIPAL ----------
+ public FormPayload buildRedirectForm(PaymentRequest req) throws Exception {
+ ApiMacSha256 api = new ApiMacSha256();
+
+ api.setParameter("DS_MERCHANT_AMOUNT", String.valueOf(req.amountCents()));
+ api.setParameter("DS_MERCHANT_ORDER", req.order()); // Usa 12 dígitos con ceros si puedes
+ api.setParameter("DS_MERCHANT_MERCHANTCODE", merchantCode);
+ api.setParameter("DS_MERCHANT_CURRENCY", currency);
+ api.setParameter("DS_MERCHANT_TRANSACTIONTYPE", txType);
+ api.setParameter("DS_MERCHANT_TERMINAL", terminal);
+ api.setParameter("DS_MERCHANT_MERCHANTURL", urlNotify);
+ api.setParameter("DS_MERCHANT_URLOK", urlOk);
+ api.setParameter("DS_MERCHANT_URLKO", urlKo);
+
+ String merchantParameters = api.createMerchantParameters();
+ String signature = api.createMerchantSignature(secretKeyBase64);
+
+ String action = "test".equalsIgnoreCase(env)
+ ? "https://sis-t.redsys.es:25443/sis/realizarPago"
+ : "https://sis.redsys.es/sis/realizarPago";
+
+ return new FormPayload(action, "HMAC_SHA256_V1", merchantParameters, signature);
+ }
+
+ // ---------- STEP 3: Decodificar Ds_MerchantParameters ----------
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ public Map decodeMerchantParametersToMap(String dsMerchantParametersB64) throws Exception {
+ try {
+ byte[] decoded = Base64.getDecoder().decode(dsMerchantParametersB64);
+ String json = new String(decoded, StandardCharsets.UTF_8);
+ return MAPPER.readValue(json, new TypeReference<>() {
+ });
+ } catch (Exception e) {
+ throw new IllegalArgumentException("No se pudo decodificar Ds_MerchantParameters", e);
+ }
+ }
+
+ // ---------- STEP 4: Validar notificación ----------
+ public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64)
+ throws Exception {
+ Map mp = decodeMerchantParametersToMap(dsMerchantParametersB64);
+ RedsysNotification notif = new RedsysNotification(mp);
+
+ if (notif.order == null || notif.order.isBlank()) {
+ throw new IllegalArgumentException("Falta Ds_Order en Ds_MerchantParameters");
+ }
+
+ ApiMacSha256 api = new ApiMacSha256();
+ api.setParameter("Ds_MerchantParameters", dsMerchantParametersB64);
+
+ String expected = api.createMerchantSignatureNotif(secretKeyBase64, api.decodeMerchantParameters(dsMerchantParametersB64)); // ✅ SOLO UN PARÁMETRO
+
+ if (!safeEqualsB64(dsSignature, expected)) {
+ throw new SecurityException("Firma Redsys no válida");
+ }
+
+ return notif;
+}
+
+
+ // ---------- HELPERS ----------
+ private static boolean safeEqualsB64(String a, String b) {
+ if (Objects.equals(a, b))
+ return true;
+ 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 raw;
+ public final String order;
+ public final String response;
+ public final long amountCents;
+ public final String currency;
+
+ public RedsysNotification(Map 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() {
+ try {
+ int r = Integer.parseInt(response);
+ return r >= 0 && r <= 99;
+ } 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;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/imprimelibros/erp/users/UserService.java b/src/main/java/com/imprimelibros/erp/users/UserService.java
index 6856390..228e2cd 100644
--- a/src/main/java/com/imprimelibros/erp/users/UserService.java
+++ b/src/main/java/com/imprimelibros/erp/users/UserService.java
@@ -1,6 +1,9 @@
package com.imprimelibros.erp.users;
import org.springframework.security.core.userdetails.UserDetailsService;
+
+import java.util.Map;
+
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
diff --git a/src/main/java/com/imprimelibros/erp/users/UserServiceImpl.java b/src/main/java/com/imprimelibros/erp/users/UserServiceImpl.java
index 107c88b..76b6843 100644
--- a/src/main/java/com/imprimelibros/erp/users/UserServiceImpl.java
+++ b/src/main/java/com/imprimelibros/erp/users/UserServiceImpl.java
@@ -2,10 +2,21 @@ package com.imprimelibros.erp.users;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+import java.text.Collator;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
+import com.imprimelibros.erp.direcciones.Direccion;
+
@Service
public class UserServiceImpl implements UserService {
@@ -29,5 +40,4 @@ public class UserServiceImpl implements UserService {
if (query == null || query.isBlank()) query = null;
return userDao.searchUsers(role, query, pageable);
}
-
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index d56bf95..d280e5d 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,5 +1,9 @@
spring.application.name=erp
+server.forward-headers-strategy=framework
+server.servlet.session.cookie.secure=true
+
+
#
# Logging
#
@@ -11,8 +15,8 @@ logging.level.org.springframework=ERROR
#
# Database Configuration
#
-spring.datasource.url=jdbc:mysql://localhost:3309/imprimelibros
-#spring.datasource.url=jdbc:mysql://127.0.0.1:3309/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid&characterEncoding=utf8
+#spring.datasource.url=jdbc:mysql://localhost:3309/imprimelibros
+spring.datasource.url=jdbc:mysql://127.0.0.1:3309/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid&characterEncoding=utf8
spring.datasource.username=imprimelibros_user
spring.datasource.password=om91irrDctd
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
@@ -101,3 +105,22 @@ spring.liquibase.change-log=classpath:db/changelog/master.yml
# spring.liquibase.url=jdbc:mysql://localhost:3306/imprimelibros
# spring.liquibase.user=tu_user
# spring.liquibase.password=tu_pass
+
+
+# Redsys
+redsys.environment=test
+redsys.merchant-code=124760810
+redsys.terminal=1
+redsys.currency=978
+redsys.transaction-type=0
+redsys.secret-key=sq7HjrUOBfKmC576ILgskD5srU870gJ7
+redsys.urls.ok=http://localhost:8080/pagos/redsys/ok
+redsys.urls.ko=http://localhost:8080/pagos/redsys/ko
+redsys.urls.notify=http://localhost:8080/pagos/redsys/notify
+
+
+# Mensajes de error mas cortos
+# Oculta el stack trace en los errores del servidor
+server.error.include-stacktrace=never
+# No mostrar el mensaje completo de excepción en la respuesta
+server.error.include-message=always
\ No newline at end of file
diff --git a/src/main/resources/db/changelog/changesets/0005-add-carts-onlyoneshipment.yml b/src/main/resources/db/changelog/changesets/0005-add-carts-onlyoneshipment.yml
new file mode 100644
index 0000000..4dae461
--- /dev/null
+++ b/src/main/resources/db/changelog/changesets/0005-add-carts-onlyoneshipment.yml
@@ -0,0 +1,37 @@
+databaseChangeLog:
+ - changeSet:
+ id: 0005-add-carts-onlyoneshipment
+ author: jjo
+ preConditions:
+ onFail: MARK_RAN
+ not:
+ columnExists:
+ tableName: carts
+ columnName: only_one_shipment
+
+ changes:
+ - addColumn:
+ tableName: carts
+ columns:
+ - column:
+ name: only_one_shipment
+ type: TINYINT(1)
+ defaultValueNumeric: 1
+ remarks: "Si 1, el carrito tiene la misma direccion para todos los items"
+ afterColumn: currency
+ constraints:
+ nullable: false
+
+ - column:
+ name: total
+ type: DECIMAL(19,6)
+ defaultValueNumeric: 0
+ remarks: "Total del carrito"
+ afterColumn: only_one_shipment
+ constraints:
+ nullable: false
+
+ rollback:
+ - dropColumn:
+ tableName: carts
+ columnName: only_one_shipment
diff --git a/src/main/resources/db/changelog/changesets/0006-add-cart-direcciones.yml b/src/main/resources/db/changelog/changesets/0006-add-cart-direcciones.yml
new file mode 100644
index 0000000..41a1177
--- /dev/null
+++ b/src/main/resources/db/changelog/changesets/0006-add-cart-direcciones.yml
@@ -0,0 +1,94 @@
+databaseChangeLog:
+ - changeSet:
+ id: 0006-add-cart-direcciones
+ author: jjo
+ preConditions:
+ onFail: MARK_RAN
+ not:
+ tableExists:
+ tableName: cart_direcciones
+
+ changes:
+ - createTable:
+ tableName: cart_direcciones
+ remarks: "Relación de direcciones y unidades por carrito/direcciones_envio"
+ columns:
+ - column:
+ name: id
+ type: BIGINT UNSIGNED AUTO_INCREMENT
+ constraints:
+ primaryKey: true
+ primaryKeyName: pk_cart_direcciones
+
+ - column:
+ name: cart_id
+ type: BIGINT
+ constraints:
+ nullable: false
+
+ - column:
+ name: direccion_id
+ type: BIGINT
+ constraints:
+ nullable: false
+
+ - column:
+ name: presupuesto_id
+ type: BIGINT
+ constraints:
+ nullable: true
+
+ - column:
+ name: unidades
+ type: INT
+ constraints:
+ nullable: true
+
+ - column:
+ name: is_palets
+ type: TINYINT(1)
+ constraints:
+ nullable: false
+ defaultValue: false
+
+ - createIndex:
+ indexName: idx_cart_dir_direccion_id
+ tableName: cart_direcciones
+ columns:
+ - column:
+ name: direccion_id
+
+ - createIndex:
+ indexName: idx_cart_dir_presupuesto_id
+ tableName: cart_direcciones
+ columns:
+ - column:
+ name: presupuesto_id
+
+ - addForeignKeyConstraint:
+ baseTableName: cart_direcciones
+ baseColumnNames: direccion_id
+ constraintName: fk_cart_dir_direccion
+ referencedTableName: direcciones
+ referencedColumnNames: id
+ onDelete: CASCADE
+ onUpdate: CASCADE
+
+ - addForeignKeyConstraint:
+ baseTableName: cart_direcciones
+ baseColumnNames: presupuesto_id
+ constraintName: fk_cart_dir_presupuesto
+ referencedTableName: presupuesto
+ referencedColumnNames: id
+ onDelete: SET NULL
+ onUpdate: CASCADE
+
+ rollback:
+ - dropForeignKeyConstraint:
+ baseTableName: cart_direcciones
+ constraintName: fk_cart_dir_direccion
+ - dropForeignKeyConstraint:
+ baseTableName: cart_direcciones
+ constraintName: fk_cart_dir_presupuesto
+ - dropTable:
+ tableName: cart_direcciones
diff --git a/src/main/resources/db/changelog/master.yml b/src/main/resources/db/changelog/master.yml
index 90f38f5..95eb18c 100644
--- a/src/main/resources/db/changelog/master.yml
+++ b/src/main/resources/db/changelog/master.yml
@@ -6,4 +6,8 @@ databaseChangeLog:
- include:
file: db/changelog/changesets/0003-create-paises.yml
- include:
- file: db/changelog/changesets/0004-create-direcciones.yml
\ No newline at end of file
+ file: db/changelog/changesets/0004-create-direcciones.yml
+ - include:
+ file: db/changelog/changesets/0005-add-carts-onlyoneshipment.yml
+ - include:
+ file: db/changelog/changesets/0006-add-cart-direcciones.yml
\ No newline at end of file
diff --git a/src/main/resources/i18n/app_es.properties b/src/main/resources/i18n/app_es.properties
index 8e2de87..4ce7940 100644
--- a/src/main/resources/i18n/app_es.properties
+++ b/src/main/resources/i18n/app_es.properties
@@ -3,6 +3,7 @@ app.yes=Sí
app.no=No
app.aceptar=Aceptar
app.cancelar=Cancelar
+app.seleccionar=Seleccionar
app.guardar=Guardar
app.editar=Editar
app.add=Añadir
diff --git a/src/main/resources/i18n/cart_es.properties b/src/main/resources/i18n/cart_es.properties
index 4fff994..f6e9f46 100644
--- a/src/main/resources/i18n/cart_es.properties
+++ b/src/main/resources/i18n/cart_es.properties
@@ -4,11 +4,47 @@ cart.empty=Tu cesta de la compra está vacía.
cart.item.presupuesto-numero=Presupuesto #
cart.precio=Precio
+cart.tabs.details=Detalles
+cart.tabs.envio=Envío
+
+cart.shipping.add=Añadir dirección
+cart.shipping.add.title=Seleccione una dirección
+cart.shipping.select-placeholder=Buscar en direcciones...
+cart.shipping.new-address=Nueva dirección
+cart.shipping.info=Todos los pedidos incluyen un envío gratuito a la Península y Baleares por línea de pedido.
+cart.shipping.order=Envío del pedido
+cart.shipping.samples=Envío de prueba
+cart.shipping.onlyOneShipment=Todo el pedido se envía a una única dirección.
+cart.shipping.tirada=Tirada:
+cart.shipping.unidades=unidades
+cart.shipping.ud=ud.
+cart.shipping.uds=uds.
+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.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.noAddressSelected=Debe seleccionar una dirección de envío para el pedido.
+cart.shipping.errors.fillAddressesItems=Debe seleccionar una dirección de envío para cada artículo de la cesta.
+
cart.resumen.title=Resumen de la cesta
-cart.resumen.base=Base imponible:
-cart.resumen.iva-4=IVA 4%:
-cart.resumen.iva-21=IVA 21%:
-cart.resumen.total=Total cesta:
+cart.resumen.base=Base imponible
+cart.resumen.envio=Coste de envío
+cart.resumen.iva-4=IVA 4%
+cart.resumen.iva-21=IVA 21%
+cart.resumen.descuento=Descuento fidelización
+cart.resumen.total=Total cesta
cart.resumen.tramitar=Tramitar pedido
-cart.resumen.fidelizacion=Si tiene descuento por fidelización, se aplicará al tramitar el pedido.
\ No newline at end of file
+cart.pass-to.customer=Mover cesta a cliente
+cart.pass-to.customer.info=Puede mover la cesta actual al cliente seleccionado. Esto eliminará la cesta del usuario actual y la asociará al cliente seleccionado.
+cart.pass-to.customer.warning=Advertencia: Esta acción no se puede deshacer y sobrescribirá la cesta del cliente seleccionado. Asegúrese de que el cliente seleccionado es correcto.
+cart.pass-to.select-customer=Seleccione un cliente
+cart.pass-to.button=Mover cesta
+cart.pass-to.success=Cesta movida correctamente al cliente {0}.
+cart.pass-to.customer.error=Debe seleccionar un cliente para mover la cesta.
+cart.pass-to.customer.error-move=Error al mover la cesta de la compra
+cart.errors.update-cart=Error al actualizar la cesta de la compra: {0}
+cart.errors.shipping=No se puede calcular el coste del envío para alguna de las direcciones seleccionadas. Por favor, póngase en contacto con el servicio de atención al cliente.
\ No newline at end of file
diff --git a/src/main/resources/i18n/direcciones_es.properties b/src/main/resources/i18n/direcciones_es.properties
index bc30637..6e3ee47 100644
--- a/src/main/resources/i18n/direcciones_es.properties
+++ b/src/main/resources/i18n/direcciones_es.properties
@@ -53,5 +53,7 @@ direcciones.error.delete-internal-error=Error interno al eliminar la dirección.
direcciones.error.noEncontrado=Dirección no encontrada.
direcciones.error.sinPermiso=No tiene permiso para realizar esta acción.
+direcciones.error.noShippingCost=No se pudo calcular el coste de envío para la dirección proporcionada.
+
direcciones.form.error.required=Campo obligatorio.
diff --git a/src/main/resources/i18n/pedidos_en.properties b/src/main/resources/i18n/pedidos_en.properties
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/resources/i18n/pedidos_es.properties b/src/main/resources/i18n/pedidos_es.properties
new file mode 100644
index 0000000..a56dd01
--- /dev/null
+++ b/src/main/resources/i18n/pedidos_es.properties
@@ -0,0 +1,17 @@
+checkout.title=Finalizar compra
+checkout.summay=Resumen de la compra
+checkout.shipping=Envío
+checkout.payment=Método de pago
+
+checkout.shipping.info=Todos los pedidos incluyen un envío gratuito a la Península y Baleares por línea de pedido.
+checkout.shipping.order=Envío del pedido
+checkout.shipping.samples=Envío de pruebas
+checkout.shipping.onlyOneShipment=Todo el pedido se envía a una única dirección.
+
+
+checkout.summary.presupuesto=#Presupuesto
+checkout.summary.titulo=Título
+checkout.summary.base=Base
+checkout.summary.iva-4=IVA 4%
+checkout.summary.iva-21=IVA 21%
+checkout.summary.envio=Envío
\ No newline at end of file
diff --git a/src/main/resources/lib/apiSha256.jar b/src/main/resources/lib/apiSha256.jar
new file mode 100644
index 0000000..83fbf6c
Binary files /dev/null and b/src/main/resources/lib/apiSha256.jar differ
diff --git a/src/main/resources/lib/bcprov-jdk15on-1.4.7.jar b/src/main/resources/lib/bcprov-jdk15on-1.4.7.jar
new file mode 100644
index 0000000..0b80922
Binary files /dev/null and b/src/main/resources/lib/bcprov-jdk15on-1.4.7.jar differ
diff --git a/src/main/resources/lib/commons-codec-1.3.jar b/src/main/resources/lib/commons-codec-1.3.jar
new file mode 100644
index 0000000..957b675
Binary files /dev/null and b/src/main/resources/lib/commons-codec-1.3.jar differ
diff --git a/src/main/resources/lib/org.json.jar b/src/main/resources/lib/org.json.jar
new file mode 100644
index 0000000..5372cb0
Binary files /dev/null and b/src/main/resources/lib/org.json.jar differ
diff --git a/src/main/resources/static/assets/css/cart.css b/src/main/resources/static/assets/css/cart.css
new file mode 100644
index 0000000..f927b98
--- /dev/null
+++ b/src/main/resources/static/assets/css/cart.css
@@ -0,0 +1,38 @@
+.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
+ background-color: #4c5c6366 !important;
+}
+
+.step-arrow-nav .nav {
+ background-color: #4c5c6333 !important;
+}
+
+.nav-link.active .bg-soft-primary {
+ background-color: #ffffff33 !important;
+ /* #4c5c63 al 20% */
+}
+
+.nav-link.active .text-primary {
+ color: #ffffff !important;
+ /* #4c5c63 al 20% */
+}
+
+.nav-link:not(.active) .bg-soft-primary {
+ background-color: #4c5c6366 !important;
+ /* #4c5c63 al 20% */
+}
+
+.nav-link:not(.active) .text-primary {
+ color: #000000 !important;
+ /* #4c5c63 al 20% */
+}
+
+.direccion-card {
+ flex: 1 1 350px; /* ancho mínimo 350px, crece si hay espacio */
+ max-width: 350px; /* opcional, para que no se estiren demasiado */
+ min-width: 340px; /* protege el ancho mínimo */
+}
+
+.shipping-addresses-item { align-items: stretch; justify-content: center;}
+.shipping-addresses-sample { align-items: stretch; justify-content: center;}
+.direccion-card { display: flex; flex-direction: column; }
+.direccion-card .card-body { display: flex; flex-direction: column; }
\ No newline at end of file
diff --git a/src/main/resources/static/assets/css/imprimelibros.css b/src/main/resources/static/assets/css/imprimelibros.css
index aafd10a..b426ded 100644
--- a/src/main/resources/static/assets/css/imprimelibros.css
+++ b/src/main/resources/static/assets/css/imprimelibros.css
@@ -29,3 +29,13 @@ body {
color: #fff;
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;
+}
diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/cart/cart.js b/src/main/resources/static/assets/js/pages/imprimelibros/cart/cart.js
index 707a93a..aae1d3a 100644
--- a/src/main/resources/static/assets/js/pages/imprimelibros/cart/cart.js
+++ b/src/main/resources/static/assets/js/pages/imprimelibros/cart/cart.js
@@ -1,44 +1,111 @@
import { formateaMoneda } from '../../imprimelibros/utils.js';
+import { showLoader, hideLoader } from '../loader.js';
$(() => {
- updateTotal();
+ $(document).ajaxStart(showLoader).ajaxStop(hideLoader);
- function updateTotal() {
- const items = $(".product");
- let iva4 = 0;
- let iva21 = 0;
- let base = 0;
- for (let i = 0; i < items.length; i++) {
- const item = $(items[i]);
- const b = item.data("base");
- const i4 = item.data("iva-4");
- const i21 = item.data("iva-21");
- base += parseFloat(b) || 0;
- iva4 += parseFloat(i4) || 0;
- iva21 += parseFloat(i21) || 0;
+ $(document).on('updateCart', () => {
+ // get form and submit
+ const form = $('#cartForm');
+ const container = $("#onlyOneShipment").is(':checked') ? $('#shippingAddressesContainer') : $('.product');
+ // remove name from container . direccion-card
+ container.find('.direccion-card input[type="hidden"]').removeAttr('name');
+
+ container.find('.direccion-card').each(function (i) {
+ $(this).find('.direccion-id').attr('name', 'direcciones[' + i + '].id');
+ $(this).find('.direccion-cp').attr('name', 'direcciones[' + i + '].cp');
+ $(this).find('.direccion-pais-code3').attr('name', 'direcciones[' + i + '].paisCode3');
+ if ($(this).find('.presupuesto-id').length > 0 && $(this).find('.presupuesto-id').val() !== null
+ && $(this).find('.presupuesto-id').val() !== "")
+ $(this).find('.presupuesto-id').attr('name', 'direcciones[' + i + '].presupuestoId');
+ if ($(this).find('.item-tirada').length > 0 && $(this).find('.item-tirada').val() !== null
+ && $(this).find('.item-tirada').val() !== "")
+ $(this).find('.item-tirada').attr('name', 'direcciones[' + i + '].unidades');
+ });
+ $.post(form.attr('action'), form.serialize(), (response) => {
+ // if success and received html, replace container summary
+ if (response) {
+ $('.cart-summary-container').replaceWith(response);
+ }
+ }).always(() => {
+ hideLoader();
+ });
+
+ checkAddressesForItems();
+ });
+
+ checkAddressesForItems();
+
+ function checkAddressesForItems() {
+ if ($('.product').length === 0) {
+ $("#alert-empty").removeClass("d-none");
+ $('.cart-content').addClass('d-none');
+ return;
}
- $("#base-cesta").text(formateaMoneda(base));
- if (iva4 > 0) {
- $("#iva-4-cesta").text(formateaMoneda(iva4));
- $("#tr-iva-4").show();
- } else {
- $("#tr-iva-4").hide();
+ else {
+ $('.cart-content').removeClass('d-none');
+ $("#alert-empty").addClass("d-none");
+ // check if select2 is initialized
+ if ($('#select-customer').length && !$('#select-customer').hasClass('select2-hidden-accessible')) {
+ initMoveCartToCustomer();
+ }
}
- if (iva21 > 0) {
- $("#iva-21-cesta").text(formateaMoneda(iva21));
- $("#tr-iva-21").show();
- } else {
- $("#tr-iva-21").hide();
+ if ($('#onlyOneShipment').is(':checked')) {
+ if ($("#shippingAddressesContainer .direccion-card").length === 0) {
+ $(".alert-shipment").removeClass("d-none");
+ $('#btn-checkout').prop('disabled', true);
+ return;
+ }
+ $(".alert-shipment").addClass("d-none");
+ $('#btn-checkout').prop('disabled', false);
+ }
+ else {
+ const items = $(".product");
+ let errorFound = false;
+ for (let i = 0; i < items.length; i++) {
+ let errorFoundItem = false;
+ const item = $(items[i]);
+ const tirada = parseInt(item.find(".item-tirada").val()) || 0;
+ const direcciones = item.find(".direccion-card");
+ let totalUnidades = 0;
+ direcciones.each(function () {
+ const unidades = parseInt($(this).find(".item-tirada").val()) || 0;
+ totalUnidades += unidades;
+ });
+ if (totalUnidades < tirada) {
+ errorFoundItem = true;
+ }
+
+ if (item.find(".shipping-addresses-sample")) {
+ const container = item.find(".shipping-addresses-sample");
+ if (container.find('.direccion-card').toArray().length === 0) {
+ errorFoundItem = true;
+ }
+ }
+ if (errorFoundItem) {
+ errorFound = true;
+ item.find(".alert-icon-shipment").removeClass("d-none");
+ }
+ else {
+ item.find(".alert-icon-shipment").addClass("d-none");
+ }
+ }
+ if (errorFound) {
+ $(".alert-shipment").removeClass("d-none");
+ $('#btn-checkout').prop('disabled', true);
+ }
+ else {
+ $(".alert-shipment").addClass("d-none");
+ $('#btn-checkout').prop('disabled', false);
+ }
}
- const total = base + iva4 + iva21;
- $("#total-cesta").text(formateaMoneda(total));
}
$(document).on("click", ".delete-item", async function (event) {
event.preventDefault();
- const cartItemId = $(this).data("cart-item-id");
+ const presupuestoId = $(this).data("cart-item-id");
const card = $(this).closest('.card.product');
// CSRF (Spring Security)
@@ -46,7 +113,7 @@ $(() => {
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.content || 'X-CSRF-TOKEN';
try {
- const res = await fetch(`/cart/delete/item/${cartItemId}`, {
+ const res = await fetch(`/cart/delete/item/${presupuestoId}`, {
method: 'DELETE',
headers: { [csrfHeader]: csrfToken }
});
@@ -55,13 +122,81 @@ $(() => {
console.error('Error al eliminar. Status:', res.status);
return;
}
- else{
+ else {
card?.remove();
- updateTotal();
+ $(document).trigger('updateCart');
}
} catch (err) {
console.error('Error en la solicitud:', err);
}
});
+
+ function initMoveCartToCustomer() {
+ if ($('#select-customer').length) {
+
+ $('#moveCart').on('click', async function (e) {
+ e.preventDefault();
+ const customerId = $('#select-customer').val();
+ if (!customerId) {
+ // set text and show alert
+ $('#alert-select-customer').text(window.languageBundle['cart.pass-to.customer.error'] || 'Debe seleccionar un cliente para mover la cesta.');
+ $('#alert-select-customer').removeClass('d-none').hide().fadeIn();
+ setTimeout(() => {
+ $('#alert-select-customer').fadeOut(function () {
+ $(this).addClass('d-none');
+ });
+ }, 5000);
+ return;
+ }
+
+ // CSRF (Spring Security)
+ const csrfToken = document.querySelector('meta[name="_csrf"]')?.content || '';
+ const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.content || 'X-CSRF-TOKEN';
+
+ try {
+ const res = await fetch(`/cart/pass-to-customer/${customerId}`, {
+ method: 'POST',
+ headers: { [csrfHeader]: csrfToken }
+ });
+
+ if (!res.ok) {
+ $('#alert-select-customer').text(window.languageBundle['cart.pass-to.customer.move.error'] || 'Error al mover la cesta de la compra');
+ $('#alert-select-customer').removeClass('d-none').hide().fadeIn();
+ setTimeout(() => {
+ $('#alert-select-customer').fadeOut(function () {
+ $(this).addClass('d-none');
+ });
+ }, 5000);
+ return;
+ }
+ else {
+ window.location.href = '/cart';
+ }
+
+ } catch (err) {
+ console.error('Error en la solicitud:', err);
+ $('#alert-select-customer').text(window.languageBundle['cart.errors.move-cart'] || 'Error al mover la cesta de la compra');
+ $('#alert-select-customer').removeClass('d-none').hide().fadeIn();
+ setTimeout(() => {
+ $('#alert-select-customer').fadeOut(function () {
+ $(this).addClass('d-none');
+ });
+ }, 5000);
+ }
+ });
+
+ $('#select-customer').select2({
+ width: '100%',
+ ajax: {
+ url: 'users/api/get-users',
+ dataType: 'json',
+ delay: 250,
+ },
+ allowClear: true
+ });
+ }
+ }
+ initMoveCartToCustomer();
+
});
\ No newline at end of file
diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js b/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js
new file mode 100644
index 0000000..ab7edfe
--- /dev/null
+++ b/src/main/resources/static/assets/js/pages/imprimelibros/cart/shipping-cart.js
@@ -0,0 +1,443 @@
+import { showLoader, hideLoader } from '../loader.js';
+
+$(() => {
+
+ // Si usas jQuery AJAX:
+ $(document).ajaxStart(showLoader).ajaxStop(hideLoader);
+
+ $("#onlyOneShipment").on('change', function () {
+ if ($(this).is(':checked')) {
+ $('.nav-product').addClass('d-none');
+ document.querySelectorAll('.card.product').forEach(card => {
+ const detailsBtn = card.querySelector('.nav-link[id^="pills-details-"][id$="-tab"]');
+ if (detailsBtn) new bootstrap.Tab(detailsBtn).show();
+ });
+ $('#shippingAddressesContainer').empty().removeClass('d-none');
+ $('.shipping-addresses-item').toArray().forEach(element => {
+ $(element).empty().addClass('d-none');
+ });
+ $('#addOrderAddress').removeClass('d-none');
+ } else {
+ $('.nav-product').removeClass('d-none');
+ $('#shippingAddressesContainer').empty().addClass('d-none');
+ $('.shipping-addresses-item').toArray().forEach(element => {
+ $(element).empty().removeClass('d-none');
+ });
+ $('#addOrderAddress').addClass('d-none');
+ }
+ $(document).trigger('updateCart');
+ });
+
+ $(document).on('click', '.btn-delete-direccion', function (e) {
+ e.preventDefault();
+ const $card = $(this).closest('.direccion-card');
+ const $div = $card.parent();
+ $card.remove();
+ if ($div.hasClass('shipping-order-address')) {
+ $('#addOrderAddress').removeClass('d-none');
+ }
+ else {
+ $div.trigger('direcciones:actualizadas');
+ }
+ $(document).trigger('updateCart');
+ });
+
+ //btn-edit-direccion
+ $(document).on('click', '.btn-edit-direccion', function (e) {
+ e.preventDefault();
+ const $card = $(this).closest('.direccion-card');
+ const container = $(this).closest('.product').find('.shipping-addresses-item');
+ 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 remainingTirada = parseInt(tirada) - parseInt(totalTirada) + parseInt($card.find('.item-tirada').val() || 0);
+ const data = getUnitsFromUser(remainingTirada);
+ data.then(data => {
+ if (data.unidades) {
+ $card.find('.item-tirada').val(data.unidades);
+ $card.find('#units-text').each(function () {
+ if (data.unidades == 1) {
+ $(this).text(`${data.unidades} ${window.languageBundle['cart.shipping.ud'] || 'unidad'}`);
+ } else {
+ $(this).text(`${data.unidades} ${window.languageBundle['cart.shipping.uds'] || 'unidades'}`);
+ }
+ });
+ container.trigger('direcciones:actualizadas');
+ $(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');
+ }
+ });
+ }
+ });
+
+ });
+
+ const language = document.documentElement.lang || 'es-ES';
+ const modalEl = document.getElementById('direccionFormModal');
+ const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
+
+ $(document).on("change", ".direccionFacturacion", function () {
+ const isChecked = $(this).is(':checked');
+ if (isChecked) {
+ $('.direccionFacturacionItems').removeClass('d-none');
+ } else {
+ $('.direccionFacturacionItems').addClass('d-none');
+ $('#razonSocial').val('');
+ $('#tipoIdentificacionFiscal').val('DNI');
+ $('#identificacionFiscal').val('');
+ }
+ });
+
+ $('#addOrderAddress').on('click', async () => {
+ if ($('#onlyOneShipment').is(':checked')) {
+ if (await seleccionarDireccionEnvio()) {
+ $('#addOrderAddress').addClass('d-none');
+ }
+ }
+ });
+
+ $(document).on('click', '.btn-add-shipping', function () {
+ const presupuestoId = $(this).closest('.product').find('.item-presupuesto-id').val();
+ const container = $(this).closest('.product').find('.shipping-addresses-item');
+ 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 remainingTirada = parseInt(tirada) - parseInt(totalTirada);
+ seleccionarDireccionEnvio(presupuestoId, remainingTirada, container);
+ });
+
+ $(document).on('click', '.btn-add-shipping-sample', function () {
+ const presupuestoId = $(this).closest('.product').find('.item-presupuesto-id').val();
+ const container = $(this).closest('.product').find('.shipping-addresses-sample');
+ seleccionarDireccionEnvio(presupuestoId, null, container);
+ });
+
+
+
+ async function seleccionarDireccionEnvio(presupuestoId = null, tirada = null, container = null) {
+
+ const { value: direccionId, isDenied } = await Swal.fire({
+ title: window.languageBundle['cart.shipping.add.title'] || 'Seleccione una dirección',
+ html: `
+
+ `,
+ showCancelButton: true,
+ showDenyButton: true,
+ buttonsStyling: false,
+ confirmButtonText: window.languageBundle['app.seleccionar'] || 'Seleccionar',
+ cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar',
+ denyButtonText: window.languageBundle['cart.shipping.new-address'] || 'Nueva dirección',
+ customClass: {
+ confirmButton: 'btn btn-secondary me-2',
+ cancelButton: 'btn btn-light',
+ denyButton: 'btn btn-secondary me-2'
+ },
+ focusConfirm: false,
+
+ // Inicializa cuando el DOM del modal ya existe
+ didOpen: () => {
+ const $select = $('#direccionSelect');
+ $select.empty(); // limpia placeholder estático
+
+ $select.select2({
+ width: '100%',
+ dropdownParent: $('.swal2-container'),
+ ajax: {
+ url: '/direcciones/select2',
+ dataType: 'json',
+ delay: 250,
+ data: params => ({ q: params.term || '' }),
+ processResults: (data) => {
+ const items = Array.isArray(data) ? data : (data.results || []);
+ return {
+ results: items.map(item => ({
+ id: item.id,
+ text: item.text, // ← Select2 necesita 'id' y 'text'
+ alias: item.alias || 'Sin alias',
+ att: item.att || '',
+ direccion: item.direccion || '',
+ cp: item.cp || '',
+ ciudad: item.ciudad || '',
+ html: `
+
+ ${item.alias || 'Sin alias'}
+ ${item.att ? `${item.att} ` : ''}
+ ${item.direccion || ''}${item.cp ? ', ' + item.cp : ''}${item.ciudad ? ', ' + item.ciudad : ''}
+
+ `
+ })),
+ pagination: { more: false } // opcional, evita que espere más páginas
+ };
+ }
+
+ },
+ placeholder: window.languageBundle['cart.shipping.select-placeholder'] || 'Buscar en direcciones...',
+ language: language,
+
+ templateResult: data => {
+ if (data.loading) return data.text;
+ return $(data.html || data.text);
+ },
+ // Selección más compacta (solo alias + ciudad)
+ templateSelection: data => {
+ if (!data.id) return data.text;
+ const alias = data.alias || data.text;
+ const ciudad = data.ciudad ? ` — ${data.ciudad}` : '';
+ return $(`${alias}${ciudad} `);
+ },
+ escapeMarkup: m => m
+ });
+ },
+
+ preConfirm: () => {
+ const $select = $('#direccionSelect');
+ const val = $select.val();
+ if (!val) {
+ Swal.showValidationMessage(
+ window.languageBundle['cart.shipping.errors.noAddressSelected'] || 'Por favor, seleccione una dirección.'
+ );
+ return false;
+ }
+ return val;
+ },
+
+ didClose: () => {
+ // Limpieza: destruir select2 para evitar fugas
+ const $select = $('#direccionSelect');
+ if ($select.data('select2')) {
+ $select.select2('destroy');
+ }
+ }
+ });
+
+ if (isDenied) {
+ $.get('/direcciones/direction-form', function (html) {
+ $('#direccionFormModalBody').html(html);
+ const title = $('#direccionFormModalBody #direccionForm').data('add');
+ $('#direccionFormModal .modal-title').text(title);
+ modal.removeClass('d-none');
+ });
+ }
+
+ let unidades = null;
+ let isPalets = 0;
+ if (tirada !== null && tirada >= 1 && direccionId) {
+
+ const data = await getUnitsFromUser(tirada);
+ if (data && data.unidades) {
+ 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 && direccionId) { // caso para todas los envios a la misma direccion
+ const isPaletsValue = await getTipoEnvio();
+ if (isPaletsValue !== null) {
+ isPalets = isPaletsValue ? 1 : 0;
+ } else {
+ // Si el usuario cancela, salir de la función
+ return false;
+ }
+ }
+ if (direccionId) {
+ // Obtén el objeto completo seleccionado
+ showLoader();
+ let uri = `/cart/get-address/${direccionId}?isPalets=${isPalets}`;
+ if (presupuestoId !== null) {
+ uri += `&presupuestoId=${presupuestoId}`;
+ if (tirada !== null) {
+ uri += `&unidades=${unidades}`;
+ }
+ }
+ const response = await fetch(uri);
+ if (response.ok) {
+ const html = await response.text();
+ if (presupuestoId !== null) {
+ container.append(html).trigger('direcciones:actualizadas');
+ }
+ else {
+ $('#shippingAddressesContainer').append(html);
+ }
+ $(document).trigger('updateCart');
+ return true;
+ }
+ hideLoader();
+ return false;
+ }
+ hideLoader();
+ return false;
+ }
+
+ async function getUnitsFromUser(tirada) {
+ const { value: formValues } = await Swal.fire({
+ title: window.languageBundle['cart.shipping.enter-units'] || 'Introduzca el número de unidades para esta dirección',
+ html: `
+
+
+ ${window.languageBundle['cart.shipping.units-label']?.replace('{max}', tirada) || `Número de unidades (máximo ${tirada})`}
+
+
+
+
+
+
+ ${window.languageBundle['cart.shipping.send-in-palets'] || 'Enviar en palets'}
+
+
+
+ ${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'}
+
+ `,
+ 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 unidades = parseInt(document.getElementById('swal-input-unidades').value, 10);
+ 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 { unidades, isPalets };
+ }
+ });
+
+ if (formValues) {
+ return formValues; // { unidades: number, isPalets: boolean }
+ }
+ return null; // Si se cancela el Swal
+ }
+
+ async function getTipoEnvio() {
+ const { isConfirmed, value } = await Swal.fire({
+ title: window.languageBundle['cart.shipping.tipo-envio'] || 'Tipo de envío',
+ html: `
+
+
+
+ ${window.languageBundle['cart.shipping.send-in-palets'] || 'Enviar en palets'}
+
+
+
+ ${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'}
+
+ `,
+ 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 popup = Swal.getPopup();
+ const chk = popup.querySelector('#swal-input-palets');
+ // Devuelve un OBJETO (siempre truthy) con el booleano dentro
+ return { isPalets: !!chk?.checked };
+ }
+ });
+
+ if (!isConfirmed) return null; // cancelado
+ return value.isPalets; // true / false
+ }
+
+
+ function checkTotalUnits(container, tirada) {
+
+ const totalUnits = container.find('.direccion-card').toArray().reduce((acc, el) => {
+ const unidades = parseInt($(el).find('.item-tirada').val()) || 0;
+ return acc + unidades;
+ }, 0);
+ if (totalUnits < tirada) {
+ return false;
+ }
+ if (container.find('.product').closest('.shipping-addresses-sample')) {
+ if (container.find('.direccion-card').toArray().length === 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ $(document).on('direcciones:actualizadas', '.shipping-addresses-item', function (e) {
+
+ const tirada = $(this).closest('.product').find('.item-tirada').val();
+ const container = $(this);
+
+ if (!checkTotalUnits(container, tirada)) {
+ container.closest('.px-2').find('.btn-add-shipping').removeClass('d-none');
+ } else {
+ container.closest('.px-2').find('.btn-add-shipping').addClass('d-none');
+ }
+ });
+
+ $(document).on('direcciones:actualizadas', '.shipping-addresses-sample', function (e) {
+
+ const container = $(this);
+
+ if (container.find('.direccion-card').toArray().length === 0) {
+ container.closest('.px-2').find('.btn-add-shipping-sample').removeClass('d-none');
+ }
+ else {
+ container.closest('.px-2').find('.btn-add-shipping-sample').addClass('d-none');
+ }
+ });
+
+ $(document).on('submit', '#direccionForm', function (e) {
+ e.preventDefault();
+ const $form = $(this);
+
+ $.ajax({
+ url: $form.attr('action'),
+ type: 'POST', // PUT simulado via _method
+ data: $form.serialize(),
+ dataType: 'html',
+ success: function (html) {
+ // Si por cualquier motivo llega 200 con fragmento, lo insertamos igual
+ if (typeof html === 'string' && html.indexOf('id="direccionForm"') !== -1 && html.indexOf(' 0;
+ const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add');
+ $('#direccionModal .modal-title').text(title);
+ return;
+ }
+ // Éxito real: cerrar y recargar tabla
+ modal.addClass('d-none');
+ seleccionarDireccionEnvio();
+ },
+ error: function (xhr) {
+ // Con 422 devolvemos el fragmento con errores aquí
+ if (xhr.status === 422 && xhr.responseText) {
+ $('#direccionFormModalBody').html(xhr.responseText);
+ const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 0;
+ const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add');
+ $('#direccionModal .modal-title').text(title);
+ initSelect2Cliente(true);
+ return;
+ }
+ // Fallback
+ $('#direccionFormModalBody').html('Error inesperado.
');
+ }
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js b/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js
new file mode 100644
index 0000000..3aa8e86
--- /dev/null
+++ b/src/main/resources/static/assets/js/pages/imprimelibros/checkout/checkout.js
@@ -0,0 +1,21 @@
+$(() => {
+
+ const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
+ const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
+ if (window.$ && csrfToken && csrfHeader) {
+ $.ajaxSetup({
+ beforeSend: function (xhr) {
+ xhr.setRequestHeader(csrfHeader, csrfToken);
+ }
+ });
+ }
+
+ const language = document.documentElement.lang || 'es-ES';
+ const modalEl = document.getElementById('direccionFormModal');
+ const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
+
+
+
+
+
+});
diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/loader.js b/src/main/resources/static/assets/js/pages/imprimelibros/loader.js
new file mode 100644
index 0000000..11152b3
--- /dev/null
+++ b/src/main/resources/static/assets/js/pages/imprimelibros/loader.js
@@ -0,0 +1,11 @@
+export function showLoader() {
+ const el = document.getElementById('sectionLoader');
+ el.classList.remove('d-none');
+ el.classList.add('d-flex');
+}
+
+export function hideLoader() {
+ const el = document.getElementById('sectionLoader');
+ el.classList.remove('d-flex');
+ el.classList.add('d-none');
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/imprimelibros/cart/_cartContent.html b/src/main/resources/templates/imprimelibros/cart/_cartContent.html
new file mode 100644
index 0000000..378648c
--- /dev/null
+++ b/src/main/resources/templates/imprimelibros/cart/_cartContent.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/imprimelibros/cart/_cartItem.html b/src/main/resources/templates/imprimelibros/cart/_cartItem.html
index 5ee3a2c..01de3dc 100644
--- a/src/main/resources/templates/imprimelibros/cart/_cartItem.html
+++ b/src/main/resources/templates/imprimelibros/cart/_cartItem.html
@@ -1,71 +1,192 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Detalles
+
+
+
+
+
+ Envío
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Presupuesto #
+ #
+
+
+
+
+
+
+ Servicios adicionales:
+
+
+
+
+
+
+ Datos de maquetación:
+
+
+
+
+
+
+ Datos de marcapáginas:
+
+
+
+
+
+
+
+
-
-
-
-
-
- Presupuesto #
- #
-
+
+
-
-
-
-
-
-
+
+
+
+
+ Presupuesto #
+ #
+
+
+
+
+
+
+
-
-
- Servicios adicionales
-
-
+
+
-
-
- Datos de maquetación:
-
-
-
+
+
+
Añadir dirección
-
-
- Datos de marcapáginas:
-
-
-
+
+
+
+
+
+
+
@@ -77,7 +198,7 @@
diff --git a/src/main/resources/templates/imprimelibros/cart/_cartSummary.html b/src/main/resources/templates/imprimelibros/cart/_cartSummary.html
new file mode 100644
index 0000000..516e328
--- /dev/null
+++ b/src/main/resources/templates/imprimelibros/cart/_cartSummary.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ :
+
+
+
+ :
+
+
+
+
+ :
+
+
+
+ :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mover
+ cesta
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/imprimelibros/cart/cart.html b/src/main/resources/templates/imprimelibros/cart/cart.html
index d54ce93..7498643 100644
--- a/src/main/resources/templates/imprimelibros/cart/cart.html
+++ b/src/main/resources/templates/imprimelibros/cart/cart.html
@@ -9,7 +9,7 @@
-
+
@@ -22,6 +22,10 @@
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- :
-
-
-
- :
-
-
-
- :
-
-
-
-
-
-
-
-
-
Checkout
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -102,6 +51,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Finalizar
+ compra
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Envío
+
+
+
+
+
+ Método de
+ pago
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Presupuesto
+ Título
+
+ Base
+
+
+
+
+
+
+ PRESUPUESTO-001
+
+
+ Título del presupuesto
+
+
+
+ 0,00
+
+
+
+
+
+
+
+
+
+
+
+
+ :
+
+
+
+ :
+
+
+
+ :
+
+
+
+
+
+
+
+
Checkout
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/imprimelibros/checkout/_envio.html b/src/main/resources/templates/imprimelibros/checkout/_envio.html
new file mode 100644
index 0000000..893721c
--- /dev/null
+++ b/src/main/resources/templates/imprimelibros/checkout/_envio.html
@@ -0,0 +1,43 @@
+
\ No newline at end of file
diff --git a/src/main/resources/templates/imprimelibros/checkout/_pago.html b/src/main/resources/templates/imprimelibros/checkout/_pago.html
new file mode 100644
index 0000000..42eb8c5
--- /dev/null
+++ b/src/main/resources/templates/imprimelibros/checkout/_pago.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/main/resources/templates/imprimelibros/checkout/checkout.html b/src/main/resources/templates/imprimelibros/checkout/checkout.html
new file mode 100644
index 0000000..5763197
--- /dev/null
+++ b/src/main/resources/templates/imprimelibros/checkout/checkout.html
@@ -0,0 +1,175 @@
+
+
+
+