Compare commits

...

3 Commits

23 changed files with 1251 additions and 110 deletions

View File

@ -193,6 +193,11 @@
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<!-- IMPORTANTE: incluir dependencias con scope=system en el fat-jar -->
<configuration>
<!-- Esto hace que meta las dependencias con scope=system en BOOT-INF/lib -->
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin> </plugin>
<!-- (Migraciones) Plugin Maven para generar/ejecutar changelogs --> <!-- (Migraciones) Plugin Maven para generar/ejecutar changelogs -->

View File

@ -1,6 +1,8 @@
package com.imprimelibros.erp.cart; package com.imprimelibros.erp.cart;
import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import com.imprimelibros.erp.cart.dto.DireccionCardDTO; import com.imprimelibros.erp.cart.dto.DireccionCardDTO;
@ -98,4 +100,53 @@ public class CartDireccion {
); );
} }
public Map<String, Object> toSkMap(Integer numeroUnidades, Double pesoKg, Boolean palets, Boolean ejemplarPrueba) {
Map<String, Object> direccion = new HashMap<>();
direccion.put("cantidad", numeroUnidades);
direccion.put("peso", pesoKg);
direccion.put("att", this.getDireccion().getAtt());
direccion.put("email", this.getDireccion().getUser().getUserName());
direccion.put("direccion", this.getDireccion().getDireccion());
direccion.put("pais_code3", this.getDireccion().getPaisCode3());
direccion.put("cp", this.getDireccion().getCp());
direccion.put("municipio", this.getDireccion().getCiudad());
direccion.put("provincia", this.getDireccion().getProvincia());
direccion.put("telefono", this.getDireccion().getTelefono());
direccion.put("entregaPieCalle", palets ? 1 : 0);
direccion.put("is_ferro_prototipo", ejemplarPrueba ? 1 : 0);
direccion.put("num_ferro_prototipo", ejemplarPrueba ? 1 : 0);
Map<String, Object> map = new HashMap<>();
map.put("direccion", direccion);
map.put("unidades", numeroUnidades);
map.put("entregaPalets", palets ? 1 : 0);
return map;
}
public Map<String, Object> toSkMapDepositoLegal() {
Map<String, Object> direccion = new HashMap<>();
direccion.put("cantidad", 4);
direccion.put("peso", 0);
direccion.put("att", "Unidades para Depósito Legal (sin envío)");
direccion.put("email", "");
direccion.put("direccion", "");
direccion.put("pais_code3", "esp");
direccion.put("cp", "");
direccion.put("municipio", "");
direccion.put("provincia", "");
direccion.put("telefono", "");
direccion.put("entregaPieCalle", 0);
direccion.put("is_ferro_prototipo", 0);
direccion.put("num_ferro_prototipo", 0);
Map<String, Object> map = new HashMap<>();
map.put("direccion", direccion);
map.put("unidades", 4);
map.put("entregaPalets", 0);
return map;
}
} }

View File

