package com.imprimelibros.erp.cart; import org.springframework.transaction.annotation.Transactional; import org.springframework.context.MessageSource; import org.springframework.stereotype.Service; import java.util.ArrayList; 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.presupuesto.service.PresupuestoService; import com.imprimelibros.erp.cart.dto.CartDireccionRepository; 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.Direccion; 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 CartDireccionRepository cartDireccionRepo; private final CartItemRepository itemRepo; private final MessageSource messageSource; private final PresupuestoRepository presupuestoRepo; private final Utils utils; private final DireccionService direccionService; private final skApiClient skApiClient; private final PedidoService pedidoService; private final PresupuestoService presupuestoService; public CartService(CartRepository cartRepo, CartItemRepository itemRepo, CartDireccionRepository cartDireccionRepo, MessageSource messageSource, PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo, Utils utils, DireccionService direccionService, skApiClient skApiClient, PedidoService pedidoService, PresupuestoService presupuestoService) { this.cartRepo = cartRepo; this.itemRepo = itemRepo; this.cartDireccionRepo = cartDireccionRepo; this.messageSource = messageSource; this.presupuestoRepo = presupuestoRepo; this.utils = utils; this.direccionService = direccionService; this.skApiClient = skApiClient; this.pedidoService = pedidoService; this.presupuestoService = presupuestoService; } public Cart findById(Long cartId) { return cartRepo.findById(cartId) .orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado")); } /** Devuelve el carrito activo o lo crea si no existe. */ @Transactional public Cart getOrCreateActiveCart(Long userId) { return cartRepo.findByUserIdAndStatus(userId, Cart.Status.ACTIVE) .orElseGet(() -> { Cart c = new Cart(); c.setUserId(userId); c.setStatus(Cart.Status.ACTIVE); return cartRepo.save(c); }); } 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) { Cart cart = getOrCreateActiveCart(userId); List> resultados = new ArrayList<>(); List items = itemRepo.findByCartId(cart.getId()); for (CartItem item : items) { Presupuesto p = item.getPresupuesto(); Map elemento = getElementoCart(p, locale); elemento.put("cartItemId", item.getId()); resultados.add(elemento); } // System.out.println("Cart items: " + resultados); return resultados; } /** Añade un presupuesto al carrito. Si ya está, no hace nada (idempotente). */ @Transactional public void addPresupuesto(Long userId, Long presupuestoId) { Cart cart = getOrCreateActiveCart(userId); boolean exists = itemRepo.existsByCartIdAndPresupuestoId(cart.getId(), presupuestoId); if (!exists) { CartItem ci = new CartItem(); ci.setCart(cart); ci.setPresupuesto(presupuestoRepo.findById(presupuestoId) .orElseThrow(() -> new IllegalArgumentException("Presupuesto no encontrado"))); itemRepo.save(ci); } } /** Elimina una línea del carrito por ID de item (validando pertenencia). */ @Transactional public void removeItem(Long userId, Long itemId) { Cart cart = getOrCreateActiveCart(userId); CartItem item = itemRepo.findById(itemId).orElseThrow(() -> new IllegalArgumentException("Item no existe")); if (!item.getCart().getId().equals(cart.getId())) throw new IllegalStateException("El item no pertenece a tu carrito"); itemRepo.delete(item); } /** Elimina una línea del carrito buscando por presupuesto_id. */ @Transactional public void removeByPresupuesto(Long userId, Long presupuestoId) { Cart cart = getOrCreateActiveCart(userId); CartItem item = itemRepo.findByCartIdAndPresupuestoId(cart.getId(), presupuestoId) .orElseThrow(() -> new IllegalArgumentException("Item no encontrado")); itemRepo.deleteById(item.getId()); } /** Vacía todo el carrito activo. */ @Transactional public void clear(Long userId) { Cart cart = getOrCreateActiveCart(userId); itemRepo.deleteByCartId(cart.getId()); } /** Marca el carrito como bloqueado (por ejemplo, antes de crear un pedido). */ @Transactional public void lockCart(Long userId) { Cart cart = getOrCreateActiveCart(userId); cart.setStatus(Cart.Status.LOCKED); cartRepo.save(cart); } @Transactional public void lockCartById(Long cartId) { Cart cart = cartRepo.findById(cartId) .orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado")); cart.setStatus(Cart.Status.LOCKED); cartRepo.save(cart); } @Transactional public long countItems(Long userId) { Cart cart = getOrCreateActiveCart(userId); return itemRepo.findByCartId(cart.getId()).size(); } private Map getElementoCart(Presupuesto presupuesto, Locale locale) { Map resumen = new HashMap<>(); resumen.put("titulo", presupuesto.getTitulo()); resumen.put("imagen", "/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + ".png"); resumen.put("imagen_alt", messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, locale)); 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()); resumen.put("iva21", presupuesto.getIvaImporte21()); resumen.put("resumen", detalles); 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("amountCents", Math.round(total * 100)); summary.put("errorShipmentCost", errorShipementCost); summary.put("cartId", cart.getId()); 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; } } // delete cart directions by direccion id in ACTIVE carts @Transactional public void deleteCartDireccionesByDireccionId(Long direccionId) { cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE); } @Transactional public Long crearPedido(Long cartId) { Cart cart = this.getCartById(cartId); List items = cart.getItems(); List> presupuestoRequests = new ArrayList<>(); for (Integer i = 0; i < items.size(); i++) { CartItem item = items.get(i); Presupuesto p = item.getPresupuesto(); Map data_to_send = presupuestoService.toSkApiRequest(p, true); data_to_send.put("createPedido", 0); if (items.size() > 1) { // Recuperar el mapa anidado datosCabecera @SuppressWarnings("unchecked") Map datosCabecera = (Map) data_to_send.get("datosCabecera"); if (datosCabecera != null) { Object tituloOriginal = datosCabecera.get("titulo"); datosCabecera.put( "titulo", "[" + (i + 1) + "/" + items.size() + "] " + (tituloOriginal != null ? tituloOriginal : "") ); } } Map direcciones_presupuesto = this.getDireccionesPresupuesto(cart, p); data_to_send.put("direcciones", direcciones_presupuesto.get("direcciones")); data_to_send.put("direccionesFP1", direcciones_presupuesto.get("direccionesFP1")); Map result = skApiClient.savePresupuesto(data_to_send); if (result.containsKey("error")) { System.out.println("Error al guardar presupuesto en SK: " + result.get("error")); // decide si seguir con otros items o abortar: // continue; o bien throw ... continue; } Object dataObj = result.get("data"); if (!(dataObj instanceof Map dataRaw)) { System.out.println("Formato inesperado de 'data' en savePresupuesto: " + result); continue; } @SuppressWarnings("unchecked") Map dataMap = (Map) dataRaw; presupuestoRequests.add(dataMap); } // Crear el pedido en base a los presupuestos guardados if(presupuestoRequests.isEmpty()) { throw new IllegalStateException("No se pudieron guardar los presupuestos en SK."); } else{ List presupuestoIds = new ArrayList<>(); for (Map presData : presupuestoRequests) { Long presId = ((Number) presData.get("id")).longValue(); presupuestoIds.add(presId); } Map ids = new HashMap<>(); ids.put("presupuesto_ids", presupuestoIds); Long pedidoId = skApiClient.crearPedido(ids); return pedidoId; } } public Map getDireccionesPresupuesto(Cart cart, Presupuesto presupuesto) { List> direccionesPresupuesto = new ArrayList<>(); List> direccionesPrueba = new ArrayList<>(); List direcciones = cart.getDirecciones().stream() .filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(presupuesto.getId())) .toList(); if (cart.getOnlyOneShipment()) { direcciones = direcciones.stream().limit(1).toList(); if (!direcciones.isEmpty()) { direccionesPresupuesto.add(direcciones.get(0).toSkMap( presupuesto.getSelectedTirada(), presupuesto.getPeso(), direcciones.get(0).getIsPalets(), false)); if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("ejemplar-prueba")) { direccionesPrueba.add(direcciones.get(0).toSkMap( 1, presupuesto.getPeso(), false, true)); } if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("deposito-legal")) { direccionesPresupuesto.add(direcciones.get(0).toSkMapDepositoLegal()); } Map direccionesRet = new HashMap<>(); direccionesRet.put("direcciones", direccionesPresupuesto); direccionesRet.put("direccionesFP1", direccionesPrueba.get(0)); return direccionesRet; } } else { for (CartDireccion cd : cart.getDirecciones()) { // direccion de ejemplar de prueba if(cd.getPresupuesto() == null || !cd.getPresupuesto().getId().equals(presupuesto.getId())) { continue; } if (cd.getUnidades() == null || cd.getUnidades() <= 0) { direccionesPrueba.add(cd.toSkMap( 1, presupuesto.getPeso(), false, true)); } else { direccionesPresupuesto.add(cd.toSkMap( cd.getUnidades(), presupuesto.getPeso(), cd.getIsPalets(), false)); } } if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("deposito-legal")) { CartDireccion cd = new CartDireccion(); direccionesPresupuesto.add(cd.toSkMapDepositoLegal()); } } Map direccionesRet = new HashMap<>(); direccionesRet.put("direcciones", direccionesPresupuesto); if(!direccionesPrueba.isEmpty()) direccionesRet.put("direccionesFP1", direccionesPrueba.get(0)); else{ direccionesRet.put("direccionesFP1", new ArrayList<>()); } return direccionesRet; } /*************************************** * 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; } }