package com.imprimelibros.erp.cart; import org.springframework.transaction.annotation.Transactional; import org.springframework.context.MessageSource; import org.springframework.stereotype.Service; import java.time.Instant; 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.users.UserService; 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.common.email.EmailService; import com.imprimelibros.erp.direcciones.DireccionService; import com.imprimelibros.erp.externalApi.skApiClient; import com.imprimelibros.erp.pedidos.PedidoRepository; import com.imprimelibros.erp.presupuesto.PresupuestoRepository; @Service public class CartService { private final EmailService emailService; private final CartRepository cartRepo; private final CartDireccionRepository cartDireccionRepo; private final CartItemRepository itemRepo; private final MessageSource messageSource; private final PresupuestoRepository presupuestoRepo; private final DireccionService direccionService; private final skApiClient skApiClient; private final PresupuestoService presupuestoService; private final PedidoRepository pedidoRepository; private final UserService userService; public CartService(CartRepository cartRepo, CartItemRepository itemRepo, CartDireccionRepository cartDireccionRepo, MessageSource messageSource, PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo, PedidoRepository pedidoRepository, DireccionService direccionService, skApiClient skApiClient,PresupuestoService presupuestoService, EmailService emailService, UserService userService) { this.cartRepo = cartRepo; this.itemRepo = itemRepo; this.cartDireccionRepo = cartDireccionRepo; this.messageSource = messageSource; this.presupuestoRepo = presupuestoRepo; this.direccionService = direccionService; this.skApiClient = skApiClient; this.presupuestoService = presupuestoService; this.emailService = emailService; this.pedidoRepository = pedidoRepository; this.userService = userService; } 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 = presupuestoService.getPresupuestoInfoForCard(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(); } public Map getCartSummaryRaw(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()) { if (direcciones != null && !direcciones.isEmpty()) { 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 (Boolean.FALSE.equals(res.get("success"))) { errorShipementCost = true; } else { shipment += (Double) res.get("shipment"); iva21 += (Double) res.get("iva21"); } } // ejemplar de prueba if (p.getServiciosJson() != null && p.getServiciosJson().contains("ejemplar-prueba")) { Map res = getShippingCost(cd, peso, 1, locale); if (Boolean.FALSE.equals(res.get("success"))) { errorShipementCost = true; } else { shipment += (Double) res.get("shipment"); iva21 += (Double) res.get("iva21"); } } } } else { 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() > 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 (Boolean.FALSE.equals(res.get("success"))) { errorShipementCost = true; } else { shipment += (Double) res.get("shipment"); iva21 += (Double) res.get("iva21"); } } firstDirection = false; } else { Map res = getShippingCost(cd, peso, unidades, locale); if (Boolean.FALSE.equals(res.get("success"))) { 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 (Boolean.FALSE.equals(res.get("success"))) { errorShipementCost = true; } else { shipment += (Double) res.get("shipment"); iva21 += (Double) res.get("iva21"); } } } } double totalBeforeDiscount = base + iva4 + iva21 + shipment; int fidelizacion = this.getDescuentoFidelizacion(cart.getUserId()); double descuento = totalBeforeDiscount * fidelizacion / 100.0; double total = totalBeforeDiscount - descuento; // Redondeo a 2 decimales base = Utils.round2(base); iva4 = Utils.round2(iva4); iva21 = Utils.round2(iva21); shipment = Utils.round2(shipment); descuento = Utils.round2(descuento); total = Utils.round2(total); Map summary = new HashMap<>(); summary.put("base", base); summary.put("iva4", iva4); summary.put("iva21", iva21); summary.put("shipment", shipment); summary.put("fidelizacion", fidelizacion); summary.put("descuento", descuento); summary.put("total", total); summary.put("amountCents", Math.round(total * 100)); summary.put("errorShipmentCost", errorShipementCost); summary.put("cartId", cart.getId()); return summary; } public int getDescuentoFidelizacion(Long userId) { // descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el // ultimo año) Instant haceUnAno = Instant.now().minusSeconds(365 * 24 * 60 * 60); double totalGastado = pedidoRepository.sumTotalByCreatedByAndCreatedAtAfter(userId, haceUnAno); 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; } public Map getCartSummary(Cart cart, Locale locale) { Map raw = getCartSummaryRaw(cart, locale); double base = (Double) raw.get("base"); double iva4 = (Double) raw.get("iva4"); double iva21 = (Double) raw.get("iva21"); double shipment = (Double) raw.get("shipment"); int fidelizacion = (Integer) raw.get("fidelizacion"); double descuento = (Double) raw.get("descuento"); double total = (Double) raw.get("total"); 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)); // negativo para mostrar summary.put("total", Utils.formatCurrency(total, locale)); summary.put("amountCents", raw.get("amountCents")); summary.put("errorShipmentCost", raw.get("errorShipmentCost")); summary.put("cartId", raw.get("cartId")); 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); // Se mueven los presupuestos de cartitems a ese usuario List items = itemRepo.findByCartId(cart.getId()); for (CartItem item : items) { Presupuesto p = item.getPresupuesto(); p.setUser(userService.findById(customerId)); presupuestoRepo.save(p); } 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); } /*************************************** * 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; } }