@ -11,9 +11,12 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.math.BigDecimal;
import java.math.RoundingMode;
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter; import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
import com.imprimelibros.erp.cart.dto.CartDireccionRepository; import com.imprimelibros.erp.cart.dto.CartDireccionRepository;
import com.imprimelibros.erp.cart.dto.DireccionCardDTO; import com.imprimelibros.erp.cart.dto.DireccionCardDTO;
import com.imprimelibros.erp.cart.dto.DireccionShipment; import com.imprimelibros.erp.cart.dto.DireccionShipment;
@ -21,7 +24,8 @@ import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
import com.imprimelibros.erp.common.Utils; import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.direcciones.DireccionService; import com.imprimelibros.erp.direcciones.DireccionService;
import com.imprimelibros.erp.externalApi.skApiClient; import com.imprimelibros.erp.externalApi.skApiClient;
import com.imprimelibros.erp.pedido.PedidoService; import com.imprimelibros.erp.pedidos.Pedido;
import com.imprimelibros.erp.pedidos.PedidoService;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository; import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
@Service @Service
@ -36,12 +40,13 @@ public class CartService {
private final DireccionService direccionService; private final DireccionService direccionService;
private final skApiClient skApiClient; private final skApiClient skApiClient;
private final PedidoService pedidoService; private final PedidoService pedidoService;
private final PresupuestoService presupuestoService;
public CartService(CartRepository cartRepo, CartItemRepository itemRepo, public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
CartDireccionRepository cartDireccionRepo, MessageSource messageSource, CartDireccionRepository cartDireccionRepo, MessageSource messageSource,
PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo, PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo,
Utils utils, DireccionService direccionService, skApiClient skApiClient, Utils utils, DireccionService direccionService, skApiClient skApiClient,
PedidoService pedidoService) { PedidoService pedidoService, PresupuestoService presupuestoService) {
this.cartRepo = cartRepo; this.cartRepo = cartRepo;
this.itemRepo = itemRepo; this.itemRepo = itemRepo;
this.cartDireccionRepo = cartDireccionRepo; this.cartDireccionRepo = cartDireccionRepo;
@ -51,16 +56,14 @@ public class CartService {
this.direccionService = direccionService; this.direccionService = direccionService;
this.skApiClient = skApiClient; this.skApiClient = skApiClient;
this.pedidoService = pedidoService; this.pedidoService = pedidoService;
this.presupuestoService = presupuestoService;
} }
public Cart findById(Long cartId) { public Cart findById(Long cartId) {
return cartRepo.findById(cartId) return cartRepo.findById(cartId)
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado")); .orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
} }
/** Devuelve el carrito activo o lo crea si no existe. */ /** Devuelve el carrito activo o lo crea si no existe. */
@Transactional @Transactional
public Cart getOrCreateActiveCart(Long userId) { public Cart getOrCreateActiveCart(Long userId) {
@ -190,14 +193,13 @@ public class CartService {
return resumen; return resumen;
} }
public Map<String, Object> getCartSummary(Cart cart, Locale locale) { public Map<String, Object> getCartSummaryRaw(Cart cart, Locale locale) {
double base = 0.0; double base = 0.0;
double iva4 = 0.0; double iva4 = 0.0;
double iva21 = 0.0; double iva21 = 0.0;
double shipment = 0.0; double shipment = 0.0;
boolean errorShipementCost = false;
Boolean errorShipementCost = false;
List<CartItem> items = cart.getItems(); List<CartItem> items = cart.getItems();
List<CartDireccion> direcciones = cart.getDirecciones(); List<CartDireccion> direcciones = cart.getDirecciones();
@ -208,55 +210,58 @@ public class CartService {
base += p.getBaseImponible().doubleValue(); base += p.getBaseImponible().doubleValue();
iva4 += p.getIvaImporte4().doubleValue(); iva4 += p.getIvaImporte4().doubleValue();
iva21 += p.getIvaImporte21().doubleValue(); iva21 += p.getIvaImporte21().doubleValue();
if (cart.getOnlyOneShipment() != null && cart.getOnlyOneShipment()) { if (cart.getOnlyOneShipment() != null && cart.getOnlyOneShipment()) {
// Si es envío único, que es a españa y no ha canarias if (direcciones != null && !direcciones.isEmpty()) {
if (direcciones != null && direcciones.size() > 0) {
CartDireccion cd = direcciones.get(0); CartDireccion cd = direcciones.get(0);
Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(), boolean freeShipment = direccionService.checkFreeShipment(
cd.getDireccion().getCp(),
cd.getDireccion().getPaisCode3()) && !cd.getIsPalets(); cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
if (!freeShipment) { if (!freeShipment) {
Integer unidades = p.getSelectedTirada(); Integer unidades = p.getSelectedTirada();
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale); Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
if (res.get("success").equals(Boolean.FALSE)) { if (Boolean.FALSE.equals(res.get("success"))) {
errorShipementCost = true; errorShipementCost = true;
} } else {
else{
shipment += (Double) res.get("shipment"); shipment += (Double) res.get("shipment");
iva21 += (Double) res.get("iva21"); 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")) {
// ejemplar de prueba
if (p.getServiciosJson() != null && p.getServiciosJson().contains("ejemplar-prueba")) {
Map<String, Object> res = getShippingCost(cd, peso, 1, locale); Map<String, Object> res = getShippingCost(cd, peso, 1, locale);
if (res.get("success").equals(Boolean.FALSE)) { if (Boolean.FALSE.equals(res.get("success"))) {
errorShipementCost = true; errorShipementCost = true;
} } else {
else{
shipment += (Double) res.get("shipment"); shipment += (Double) res.get("shipment");
iva21 += (Double) res.get("iva21"); iva21 += (Double) res.get("iva21");
} }
} }
} }
} else { } else {
// envio por cada presupuesto
// buscar la direccion asignada a este presupuesto
if (direcciones == null) if (direcciones == null)
continue; continue;
List<CartDireccion> cd_presupuesto = direcciones.stream() List<CartDireccion> cd_presupuesto = direcciones.stream()
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(p.getId()) .filter(d -> d.getPresupuesto() != null
&& d.getUnidades() != null && d.getUnidades() != null && d.getUnidades() > 0) && d.getPresupuesto().getId().equals(p.getId())
&& d.getUnidades() != null
&& d.getUnidades() > 0)
.toList(); .toList();
Boolean firstDirection = true;
boolean firstDirection = true;
for (CartDireccion cd : cd_presupuesto) { for (CartDireccion cd : cd_presupuesto) {
Integer unidades = cd.getUnidades(); Integer unidades = cd.getUnidades();
if (firstDirection) { if (firstDirection) {
Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(), boolean freeShipment = direccionService.checkFreeShipment(
cd.getDireccion().getCp(),
cd.getDireccion().getPaisCode3()) && !cd.getIsPalets(); cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
if (!freeShipment && unidades != null && unidades > 0) { if (!freeShipment && unidades != null && unidades > 0) {
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale); Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
if (res.get("success").equals(Boolean.FALSE)) { if (Boolean.FALSE.equals(res.get("success"))) {
errorShipementCost = true; errorShipementCost = true;
} else { } else {
shipment += (Double) res.get("shipment"); shipment += (Double) res.get("shipment");
@ -266,7 +271,7 @@ public class CartService {
firstDirection = false; firstDirection = false;
} else { } else {
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale); Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
if (res.get("success").equals(Boolean.FALSE)) { if (Boolean.FALSE.equals(res.get("success"))) {
errorShipementCost = true; errorShipementCost = true;
} else { } else {
shipment += (Double) res.get("shipment"); shipment += (Double) res.get("shipment");
@ -274,18 +279,19 @@ public class CartService {
} }
} }
} }
// ejemplar de prueba // ejemplar de prueba
CartDireccion cd_prueba = direcciones.stream() CartDireccion cd_prueba = direcciones.stream()
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(p.getId()) .filter(d -> d.getPresupuesto() != null
&& d.getPresupuesto().getId().equals(p.getId())
&& d.getUnidades() == null) && d.getUnidades() == null)
.findFirst().orElse(null); .findFirst().orElse(null);
if (cd_prueba != null) {
if (cd_prueba != null) {
Map<String, Object> res = getShippingCost(cd_prueba, peso, 1, locale); Map<String, Object> res = getShippingCost(cd_prueba, peso, 1, locale);
if (res.get("success").equals(Boolean.FALSE)) { if (Boolean.FALSE.equals(res.get("success"))) {
errorShipementCost = true; errorShipementCost = true;
} } else {
else{
shipment += (Double) res.get("shipment"); shipment += (Double) res.get("shipment");
iva21 += (Double) res.get("iva21"); iva21 += (Double) res.get("iva21");
} }
@ -293,11 +299,45 @@ public class CartService {
} }
} }
double total = base + iva4 + iva21 + shipment; double totalBeforeDiscount = base + iva4 + iva21 + shipment;
int fidelizacion = pedidoService.getDescuentoFidelizacion(); int fidelizacion = pedidoService.getDescuentoFidelizacion();
double descuento = (total) * fidelizacion / 100.0; double descuento = totalBeforeDiscount * fidelizacion / 100.0;
total -= descuento; 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<String, Object> 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 Map<String, Object> getCartSummary(Cart cart, Locale locale) {
Map<String, Object> 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<String, Object> summary = new HashMap<>(); Map<String, Object> summary = new HashMap<>();
summary.put("base", Utils.formatCurrency(base, locale)); summary.put("base", Utils.formatCurrency(base, locale));
@ -305,11 +345,11 @@ public class CartService {
summary.put("iva21", Utils.formatCurrency(iva21, locale)); summary.put("iva21", Utils.formatCurrency(iva21, locale));
summary.put("shipment", Utils.formatCurrency(shipment, locale)); summary.put("shipment", Utils.formatCurrency(shipment, locale));
summary.put("fidelizacion", fidelizacion + "%"); summary.put("fidelizacion", fidelizacion + "%");
summary.put("descuento", Utils.formatCurrency(-descuento, locale)); summary.put("descuento", Utils.formatCurrency(-descuento, locale)); // negativo para mostrar
summary.put("total", Utils.formatCurrency(total, locale)); summary.put("total", Utils.formatCurrency(total, locale));
summary.put("amountCents", Math.round(total * 100)); summary.put("amountCents", raw.get("amountCents"));
summary.put("errorShipmentCost", errorShipementCost); summary.put("errorShipmentCost", raw.get("errorShipmentCost"));
summary.put("cartId", cart.getId()); summary.put("cartId", raw.get("cartId"));
return summary; return summary;
} }
@ -404,13 +444,159 @@ public class CartService {
// delete cart directions by direccion id in ACTIVE carts // delete cart directions by direccion id in ACTIVE carts
@Transactional @Transactional
public void deleteCartDireccionesByDireccionId(Long direccionId) { public void deleteCartDireccionesByDireccionId(Long direccionId) {
/*List<CartDireccion> cartDirecciones = cartDireccionRepo.findByDireccion_IdAndCart_Status(direccionId, Cart.Status.ACTIVE);
for (CartDireccion cd : cartDirecciones) {
cartDireccionRepo.deleteById(cd.getId());
}*/
cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE); cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE);
} }
@Transactional
public Long crearPedido(Long cartId, Locale locale) {
Cart cart = this.getCartById(cartId);
List<CartItem> items = cart.getItems();
List<Map<String, Object>> presupuestoRequests = new ArrayList<>();
List<Long> presupuestoIds = new ArrayList<>();
for (Integer i = 0; i < items.size(); i++) {
CartItem item = items.get(i);
Presupuesto p = item.getPresupuesto();
Map<String, Object> 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<String, Object> datosCabecera = (Map<String, Object>) data_to_send.get("datosCabecera");
if (datosCabecera != null) {
Object tituloOriginal = datosCabecera.get("titulo");
datosCabecera.put(
"titulo",
"[" + (i + 1) + "/" + items.size() + "] " + (tituloOriginal != null ? tituloOriginal : ""));
}
}
Map<String, Object> 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<String, Object> 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<String, Object> dataMap = (Map<String, Object>) dataRaw;
Long presId = ((Number) dataMap.get("id")).longValue();
String skin = ((String) dataMap.get("iskn")).toString();
p.setProveedor("Safekat");
p.setProveedorRef1(skin);
p.setProveedorRef2(presId);
p.setEstado(Presupuesto.Estado.aceptado);
presupuestoRepo.save(p);
presupuestoIds.add(p.getId());
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 {
ArrayList<Long> presupuestoSkIds = new ArrayList<>();
for (Map<String, Object> presData : presupuestoRequests) {
Long presId = ((Number) presData.get("id")).longValue();
presupuestoSkIds.add(presId);
}
Map<String, Object> ids = new HashMap<>();
ids.put("presupuesto_ids", presupuestoSkIds);
Long pedidoId = skApiClient.crearPedido(ids);
if (pedidoId == null) {
throw new IllegalStateException("No se pudo crear el pedido en SK.");
}
Pedido pedidoInterno = pedidoService.crearPedido(presupuestoIds, this.getCartSummaryRaw(cart, locale),
"Safekat", String.valueOf(pedidoId), cart.getUserId());
return pedidoInterno.getId();
}
}
public Map<String, Object> getDireccionesPresupuesto(Cart cart, Presupuesto presupuesto) {
List<Map<String, Object>> direccionesPresupuesto = new ArrayList<>();
List<Map<String, Object>> direccionesPrueba = new ArrayList<>();
List<CartDireccion> 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<String, Object> 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<String, Object> 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 * MÉTODOS PRIVADOS
***************************************/ ***************************************/

View File

@ -47,10 +47,17 @@ public class Utils {
this.messageSource = messageSource; this.messageSource = messageSource;
} }
public static double round2(double value) {
return BigDecimal.valueOf(value)
.setScale(2, RoundingMode.HALF_UP)
.doubleValue();
}
public static boolean isCurrentUserAdmin() { public static boolean isCurrentUserAdmin() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth.getAuthorities().stream() return auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN")); .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")
|| a.getAuthority().equals("ROLE_SUPERADMIN"));
} }
public static Long currentUserId(Principal principal) { public static Long currentUserId(Principal principal) {

View File

@ -2,6 +2,8 @@ package com.imprimelibros.erp.direcciones;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.SQLRestriction;

View File

@ -128,6 +128,125 @@ public class skApiClient {
}); });
} }
public Map<String, Object> savePresupuesto(Map<String, Object> requestBody) {
return performWithRetryMap(() -> {
String url = this.skApiUrl + "api/guardar";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(authService.getToken());
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.POST,
entity,
String.class);
ObjectMapper mapper = new ObjectMapper();
try {
Map<String, Object> responseBody = mapper.readValue(
response.getBody(),
new TypeReference<Map<String, Object>>() {
});
// Si la API devuelve "error" a nivel raíz
if (responseBody.get("error") != null) {
// Devolvemos un mapa con sólo el error para que el caller decida
return Map.of("error", responseBody.get("error"));
}
Object dataObj = responseBody.get("data");
if (dataObj instanceof Map<?, ?> dataRaw) {
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) dataRaw;
Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success")
: false);
Long id = ((Integer) data.get("id")).longValue();
String iskn = (String) data.get("iskn");
// OJO: aquí mantengo tu lógica tal cual (success == null o false => OK)
// Si tu API realmente usa success=true como éxito, esto habría que invertirlo.
if (success != null && success) {
if (id != null && iskn != null) {
data.put("id", Long.valueOf(id));
data.put("iskn", iskn);
}
} else {
// Tu lógica actual: si success es true u otra cosa → error 2
return Map.of("error", 2);
}
// Devolvemos sólo la parte interesante: el data ya enriquecido
return Map.of("data", data);
}
// Si data no es un Map, devolvemos error genérico
return Map.of("error", 1);
} catch (JsonProcessingException e) {
e.printStackTrace();
return Map.of("error", 1);
}
});
}
public Long crearPedido(Map<String, Object> requestBody) {
Map<String, Object> result = performWithRetryMap(() -> {
String url = this.skApiUrl + "api/crear-pedido";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(authService.getToken());
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.POST,
entity,
String.class);
ObjectMapper mapper = new ObjectMapper();
try {
Map<String, Object> responseBody = mapper.readValue(
response.getBody(),
new TypeReference<Map<String, Object>>() {
});
// Si la API devuelve "error" a nivel raíz
if (responseBody.get("error") != null) {
// Devolvemos un mapa con sólo el error para que el caller decida
return Map.of("error", responseBody.get("error"));
}
Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false);
Long id = ((Integer) responseBody.get("id")).longValue();
if (success != null && id != null && success) {
return Map.of("data", id);
} else {
// Tu lógica actual: si success es true u otra cosa → error 2
return Map.of("error", 2);
}
} catch (JsonProcessingException e) {
e.printStackTrace();
return Map.of("error", 1);
}
});
if (result.get("error") != null) {
throw new RuntimeException("Error al crear el pedido: " + result.get("error"));
}
return (Long) result.get("data");
}
public Integer getMaxSolapas(Map<String, Object> requestBody, Locale locale) { public Integer getMaxSolapas(Map<String, Object> requestBody, Locale locale) {
try { try {
String jsonResponse = performWithRetry(() -> { String jsonResponse = performWithRetry(() -> {
@ -238,7 +357,6 @@ public class skApiClient {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(authService.getToken()); headers.setBearerAuth(authService.getToken());
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
uri, uri,
HttpMethod.GET, HttpMethod.GET,
@ -255,10 +373,10 @@ public class skApiClient {
return Map.of("error", messageSource.getMessage("direcciones.error.noShippingCost", null, locale)); return Map.of("error", messageSource.getMessage("direcciones.error.noShippingCost", null, locale));
} else { } else {
Double total = Optional.ofNullable(responseBody.get("data")) Double total = Optional.ofNullable(responseBody.get("data"))
.filter(Number.class::isInstance) .filter(Number.class::isInstance)
.map(Number.class::cast) .map(Number.class::cast)
.map(Number::doubleValue) .map(Number::doubleValue)
.orElse(0.0); .orElse(0.0);
return Map.of("data", total); return Map.of("data", total);
} }

View File

@ -295,11 +295,11 @@ public class PaymentController {
} }
@PostMapping(value = "/transfer/completed/{id}", produces = "application/json") @PostMapping(value = "/transfer/completed/{id}", produces = "application/json")
public ResponseEntity<Map<String, Object>> markTransferAsCaptured(@PathVariable Long id) { public ResponseEntity<Map<String, Object>> markTransferAsCaptured(@PathVariable Long id, Locale locale) {
Map<String, Object> response; Map<String, Object> response;
try { try {
paymentService.markBankTransferAsCaptured(id); paymentService.markBankTransferAsCaptured(id, locale);
response = Map.of("success", true); response = Map.of("success", true);
return ResponseEntity.ok(response); return ResponseEntity.ok(response);

View File

@ -15,6 +15,7 @@ import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.payments.repo.WebhookEventRepository; import com.imprimelibros.erp.payments.repo.WebhookEventRepository;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
@Service @Service
@ -36,7 +37,7 @@ public class PaymentService {
this.payRepo = payRepo; this.payRepo = payRepo;
this.txRepo = txRepo; this.txRepo = txRepo;
this.refundRepo = refundRepo; this.refundRepo = refundRepo;
this.redsysService = redsysService; this.redsysService = redsysService;
this.webhookEventRepo = webhookEventRepo; this.webhookEventRepo = webhookEventRepo;
this.cartService = cartService; this.cartService = cartService;
} }
@ -82,7 +83,7 @@ public class PaymentService {
} }
@Transactional @Transactional
public void handleRedsysNotification(String dsSignature, String dsMerchantParameters) throws Exception { public void handleRedsysNotification(String dsSignature, String dsMerchantParameters, Locale locale) throws Exception {
// 0) Intentamos parsear la notificación. Si falla, registramos el webhook crudo // 0) Intentamos parsear la notificación. Si falla, registramos el webhook crudo
// y salimos. // y salimos.
@ -197,7 +198,7 @@ public class PaymentService {
} }
if (authorized) { if (authorized) {
processOrder(notif.cartId); processOrder(notif.cartId, locale);
} }
payRepo.save(p); payRepo.save(p);
@ -317,7 +318,7 @@ public class PaymentService {
} }
@Transactional @Transactional
public void markBankTransferAsCaptured(Long paymentId) { public void markBankTransferAsCaptured(Long paymentId, Locale locale) {
Payment p = payRepo.findById(paymentId) Payment p = payRepo.findById(paymentId)
.orElseThrow(() -> new IllegalArgumentException("Payment no encontrado: " + paymentId)); .orElseThrow(() -> new IllegalArgumentException("Payment no encontrado: " + paymentId));
@ -354,7 +355,7 @@ public class PaymentService {
// 4) Procesar el pedido asociado al carrito (si existe) // 4) Procesar el pedido asociado al carrito (si existe)
if (p.getOrderId() != null) { if (p.getOrderId() != null) {
processOrder(p.getOrderId()); processOrder(p.getOrderId(), locale);
} }
} }
@ -450,17 +451,26 @@ public class PaymentService {
return code >= 0 && code <= 99; return code >= 0 && code <= 99;
} }
private Boolean processOrder(Long cartId) { /**
// GENERAR PEDIDO A PARTIR DEL CARRITO * Procesa el pedido asociado al carrito:
* - bloquea el carrito
* - crea el pedido a partir del carrito
*
*/
private Boolean processOrder(Long cartId, Locale locale) {
Cart cart = this.cartService.findById(cartId); Cart cart = this.cartService.findById(cartId);
if (cart != null) { if (cart != null) {
// Bloqueamos el carrito // Bloqueamos el carrito
this.cartService.lockCartById(cart.getId()); this.cartService.lockCartById(cart.getId());
// order ID es generado dentro de createOrderFromCart donde se marcan los // Creamos el pedido
// presupuestos como no editables Long orderId = this.cartService.crearPedido(cart.getId(), locale);
// Long orderId = if(orderId == null){
// this.cartService.pedidoService.createOrderFromCart(cart.getId(), p.getId()); return false;
// p.setOrderId(orderId); }
else{
// envio de correo de confirmacion de pedido podria ir aqui
}
} }
return true; return true;

View File

@ -1,26 +0,0 @@
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;
}
}

View File

@ -0,0 +1,191 @@
package com.imprimelibros.erp.pedidos;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "pedidos")
public class Pedido {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Campos económicos
@Column(name = "base", nullable = false)
private Double base;
@Column(name = "envio", nullable = false)
private Double envio = 0.0;
@Column(name = "iva4", nullable = false)
private Double iva4 = 0.0;
@Column(name = "iva21", nullable = false)
private Double iva21 = 0.0;
@Column(name = "descuento", nullable = false)
private Double descuento = 0.0;
@Column(name = "total", nullable = false)
private Double total = 0.0;
// Datos de proveedor
@Column(name = "proveedor", length = 100)
private String proveedor;
@Column(name = "proveedor_ref", length = 100)
private String proveedorRef;
// Auditoría básica (coincidiendo con las columnas que se ven en la captura)
@Column(name = "created_by")
private Long createdBy;
@Column(name = "updated_by")
private Long updatedBy;
@Column(name = "deleted_by")
private Long deletedBy;
@Column(name = "deleted", nullable = false)
private boolean deleted = false;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
// --- Getters y setters ---
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Double getBase() {
return base;
}
public void setBase(Double base) {
this.base = base;
}
public Double getEnvio() {
return envio;
}
public void setEnvio(Double envio) {
this.envio = envio;
}
public Double getIva4() {
return iva4;
}
public void setIva4(Double iva4) {
this.iva4 = iva4;
}
public Double getIva21() {
return iva21;
}
public void setIva21(Double iva21) {
this.iva21 = iva21;
}
public Double getDescuento() {
return descuento;
}
public void setDescuento(Double descuento) {
this.descuento = descuento;
}
public Double getTotal() {
return total;
}
public void setTotal(Double total) {
this.total = total;
}
public String getProveedor() {
return proveedor;
}
public void setProveedor(String proveedor) {
this.proveedor = proveedor;
}
public String getProveedorRef() {
return proveedorRef;
}
public void setProveedorRef(String proveedorRef) {
this.proveedorRef = proveedorRef;
}
public Long getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}
public Long getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(Long updatedBy) {
this.updatedBy = updatedBy;
}
public Long getDeletedBy() {
return deletedBy;
}
public void setDeletedBy(Long deletedBy) {
this.deletedBy = deletedBy;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@ -0,0 +1,71 @@
package com.imprimelibros.erp.pedidos;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
@Entity
@Table(name = "pedidos_lineas")
public class PedidoLinea {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "pedido_id", nullable = false)
private Pedido pedido;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "presupuesto_id", nullable = false)
private Presupuesto presupuesto;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "created_by", nullable = false)
private Long createdBy;
// --- Getters y setters ---
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Pedido getPedido() {
return pedido;
}
public void setPedido(Pedido pedido) {
this.pedido = pedido;
}
public Presupuesto getPresupuesto() {
return presupuesto;
}
public void setPresupuesto(Presupuesto presupuesto) {
this.presupuesto = presupuesto;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public Long getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}
}

View File

@ -0,0 +1,14 @@
package com.imprimelibros.erp.pedidos;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PedidoLineaRepository extends JpaRepository<PedidoLinea, Long> {
List<PedidoLinea> findByPedidoId(Long pedidoId);
List<PedidoLinea> findByPresupuestoId(Long presupuestoId);
}

View File

@ -0,0 +1,10 @@
package com.imprimelibros.erp.pedidos;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PedidoRepository extends JpaRepository<Pedido, Long> {
// aquí podrás añadir métodos tipo:
// List<Pedido> findByDeletedFalse();
}

View File

@ -0,0 +1,102 @@
package com.imprimelibros.erp.pedidos;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
@Service
public class PedidoService {
private final PedidoRepository pedidoRepository;
private final PedidoLineaRepository pedidoLineaRepository;
private final PresupuestoRepository presupuestoRepository;
public PedidoService(PedidoRepository pedidoRepository, PedidoLineaRepository pedidoLineaRepository,
PresupuestoRepository presupuestoRepository) {
this.pedidoRepository = pedidoRepository;
this.pedidoLineaRepository = pedidoLineaRepository;
this.presupuestoRepository = presupuestoRepository;
}
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;
}
/**
* Crea un pedido a partir de:
* - lista de IDs de presupuesto
* - resumen numérico del carrito (getCartSummaryRaw)
* - datos de proveedor
* - usuario que crea el pedido
*/
@Transactional
public Pedido crearPedido(List<Long> presupuestoIds,
Map<String, Object> cartSummaryRaw,
String proveedor,
String proveedorRef,
Long userId) {
Pedido pedido = new Pedido();
// Datos económicos (ojo con las claves, son las del summaryRaw)
pedido.setBase((Double) cartSummaryRaw.getOrDefault("base", 0.0d));
pedido.setEnvio((Double) cartSummaryRaw.getOrDefault("shipment", 0.0d));
pedido.setIva4((Double) cartSummaryRaw.getOrDefault("iva4", 0.0d));
pedido.setIva21((Double) cartSummaryRaw.getOrDefault("iva21", 0.0d));
pedido.setDescuento((Double) cartSummaryRaw.getOrDefault("descuento", 0.0d));
pedido.setTotal((Double) cartSummaryRaw.getOrDefault("total", 0.0d));
// Proveedor
pedido.setProveedor(proveedor);
pedido.setProveedorRef(proveedorRef);
// Auditoría mínima
pedido.setCreatedBy(userId);
pedido.setCreatedAt(LocalDateTime.now());
pedido.setDeleted(false);
pedido.setUpdatedAt(LocalDateTime.now());
pedido.setUpdatedBy(userId);
// Guardamos el pedido
Pedido saved = pedidoRepository.save(pedido);
// Crear líneas del pedido
for (Long presupuestoId : presupuestoIds) {
Presupuesto presupuesto = presupuestoRepository.getReferenceById(presupuestoId);
PedidoLinea linea = new PedidoLinea();
linea.setPedido(saved);
linea.setPresupuesto(presupuesto);
linea.setCreatedBy(userId);
linea.setCreatedAt(LocalDateTime.now());
pedidoLineaRepository.save(linea);
}
return saved;
}
}

View File

@ -573,7 +573,7 @@ public class PresupuestoController {
String path = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) String path = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest().getRequestURI(); .getRequest().getRequestURI();
String mode = path.contains("/view/") ? "view" : "edit"; String mode = path.contains("/view/") ? "view" : "edit";
if (mode.equals("view")) { if (mode.equals("view") || presupuestoOpt.get().getEstado() != Presupuesto.Estado.borrador) {
model.addAttribute("appMode", "view"); model.addAttribute("appMode", "view");
} else { } else {
model.addAttribute("cliente_id", presupuestoOpt.get().getUser().getId()); model.addAttribute("cliente_id", presupuestoOpt.get().getUser().getId());

View File

@ -120,7 +120,7 @@ public class PresupuestoDatatableService {
String id = String.valueOf(p.getId()); String id = String.valueOf(p.getId());
String editBtn = "<a href=\"javascript:void(0);\" data-id=\"" + id + "\" class=\"link-success btn-edit-" + String editBtn = "<a href=\"javascript:void(0);\" data-id=\"" + id + "\" class=\"link-success btn-edit-" +
(p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado") + " fs-15\"><i class=\"ri-" + (p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado") + " fs-15\"><i class=\"ri-" +
(p.getOrigen().equals(Presupuesto.Origen.publico) ? "eye" : "pencil") + "-line\"></i></a>"; (p.getOrigen().equals(Presupuesto.Origen.publico) || p.getEstado() == Presupuesto.Estado.aceptado ? "eye" : "pencil") + "-line\"></i></a>";
String deleteBtn = borrador ? "<a href=\"javascript:void(0);\" data-id=\"" + id String deleteBtn = borrador ? "<a href=\"javascript:void(0);\" data-id=\"" + id
+ "\" class=\"link-danger btn-delete-" + "\" class=\"link-danger btn-delete-"

View File

@ -99,23 +99,27 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
modificado("presupuesto.estado.modificado"); modificado("presupuesto.estado.modificado");
private final String messageKey; private final String messageKey;
Estado(String messageKey) { Estado(String messageKey) {
this.messageKey = messageKey; this.messageKey = messageKey;
} }
public String getMessageKey() { public String getMessageKey() {
return messageKey; return messageKey;
} }
} }
public enum Entrega{ public enum Entrega {
peninsula("presupuesto.entrega.peninsula"), peninsula("presupuesto.entrega.peninsula"),
canarias("presupuesto.entrega.canarias"), canarias("presupuesto.entrega.canarias"),
paises_ue("presupuesto.entrega.paises-ue"); paises_ue("presupuesto.entrega.paises-ue");
private final String messageKey; private final String messageKey;
Entrega(String messageKey) { Entrega(String messageKey) {
this.messageKey = messageKey; this.messageKey = messageKey;
} }
public String getMessageKey() { public String getMessageKey() {
return messageKey; return messageKey;
} }
@ -371,6 +375,18 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
@Column(name = "alto_faja") @Column(name = "alto_faja")
private Integer altoFaja = 0; private Integer altoFaja = 0;
@Column(name = "comentario", columnDefinition = "TEXT")
private String comentario;
@Column(name = "proveedor", length = 100)
private String proveedor;
@Column(name = "proveedor_ref1", length = 100)
private String proveedorRef1;
@Column(name = "proveedor_ref2")
private Long proveedorRef2;
// ====== MÉTODOS AUX ====== // ====== MÉTODOS AUX ======
public String resumenPresupuesto() { public String resumenPresupuesto() {
@ -912,16 +928,48 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
this.altoFaja = altoFaja; this.altoFaja = altoFaja;
} }
public Long getId(){ public Long getId() {
return id; return id;
} }
public void setId(Long id){ public String getComentario() {
return comentario;
}
public void setComentario(String comentario) {
this.comentario = comentario;
}
public String getProveedor() {
return proveedor;
}
public void setProveedor(String proveedor) {
this.proveedor = proveedor;
}
public String getProveedorRef1() {
return proveedorRef1;
}
public void setProveedorRef1(String proveedorRef1) {
this.proveedorRef1 = proveedorRef1;
}
public Long getProveedorRef2() {
return proveedorRef2;
}
public void setProveedorRef2(Long proveedorRef2) {
this.proveedorRef2 = proveedorRef2;
}
public void setId(Long id) {
this.id = id; this.id = id;
} }
public Double getPeso(){ public Double getPeso() {
// get peso from first element of pricingSnapshotJson (need to parse JSON) // get peso from first element of pricingSnapshotJson (need to parse JSON)
// pricingSnapshotJson = {"xxx":{"peso":0.5,...}} is a String // pricingSnapshotJson = {"xxx":{"peso":0.5,...}} is a String
if (this.pricingSnapshotJson != null && !this.pricingSnapshotJson.isEmpty()) { if (this.pricingSnapshotJson != null && !this.pricingSnapshotJson.isEmpty()) {

View File

@ -290,6 +290,10 @@ public class PresupuestoService {
} }
public Map<String, Object> toSkApiRequest(Presupuesto presupuesto) { public Map<String, Object> toSkApiRequest(Presupuesto presupuesto) {
return toSkApiRequest(presupuesto, false);
}
public Map<String, Object> toSkApiRequest(Presupuesto presupuesto, Boolean toSave) {
final int SK_CLIENTE_ID = 1284; final int SK_CLIENTE_ID = 1284;
final int SK_PAGINAS_CUADERNILLO = 32; final int SK_PAGINAS_CUADERNILLO = 32;
@ -311,9 +315,16 @@ public class PresupuestoService {
Map<String, Object> body = new HashMap<>(); Map<String, Object> body = new HashMap<>();
body.put("tipo_impresion_id", this.getTipoImpresionId(presupuesto)); body.put("tipo_impresion_id", this.getTipoImpresionId(presupuesto));
body.put("tirada", Arrays.stream(presupuesto.getTiradas()) if (toSave) {
.filter(Objects::nonNull) body.put("tirada", Arrays.stream(presupuesto.getTiradas())
.collect(Collectors.toList())); .filter(Objects::nonNull)
.map(tirada -> tirada + 4)
.collect(Collectors.toList()));
} else {
body.put("tirada", Arrays.stream(presupuesto.getTiradas())
.filter(Objects::nonNull)
.collect(Collectors.toList()));
}
body.put("tamanio", tamanio); body.put("tamanio", tamanio);
body.put("tipo", presupuesto.getTipoEncuadernacion()); body.put("tipo", presupuesto.getTipoEncuadernacion());
body.put("clienteId", SK_CLIENTE_ID); body.put("clienteId", SK_CLIENTE_ID);
@ -343,9 +354,39 @@ public class PresupuestoService {
faja.put("alto", presupuesto.getAltoFaja()); faja.put("alto", presupuesto.getAltoFaja());
body.put("faja", faja); body.put("faja", faja);
} }
// body.put("servicios", servicios);
if (toSave) {
Map<String, Object> data = new HashMap<>();
data.put("input_data", body);
data.put("ferroDigital", 1);
data.put("ferro", 0);
data.put("marcapaginas", 0);
data.put("retractilado5", 0);
if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().indexOf("ejemplar-prueba") > 0) {
data.put("prototipo", 1);
} else {
data.put("prototipo", 0);
}
if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().indexOf("retractilado") > 0) {
data.put("retractilado", 1);
} else {
data.put("retractilado", 0);
}
data.put("ivaReducido", presupuesto.getIvaReducido() ? 1 : 0);
data.put("confirmar", 1);
Map<String, Object> datosCabecera = new HashMap<>();
datosCabecera.put("titulo", presupuesto.getTitulo());
datosCabecera.put("autor", presupuesto.getAutor());
datosCabecera.put("isbn", presupuesto.getIsbn());
datosCabecera.put("coleccion", "");
datosCabecera.put("referenciaCliente", presupuesto.getId());
data.put("datosCabecera", datosCabecera);
return data;
}
return body; return body;
} }
public Integer getTipoImpresionId(Presupuesto presupuesto) { public Integer getTipoImpresionId(Presupuesto presupuesto) {
@ -599,11 +640,11 @@ public class PresupuestoService {
.doubleValue(); .doubleValue();
} }
// precio calculado por matrices * num. cols -1 * precio por columna // precio calculado por matrices * num. cols -1 * precio por columna
if (presupuestoMaquetacion.getNumColumnas() > 1) { if (presupuestoMaquetacion.getNumColumnas() > 1) {
precio = precio.add(precio.multiply( precio = precio.add(precio.multiply(
BigDecimal.valueOf(presupuestoMaquetacion.getNumColumnas() - 1)) BigDecimal.valueOf(presupuestoMaquetacion.getNumColumnas() - 1))
.multiply(BigDecimal.valueOf(price.apply("columnas"))) ); .multiply(BigDecimal.valueOf(price.apply("columnas"))));
} }
precio = precio precio = precio

View File

@ -103,10 +103,10 @@ public class RedsysController {
@PostMapping(value = "/ok", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @PostMapping(value = "/ok", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody @ResponseBody
public ResponseEntity<String> okPost(@RequestParam("Ds_Signature") String signature, public ResponseEntity<String> okPost(@RequestParam("Ds_Signature") String signature,
@RequestParam("Ds_MerchantParameters") String merchantParameters) { @RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) {
try { try {
// opcional: idempotente, si /notify ya ha hecho el trabajo no pasa nada // opcional: idempotente, si /notify ya ha hecho el trabajo no pasa nada
paymentService.handleRedsysNotification(signature, merchantParameters); paymentService.handleRedsysNotification(signature, merchantParameters, locale);
return ResponseEntity.ok("<h2>Pago realizado correctamente</h2><a href=\"/cart\">Volver</a>"); return ResponseEntity.ok("<h2>Pago realizado correctamente</h2><a href=\"/cart\">Volver</a>");
} catch (Exception e) { } catch (Exception e) {
return ResponseEntity.badRequest() return ResponseEntity.badRequest()
@ -127,11 +127,11 @@ public class RedsysController {
@ResponseBody @ResponseBody
public ResponseEntity<String> koPost( public ResponseEntity<String> koPost(
@RequestParam("Ds_Signature") String signature, @RequestParam("Ds_Signature") String signature,
@RequestParam("Ds_MerchantParameters") String merchantParameters) { @RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) {
try { try {
// Procesamos la notificación IGUAL que en /ok y /notify // Procesamos la notificación IGUAL que en /ok y /notify
paymentService.handleRedsysNotification(signature, merchantParameters); paymentService.handleRedsysNotification(signature, merchantParameters, locale);
// Mensaje para el usuario (pago cancelado/rechazado) // Mensaje para el usuario (pago cancelado/rechazado)
String html = "<h2>Pago cancelado o rechazado</h2><a href=\"/checkout\">Volver</a>"; String html = "<h2>Pago cancelado o rechazado</h2><a href=\"/checkout\">Volver</a>";
@ -146,9 +146,9 @@ public class RedsysController {
@PostMapping(value = "/notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @PostMapping(value = "/notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody @ResponseBody
public String notifyRedsys(@RequestParam("Ds_Signature") String signature, public String notifyRedsys(@RequestParam("Ds_Signature") String signature,
@RequestParam("Ds_MerchantParameters") String merchantParameters) { @RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) {
try { try {
paymentService.handleRedsysNotification(signature, merchantParameters); paymentService.handleRedsysNotification(signature, merchantParameters, locale);
return "OK"; return "OK";
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); // 👈 para ver el motivo del 500 en logs e.printStackTrace(); // 👈 para ver el motivo del 500 en logs

View File

@ -0,0 +1,210 @@
databaseChangeLog:
- changeSet:
id: 0011-update-pedidos-presupuesto
author: jjo
changes:
# 1) Nuevas columnas en PRESUPUESTO
- addColumn:
tableName: presupuesto
columns:
- column:
name: comentario
type: TEXT
afterColumn: pricing_snapshot
constraints:
nullable: true
- column:
name: proveedor
type: VARCHAR(100)
constraints:
nullable: true
- column:
name: proveedor_ref1
type: VARCHAR(100)
constraints:
nullable: true
- column:
name: proveedor_ref2
type: BIGINT
constraints:
nullable: true
# 2) Cambios en PEDIDOS
# 2.1 Eliminar FK fk_pedidos_presupuesto
- dropForeignKeyConstraint:
baseTableName: pedidos
constraintName: fk_pedidos_presupuesto
# 2.2 Eliminar columna presupuesto_id
- dropColumn:
tableName: pedidos
columnName: presupuesto_id
# 2.3 Añadir nuevas columnas después de id
- addColumn:
tableName: pedidos
columns:
- column:
name: base
type: DOUBLE
afterColumn: id
constraints:
nullable: false
- column:
name: envio
type: DOUBLE
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: iva4
type: DOUBLE
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: iva21
type: DOUBLE
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: descuento
type: DOUBLE
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: total
type: DOUBLE
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: proveedor
type: VARCHAR(100)
afterColumn: total
constraints:
nullable: true
- column:
name: proveedor_ref
type: VARCHAR(100)
afterColumn: proveedor
constraints:
nullable: true
# 3) Crear tabla PEDIDOS_LINEAS
- createTable:
tableName: pedidos_lineas
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: pedido_id
type: BIGINT
constraints:
nullable: false
- column:
name: presupuesto_id
type: BIGINT
constraints:
nullable: false
- column:
name: created_at
type: DATETIME(3)
constraints:
nullable: true
- column:
name: created_by
type: BIGINT
constraints:
nullable: false
# FKs de pedidos_lineas
- addForeignKeyConstraint:
baseTableName: pedidos_lineas
baseColumnNames: pedido_id
constraintName: fk_pedidos_lineas_pedido
referencedTableName: pedidos
referencedColumnNames: id
onDelete: RESTRICT
- addForeignKeyConstraint:
baseTableName: pedidos_lineas
baseColumnNames: presupuesto_id
constraintName: fk_pedidos_lineas_presupuesto
referencedTableName: presupuesto
referencedColumnNames: id
onDelete: RESTRICT
- addForeignKeyConstraint:
baseTableName: pedidos_lineas
baseColumnNames: created_by
constraintName: fk_pedidos_lineas_created_by_user
referencedTableName: users
referencedColumnNames: id
onDelete: RESTRICT
rollback:
# 3) Eliminar tabla pedidos_lineas y sus FKs
- dropTable:
tableName: pedidos_lineas
# 2) Revertir cambios en PEDIDOS
- dropColumn:
tableName: pedidos
columns:
- column:
name: base
- column:
name: envio
- column:
name: iva4
- column:
name: iva21
- column:
name: descuento
- column:
name: total
- column:
name: proveedor
- column:
name: proveedor_ref
# 2.2 Volver a crear presupuesto_id
- addColumn:
tableName: pedidos
columns:
- column:
name: presupuesto_id
type: BIGINT
constraints:
nullable: true
# 2.1 Volver a crear la FK fk_pedidos_presupuesto
- addForeignKeyConstraint:
baseTableName: pedidos
baseColumnNames: presupuesto_id
constraintName: fk_pedidos_presupuesto
referencedTableName: presupuesto
referencedColumnNames: id
onDelete: RESTRICT
# 1) Eliminar columnas añadidas en PRESUPUESTO
- dropColumn:
tableName: presupuesto
columns:
- column:
name: comentario
- column:
name: proveedor
- column:
name: proveedor_ref1
- column:
name: proveedor_ref2

View File

@ -18,4 +18,6 @@ databaseChangeLog:
- include: - include:
file: db/changelog/changesets/0009-add-composite-unique-txid-type.yml file: db/changelog/changesets/0009-add-composite-unique-txid-type.yml
- include: - include:
file: db/changelog/changesets/0010-drop-unique-tx-gateway.yml file: db/changelog/changesets/0010-drop-unique-tx-gateway.yml
- include:
file: db/changelog/changesets/0011-update-pedidos-presupuesto.yml

View File

@ -0,0 +1,72 @@
<div id="presupuesto-app" th:data-mode="${appMode} ?: 'public'" th:data-id="${id} ?: ''" th:fragment="presupuestador">
<div class="row" id="presupuesto-row">
<div class="animate-fadeInUpBounce">
<!-- Ribbon Shape -->
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow">
<div class="card-body">
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{presupuesto.resumen}">Resumen
</div>
</div>
<div class="ribbon-content mt-4">
<div id="div-extras" class="hstack gap-2 justify-content-center flex-wrap">
</div>
</div>
</div>
<!-- End Ribbon Shape -->
<div class="col-9 mx-auto mt-4">
<h5 id="resumen-titulo" class="text-center"></h5>
<table id="resumen-tabla-final" class="table table-borderless table-striped mt-3"
th:data-currency="#{app.currency}">
<thead>
<tr>
<th></th>
<th th:text="#{presupuesto.resumen.tabla.descripcion}">Descripción</th>
<th th:text="#{presupuesto.resumen.tabla.cantidad}">Cantidad</th>
<th th:text="#{presupuesto.resumen.tabla.precio-unidad}">Precio unitario</th>
<th th:text="#{presupuesto.resumen.tabla.precio-total}">Precio total</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr class="table-active">
<th colspan="4" class="text-end" th:text="#{presupuesto.resumen.tabla.base}">Total</th>
<th class="text-end" id="resumen-base">0,00 €</th>
</tr>
<tr id="tr-resumen-iva4" class="table-active">
<th colspan="4" class="text-end" th:text="#{presupuesto.resumen.tabla.iva4}">IVA (4%)</th>
<th class="text-end" id="resumen-iva4">0,00 €</th>
</tr>
<tr id="tr-resumen-iva21" class="table-active">
<th colspan="4" class="text-end" th:text="#{presupuesto.resumen.tabla.iva21}">IVA (21%)</th>
<th class="text-end" id="resumen-iva21">0,00 €</th>
</tr>
<tr class="table-active">
<th colspan="4" class="text-end" th:text="#{presupuesto.resumen.tabla.total}">Total con IVA
</th>
<th class="text-end" id="resumen-total">0,00 €</th>
</tfoot>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
<button type="button"
class="btn btn-secondary d-flex align-items-center mx-2 btn-imprimir">
<i class="ri-printer-line me-2"></i>
<span th:text="#{app.imprimir}">Imprimir</span>
</button>
<button type="button"
class="btn btn-secondary d-flex align-items-center mx-2 add-cart-btn">
<i class="ri-shopping-cart-line me-2"></i>
<span th:text="#{presupuesto.add-to-cart}">Añadir a la cesta</span>
</button>
</div>
</div>
</div>
<!--end row-->
</div>

View File

@ -0,0 +1,27 @@
package com.imprimelibros.erp.presupuesto;
import java.util.Locale;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.imprimelibros.erp.cart.CartService;
@SpringBootTest
public class savePresupuestosTest {
@Autowired
private CartService cartService;
@Test
void testGuardarPresupuesto() {
Locale locale = new Locale("es", "ES");
Long resultado = cartService.crearPedido(9L, locale);
System.out.println("📦 Presupuesto guardado:");
System.out.println(resultado);
// Aquí irían las aserciones para verificar que el presupuesto se guardó correctamente
}
}