haciendo pagos pendientes

This commit is contained in:
2025-12-22 20:41:21 +01:00
parent 4cc47b4249
commit d4120bb486
18 changed files with 1312 additions and 572 deletions

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.8</version>
<version>3.5.9</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.imprimelibros</groupId>

View File

@ -5,6 +5,7 @@ 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;
@ -20,15 +21,17 @@ 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.Pedido;
import com.imprimelibros.erp.pedidos.PedidoService;
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;
@ -36,14 +39,13 @@ public class CartService {
private final PresupuestoRepository presupuestoRepo;
private final DireccionService direccionService;
private final skApiClient skApiClient;
private final PedidoService pedidoService;
private final PresupuestoService presupuestoService;
private final PedidoRepository pedidoRepository;
public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
CartDireccionRepository cartDireccionRepo, MessageSource messageSource,
PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo,
DireccionService direccionService, skApiClient skApiClient,
PedidoService pedidoService, PresupuestoService presupuestoService) {
PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo, PedidoRepository pedidoRepository,
DireccionService direccionService, skApiClient skApiClient,PresupuestoService presupuestoService, EmailService emailService) {
this.cartRepo = cartRepo;
this.itemRepo = itemRepo;
this.cartDireccionRepo = cartDireccionRepo;
@ -51,8 +53,9 @@ public class CartService {
this.presupuestoRepo = presupuestoRepo;
this.direccionService = direccionService;
this.skApiClient = skApiClient;
this.pedidoService = pedidoService;
this.presupuestoService = presupuestoService;
this.emailService = emailService;
this.pedidoRepository = pedidoRepository;
}
public Cart findById(Long cartId) {
@ -264,7 +267,7 @@ public class CartService {
}
double totalBeforeDiscount = base + iva4 + iva21 + shipment;
int fidelizacion = pedidoService.getDescuentoFidelizacion(cart.getUserId());
int fidelizacion = this.getDescuentoFidelizacion(cart.getUserId());
double descuento = totalBeforeDiscount * fidelizacion / 100.0;
double total = totalBeforeDiscount - descuento;
@ -291,6 +294,27 @@ public class CartService {
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<String, Object> getCartSummary(Cart cart, Locale locale) {
Map<String, Object> raw = getCartSummaryRaw(cart, locale);
@ -411,184 +435,6 @@ public class CartService {
cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE);
}
@Transactional
public Long crearPedido(Long cartId, Long dirFactId, Locale locale) {
Cart cart = this.getCartById(cartId);
List<CartItem> items = cart.getItems();
List<Map<String, Object>> presupuestoRequests = new ArrayList<>();
Map<String, Object> presupuestoDireccionesRequest = new HashMap<>();
List<Long> presupuestoIds = new ArrayList<>();
for (Integer i = 0; i < items.size(); i++) {
CartItem item = items.get(i);
Presupuesto pCart = item.getPresupuesto();
// Asegurarnos de trabajar con la entidad gestionada por JPA
Presupuesto p = presupuestoRepo.findById(pCart.getId())
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + pCart.getId()));
Map<String, Object> data_to_send = presupuestoService.toSkApiRequest(p, true);
data_to_send.put("createPedido", 0);
// 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"));
presupuestoDireccionesRequest.put(p.getId().toString(), direcciones_presupuesto);
Map<String, Object> result = skApiClient.savePresupuesto(data_to_send);
if (result.containsKey("error")) {
System.out.println("Error al guardar presupuesto en SK");
System.out.println("-------------------------");
System.out.println(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,
presupuestoDireccionesRequest,
dirFactId,
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<>();
if (cart.getOnlyOneShipment()) {
List<CartDireccion> direcciones = cart.getDirecciones().stream().limit(1).toList();
if (!direcciones.isEmpty()) {
if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
presupuesto.getSelectedTirada(),
presupuesto.getPeso(),
direcciones.get(0).getIsPalets(),
false));
direccionesPresupuesto.add(direcciones.get(0).toSkMapDepositoLegal());
}
else {
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));
}
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;
}
} else {
List<CartDireccion> direcciones = cart.getDirecciones().stream()
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(presupuesto.getId()))
.toList();
for (CartDireccion cd : direcciones) {
// 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

View File

@ -13,9 +13,13 @@ import com.imprimelibros.erp.redsys.RedsysService.RedsysNotification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.payments.repo.WebhookEventRepository;
import com.imprimelibros.erp.pedidos.Pedido;
import com.imprimelibros.erp.pedidos.PedidoLinea;
import com.imprimelibros.erp.pedidos.PedidoService;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@Service
@ -28,18 +32,55 @@ public class PaymentService {
private final WebhookEventRepository webhookEventRepo;
private final ObjectMapper om = new ObjectMapper();
private final CartService cartService;
private final PedidoService pedidoService;
public PaymentService(PaymentRepository payRepo,
PaymentTransactionRepository txRepo,
RefundRepository refundRepo,
RedsysService redsysService,
WebhookEventRepository webhookEventRepo, CartService cartService) {
WebhookEventRepository webhookEventRepo,
CartService cartService,
PedidoService pedidoService) {
this.payRepo = payRepo;
this.txRepo = txRepo;
this.refundRepo = refundRepo;
this.redsysService = redsysService;
this.webhookEventRepo = webhookEventRepo;
this.cartService = cartService;
this.pedidoService = pedidoService;
}
public Payment findFailedPaymentByOrderId(Long orderId) {
return payRepo.findByOrderIdAndStatus(orderId, PaymentStatus.failed)
.orElse(null);
}
public Map<String, Long> getPaymentTransactionData(Long paymentId) {
PaymentTransaction tx = txRepo.findByPaymentIdAndType(
paymentId,
PaymentTransactionType.CAPTURE)
.orElse(null);
if (tx == null) {
return null;
}
String resp_payload = tx.getResponsePayload();
try {
ObjectMapper om = new ObjectMapper();
var node = om.readTree(resp_payload);
Long cartId = null;
Long dirFactId = null;
if (node.has("cartId")) {
cartId = node.get("cartId").asLong();
}
if (node.has("dirFactId")) {
dirFactId = node.get("dirFactId").asLong();
}
return Map.of(
"cartId", cartId,
"dirFactId", dirFactId);
} catch (Exception e) {
return null;
}
}
/**
@ -47,14 +88,15 @@ public class PaymentService {
* oficial (ApiMacSha256).
*/
@Transactional
public FormPayload createRedsysPayment(Long cartId, Long dirFactId, Long amountCents, String currency, String method)
public FormPayload createRedsysPayment(Long cartId, Long dirFactId, Long amountCents, String currency, String method, Long orderId)
throws Exception {
Payment p = new Payment();
p.setOrderId(null);
p.setOrderId(orderId);
Cart cart = this.cartService.findById(cartId);
if (cart != null && cart.getUserId() != null) {
p.setUserId(cart.getUserId());
this.cartService.lockCartById(cartId);
}
p.setCurrency(currency);
p.setAmountTotalCents(amountCents);
@ -62,10 +104,6 @@ public class PaymentService {
p.setStatus(PaymentStatus.requires_payment_method);
p = payRepo.saveAndFlush(p);
// ANTES:
// String dsOrder = String.format("%012d", p.getId());
// AHORA: timestamp
long now = System.currentTimeMillis();
String dsOrder = String.format("%012d", now % 1_000_000_000_000L);
@ -207,16 +245,12 @@ public class PaymentService {
p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents);
p.setAuthorizedAt(LocalDateTime.now());
p.setCapturedAt(LocalDateTime.now());
pedidoService.setOrderAsPaid(p.getOrderId());
} else {
p.setStatus(PaymentStatus.failed);
p.setFailedAt(LocalDateTime.now());
}
if (authorized) {
Long orderId = processOrder(notif.cartId, notif.dirFactId, locale);
if (orderId != null) {
p.setOrderId(orderId);
}
pedidoService.markPedidoAsPaymentDenied(p.getOrderId());
}
payRepo.save(p);
@ -311,7 +345,7 @@ public class PaymentService {
}
@Transactional
public Payment createBankTransferPayment(Long cartId, Long dirFactId, long amountCents, String currency) {
public Payment createBankTransferPayment(Long cartId, Long dirFactId, long amountCents, String currency, Locale locale, Long orderId) {
Payment p = new Payment();
p.setOrderId(null);
@ -326,6 +360,9 @@ public class PaymentService {
p.setAmountTotalCents(amountCents);
p.setGateway("bank_transfer");
p.setStatus(PaymentStatus.requires_action); // pendiente de ingreso
if (orderId != null) {
p.setOrderId(orderId);
}
p = payRepo.save(p);
// Crear transacción pendiente
@ -406,13 +443,17 @@ public class PaymentService {
// ignorar
}
// 4) Procesar el pedido asociado al carrito (si existe)
if (cartId != null) {
Long orderId = processOrder(cartId, dirFactId, locale);
// 4) Procesar el pedido asociado al carrito (si existe) o marcar el pedido como pagado
if(p.getOrderId() != null) {
pedidoService.setOrderAsPaid(p.getOrderId());
}
/*else if (cartId != null) {
// Se procesa el pedido dejando el estado calculado en processOrder
Long orderId = processOrder(cartId, dirFactId, locale, null);
if (orderId != null) {
p.setOrderId(orderId);
}
}
}*/
payRepo.save(p);
}
@ -508,30 +549,5 @@ public class PaymentService {
return code >= 0 && code <= 99;
}
/**
* Procesa el pedido asociado al carrito:
* - bloquea el carrito
* - crea el pedido a partir del carrito
*
*/
@Transactional
private Long processOrder(Long cartId, Long dirFactId, Locale locale) {
Cart cart = this.cartService.findById(cartId);
if (cart != null) {
// Bloqueamos el carrito
this.cartService.lockCartById(cart.getId());
// Creamos el pedido
Long orderId = this.cartService.crearPedido(cart.getId(), dirFactId, locale);
if (orderId == null) {
return null;
} else {
// envio de correo de confirmacion de pedido podria ir aqui
return orderId;
}
}
return null;
}
}

View File

@ -2,10 +2,13 @@
package com.imprimelibros.erp.payments.repo;
import com.imprimelibros.erp.payments.model.Payment;
import com.imprimelibros.erp.payments.model.PaymentStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface PaymentRepository extends JpaRepository<Payment, Long> {
Optional<Payment> findByGatewayAndGatewayOrderId(String gateway, String gatewayOrderId);
Optional<Payment> findByOrderIdAndStatus(Long orderId, PaymentStatus status);
}

View File

@ -14,6 +14,10 @@ import java.util.Optional;
public interface PaymentTransactionRepository extends JpaRepository<PaymentTransaction, Long>, JpaSpecificationExecutor<PaymentTransaction> {
List<PaymentTransaction> findByGatewayTransactionId(String gatewayTransactionId);
Optional<PaymentTransaction> findByIdempotencyKey(String idempotencyKey);
Optional<PaymentTransaction> findByPaymentIdAndType(
Long paymentId,
PaymentTransactionType type
);
Optional<PaymentTransaction> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc(
Long paymentId,
PaymentTransactionType type,

View File

@ -10,12 +10,15 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
public class PedidoLinea {
public enum Estado {
aprobado("pedido.estado.aprobado", 1),
maquetacion("pedido.estado.maquetacion", 2),
haciendo_ferro("pedido.estado.haciendo_ferro", 3),
ferro_cliente("pedido.estado.ferro_cliente", 4),
produccion("pedido.estado.produccion", 5),
cancelado("pedido.estado.cancelado", 6);
pendiente_pago("pedido.estado.pendiente_pago", 1),
procesando_pago("pedido.estado.procesando_pago", 2),
denegado_pago("pedido.estado.denegado_pago", 3),
aprobado("pedido.estado.aprobado", 4),
maquetacion("pedido.estado.maquetacion", 5),
haciendo_ferro("pedido.estado.haciendo_ferro", 6),
ferro_cliente("pedido.estado.ferro_cliente", 7),
produccion("pedido.estado.produccion", 8),
cancelado("pedido.estado.cancelado", 9);
private final String messageKey;
private final int priority;

View File

@ -11,6 +11,10 @@ import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.cart.Cart;
import com.imprimelibros.erp.cart.CartDireccion;
import com.imprimelibros.erp.cart.CartItem;
import com.imprimelibros.erp.cart.CartService;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.direcciones.Direccion;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
@ -30,10 +34,11 @@ public class PedidoService {
private final DireccionService direccionService;
private final UserService userService;
private final PresupuestoService presupuestoService;
private final CartService cartService;
public PedidoService(PedidoRepository pedidoRepository, PedidoLineaRepository pedidoLineaRepository,
PresupuestoRepository presupuestoRepository, PedidoDireccionRepository pedidoDireccionRepository,
DireccionService direccionService, UserService userService, PresupuestoService presupuestoService) {
DireccionService direccionService, UserService userService, PresupuestoService presupuestoService, CartService cartService) {
this.pedidoRepository = pedidoRepository;
this.pedidoLineaRepository = pedidoLineaRepository;
this.presupuestoRepository = presupuestoRepository;
@ -41,48 +46,22 @@ public class PedidoService {
this.direccionService = direccionService;
this.userService = userService;
this.presupuestoService = presupuestoService;
this.cartService = cartService;
}
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;
}
/**
* 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> presupuestoDirecciones,
Long cartId,
Long direccionFacturacionId,
Map<String, Object> cartSummaryRaw,
String proveedor,
String proveedorRef,
Long userId) {
String proveedorRef) {
Pedido pedido = new Pedido();
Cart cart = cartService.getCartById(cartId);
Map<String, Object> cartSummaryRaw = cartService.getCartSummaryRaw(cart, Locale.getDefault());
// 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));
@ -92,10 +71,13 @@ public class PedidoService {
pedido.setTotal((Double) cartSummaryRaw.getOrDefault("total", 0.0d));
// Proveedor
if(proveedor != null && proveedorRef != null) {
pedido.setProveedor(proveedor);
pedido.setProveedorRef(proveedorRef);
}
// Auditoría mínima
Long userId = cart.getUserId();
pedido.setCreatedBy(userService.findById(userId));
pedido.setCreatedAt(Instant.now());
pedido.setDeleted(false);
@ -103,8 +85,36 @@ public class PedidoService {
pedido.setUpdatedBy(userService.findById(userId));
// Guardamos el pedido
Pedido saved = pedidoRepository.save(pedido);
Pedido pedidoGuardado = pedidoRepository.save(pedido);
List<CartItem> items = cart.getItems();
for (Integer i = 0; i < items.size(); i++) {
CartItem item = items.get(i);
Presupuesto pCart = item.getPresupuesto();
// Asegurarnos de trabajar con la entidad gestionada por JPA
Presupuesto p = presupuestoRepository.findById(pCart.getId())
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + pCart.getId()));
p.setEstado(Presupuesto.Estado.aceptado);
presupuestoRepository.save(p);
PedidoLinea linea = new PedidoLinea();
linea.setPedido(pedidoGuardado);
linea.setPresupuesto(p);
linea.setCreatedBy(userId);
linea.setCreatedAt(LocalDateTime.now());
linea.setEstado(PedidoLinea.Estado.pendiente_pago);
linea.setEstadoManual(false);
pedidoLineaRepository.save(linea);
// Guardar las direcciones asociadas a la línea del pedido
Map<String, Object> direcciones_presupuesto = this.getDireccionesPresupuesto(cart, p);
saveDireccionesPedidoLinea(direcciones_presupuesto, pedidoGuardado, linea, direccionFacturacionId);
}
/*
// Crear líneas del pedido
for (Long presupuestoId : presupuestoIds) {
Presupuesto presupuesto = presupuestoRepository.getReferenceById(presupuestoId);
@ -114,7 +124,11 @@ public class PedidoService {
linea.setPresupuesto(presupuesto);
linea.setCreatedBy(userId);
linea.setCreatedAt(LocalDateTime.now());
if(estadoInicial != null){
linea.setEstado(estadoInicial);
} else {
linea.setEstado(getEstadoInicial(presupuesto));
}
linea.setEstadoManual(false);
pedidoLineaRepository.save(linea);
@ -125,10 +139,175 @@ public class PedidoService {
saveDireccionesPedidoLinea(direcciones, saved, linea, direccionFacturacionId);
}
}
*/
return saved;
return pedidoGuardado;
}
public Boolean markPedidoAsProcesingPayment(Long pedidoId){
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
if(pedido == null){
return false;
}
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
for (PedidoLinea linea : lineas) {
linea.setEstado(PedidoLinea.Estado.procesando_pago);
pedidoLineaRepository.save(linea);
}
return true;
}
public Boolean markPedidoAsPaymentDenied(Long pedidoId){
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
if(pedido == null){
return false;
}
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
for (PedidoLinea linea : lineas) {
linea.setEstado(PedidoLinea.Estado.denegado_pago);
pedidoLineaRepository.save(linea);
}
return true;
}
public Boolean markPedidoAsPaid(Long pedidoId){
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
if(pedido == null){
return false;
}
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
for (PedidoLinea linea : lineas) {
linea.setEstado(this.getEstadoInicial(linea.getPresupuesto()));
pedidoLineaRepository.save(linea);
// Save presupuesto in SK
}
// Save pedido in SK
return true;
}
public Pedido findById(Long pedidoId){
return pedidoRepository.findById(pedidoId).orElse(null);
}
/*
@Transactional
public Long crearPedido(Long cartId, Long dirFactId, Locale locale) {
return crearPedido(cartId, dirFactId, locale, null);
}
@Transactional
// Crear pedido interno (no en el proveedor) a partir del carrito
public Long crearPedido(Long cartId, Long dirFactId, Locale locale, PedidoLinea.Estado estadoInicial) {
Cart cart = cartService.getCartById(cartId);
List<CartItem> items = cart.getItems();
List<Map<String, Object>> presupuestoRequests = new ArrayList<>();
Map<String, Object> presupuestoDireccionesRequest = new HashMap<>();
List<Long> presupuestoIds = new ArrayList<>();
for (Integer i = 0; i < items.size(); i++) {
CartItem item = items.get(i);
Presupuesto pCart = item.getPresupuesto();
// Asegurarnos de trabajar con la entidad gestionada por JPA
Presupuesto p = presupuestoRepository.findById(pCart.getId())
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + pCart.getId()));
/*Map<String, Object> data_to_send = presupuestoService.toSkApiRequest(p, true);
data_to_send.put("createPedido", 0);
// 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"));
presupuestoDireccionesRequest.put(p.getId().toString(), direcciones_presupuesto);
Map<String, Object> result = skApiClient.savePresupuesto(data_to_send);
if (result.containsKey("error")) {
System.out.println("Error al guardar presupuesto en SK");
System.out.println("-------------------------");
System.out.println(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,
presupuestoDireccionesRequest,
dirFactId,
this.getCartSummaryRaw(cart, locale),
"Safekat",
String.valueOf(pedidoId),
cart.getUserId(),
estadoInicial);
return pedidoInterno.getId();
}
}
*/
/** Lista de los items del pedido preparados para la vista*/
@Transactional
public List<Map<String, Object>> getLineas(Long pedidoId, Locale locale) {
@ -160,17 +339,119 @@ public class PedidoService {
return pedidoDireccionRepository.findByPedidoLinea_Id(pedidoLineaId);
}
public Boolean setOrderAsPaid(Long pedidoId) {
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
if (pedido == null) {
return false;
}
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
for (PedidoLinea linea : lineas) {
if (linea.getEstado() == Estado.pendiente_pago) {
Presupuesto presupuesto = linea.getPresupuesto();
linea.setEstado(getEstadoInicial(presupuesto));
pedidoLineaRepository.save(linea);
}
}
return true;
}
/***************************
* MÉTODOS PRIVADOS
***************************/
// Obtener las direcciones de envío asociadas a un presupuesto en el carrito
private Map<String, Object> getDireccionesPresupuesto(Cart cart, Presupuesto presupuesto) {
List<Map<String, Object>> direccionesPresupuesto = new ArrayList<>();
List<Map<String, Object>> direccionesPrueba = new ArrayList<>();
if (cart.getOnlyOneShipment()) {
List<CartDireccion> direcciones = cart.getDirecciones().stream().limit(1).toList();
if (!direcciones.isEmpty()) {
if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
presupuesto.getSelectedTirada(),
presupuesto.getPeso(),
direcciones.get(0).getIsPalets(),
false));
direccionesPresupuesto.add(direcciones.get(0).toSkMapDepositoLegal());
}
else {
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));
}
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;
}
} else {
List<CartDireccion> direcciones = cart.getDirecciones().stream()
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(presupuesto.getId()))
.toList();
for (CartDireccion cd : direcciones) {
// 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;
}
@Transactional
private void saveDireccionesPedidoLinea(
Map<String, Map<String, Object>> direcciones,
Map<String, Object> direcciones,
Pedido pedido,
PedidoLinea linea, Long direccionFacturacionId) {
// direccion prueba
if (direcciones.containsKey("direccionesFP1")) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> fp1 = (Map<String, Object>) direcciones.get("direccionesFP1");
@SuppressWarnings("unchecked")
PedidoDireccion direccion = saveDireccion(

View File

@ -166,9 +166,16 @@ public class PedidosController {
return text;
})
.add("actions", pedido -> {
return "<span class=\'badge bg-success btn-view \' data-id=\'" + pedido.getId()
String data = "<span class=\'badge bg-success btn-view \' data-id=\'" + pedido.getId()
+ "\' style=\'cursor: pointer;\'>"
+ messageSource.getMessage("app.view", null, locale) + "</span>";
List<PedidoLinea> lineas = repoPedidoLinea.findByPedidoId(pedido.getId());
boolean hasDenegadoPago = lineas.stream()
.anyMatch(linea -> PedidoLinea.Estado.denegado_pago.equals(linea.getEstado()));
if (hasDenegadoPago) {
data += " <span class='badge bg-danger btn-pay' data-amount='" + pedido.getTotal() + "' data-id=\\'" + pedido.getId() + "\\' style='cursor: pointer;'>" + messageSource.getMessage("app.pay", null, locale) + "</span>";
}
return data;
})
.where(base)
.toJson(total);

View File

@ -1,10 +1,15 @@
package com.imprimelibros.erp.redsys;
import com.imprimelibros.erp.cart.Cart;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.payments.PaymentService;
import com.imprimelibros.erp.payments.model.Payment;
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
import com.imprimelibros.erp.pedidos.Pedido;
import com.imprimelibros.erp.pedidos.PedidoService;
import com.imprimelibros.erp.redsys.RedsysService.FormPayload;
import groovy.util.logging.Log;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -27,6 +32,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
@Controller
@ -37,13 +43,16 @@ public class RedsysController {
private final MessageSource messageSource;
private final SpringTemplateEngine templateEngine;
private final ServletContext servletContext;
private final PedidoService pedidoService;
public RedsysController(PaymentService paymentService, MessageSource messageSource,
SpringTemplateEngine templateEngine, ServletContext servletContext) {
SpringTemplateEngine templateEngine, ServletContext servletContext,
PedidoService pedidoService) {
this.paymentService = paymentService;
this.messageSource = messageSource;
this.templateEngine = templateEngine;
this.servletContext = servletContext;
this.pedidoService = pedidoService;
}
@PostMapping(value = "/crear", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ -55,9 +64,15 @@ public class RedsysController {
HttpServletResponse response, Locale locale)
throws Exception {
// Creamos el pedido inteno
Pedido order = pedidoService.crearPedido(cartId, dirFactId, null, null);
if ("bank-transfer".equalsIgnoreCase(method)) {
// 1) Creamos el Payment interno SIN orderId (null)
Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR");
Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR", locale, order.getId());
pedidoService.markPedidoAsProcesingPayment(order.getId());
// 1⃣ Crear la "aplicación" web de Thymeleaf (Jakarta)
JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext);
@ -89,7 +104,102 @@ public class RedsysController {
}
// Tarjeta o Bizum (Redsys)
FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method);
FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method, order.getId());
String html = """
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>
<body onload="document.forms[0].submit()">
<form action="%s" method="post">
<input type="hidden" name="Ds_SignatureVersion" value="%s"/>
<input type="hidden" name="Ds_MerchantParameters" value="%s"/>
<input type="hidden" name="Ds_Signature" value="%s"/>
<input type="hidden" name="cartId" value="%d"/>
<noscript>
<p>Haz clic en pagar para continuar</p>
<button type="submit">Pagar</button>
</noscript>
</form>
</body></html>
""".formatted(
form.action(),
form.signatureVersion(),
form.merchantParameters(),
form.signature(), cartId);
byte[] body = html.getBytes(StandardCharsets.UTF_8);
return ResponseEntity.ok()
.contentType(MediaType.TEXT_HTML)
.body(body);
}
@PostMapping(value = "/reintentar", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody
public ResponseEntity<byte[]> reintentarPago(@RequestParam("amountCents") Long amountCents,
@RequestParam("method") String method, @RequestParam("orderId") Long orderId,
HttpServletRequest request,
HttpServletResponse response, Locale locale)
throws Exception {
// Creamos el pedido inteno
Pedido order = pedidoService.findById(orderId);
// Find the payment with orderId = order.getId() and status = failed
Payment failedPayment = paymentService.findFailedPaymentByOrderId(order.getId());
if (failedPayment == null) {
throw new Exception("No se encontró un pago fallido para el pedido " + order.getId());
}
Long cartId = null;
Long dirFactId = null;
// Find payment transaction details from failedPayment if needed
try{
Map<String, Long> transactionDetails = paymentService.getPaymentTransactionData(failedPayment.getId());
cartId = transactionDetails.get("cartId");
dirFactId = transactionDetails.get("dirFactId");
} catch (Exception e) {
throw new Exception("No se pudieron obtener los detalles de la transacción para el pago " + failedPayment.getId());
}
if ("bank-transfer".equalsIgnoreCase(method)) {
// 1) Creamos el Payment interno SIN orderId (null)
Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR", locale, order.getId());
pedidoService.markPedidoAsProcesingPayment(order.getId());
// 1⃣ Crear la "aplicación" web de Thymeleaf (Jakarta)
JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext);
// 2⃣ Construir el intercambio web desde request/response
response.setContentType("text/html;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
IWebExchange exchange = app.buildExchange(request, response);
// 3⃣ Crear el contexto WebContext con Locale
WebContext ctx = new WebContext(exchange, locale);
String importeFormateado = Utils.formatCurrency(amountCents / 100.0, locale);
ctx.setVariable("importe", importeFormateado);
ctx.setVariable("concepto", "TRANSF-" + p.getOrderId());
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
boolean isAuth = auth != null
&& auth.isAuthenticated()
&& !(auth instanceof AnonymousAuthenticationToken);
ctx.setVariable("isAuth", isAuth);
// 3) Renderizamos la plantilla a HTML
String html = templateEngine.process("imprimelibros/pagos/transfer", ctx);
byte[] body = html.getBytes(StandardCharsets.UTF_8);
return ResponseEntity.ok()
.contentType(MediaType.TEXT_HTML)
.body(body);
}
// Tarjeta o Bizum (Redsys)
FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method, order.getId());
String html = """
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>

View File

@ -0,0 +1,32 @@
databaseChangeLog:
- changeSet:
id: 0019-add-estados-pago-to-pedidos-lineas
author: jjo
changes:
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: >
enum(
'pendiente_pago',
'procesando_pago',
'aprobado',
'maquetacion',
'haciendo_ferro',
'produccion',
'terminado',
'cancelado'
)
rollback:
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: >
enum(
'aprobado',
'maquetacion',
'haciendo_ferro',
'produccion',
'terminado',
'cancelado'
)

View File

@ -0,0 +1,35 @@
databaseChangeLog:
- changeSet:
id: 0020-add-estados-pago-to-pedidos-lineas-2
author: jjo
changes:
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: >
enum(
'pendiente_pago',
'procesando_pago',
'denegado_pago',
'aprobado',
'maquetacion',
'haciendo_ferro',
'produccion',
'terminado',
'cancelado'
)
rollback:
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: >
enum(
'pendiente_pago',
'procesando_pago',
'aprobado',
'maquetacion',
'haciendo_ferro',
'produccion',
'terminado',
'cancelado'
)

View File

@ -35,3 +35,7 @@ databaseChangeLog:
file: db/changelog/changesets/0017-add-fecha-entrega-to-pedidos-lineas.yml
- include:
file: db/changelog/changesets/0018-change-presupuesto-ch-3.yml
- include:
file: db/changelog/changesets/0019-add-estados-pago-to-pedidos-lineas.yml
- include:
file: db/changelog/changesets/0020-add-estados-pago-to-pedidos-lineas-2.yml

View File

@ -11,6 +11,7 @@ app.back=Volver
app.eliminar=Eliminar
app.imprimir=Imprimir
app.view=Ver
app.pay=Pagar
app.acciones.siguiente=Siguiente
app.acciones.anterior=Anterior

View File

@ -17,6 +17,9 @@ checkout.success.payment=Pago realizado con éxito. Gracias por su compra.
checkout.make-payment=Realizar el pago
checkout.authorization-required=Certifico que tengo los derechos para imprimir los archivos incluidos en mi pedido y me hago responsable en caso de reclamación de los mismos
pedido.estado.pendiente_pago=Pendiente de pago
pedido.estado.procesando_pago=Procesando pago
pedido.estado.denegado_pago=Pago denegado
pedido.estado.aprobado=Aprobado
pedido.estado.maquetacion=Maquetación
pedido.estado.haciendo_ferro=Haciendo ferro

View File

@ -5,4 +5,42 @@ $(() => {
window.location.href = url;
});
$(document).on('click', '.btn-pay', function () {
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
if (window.$ && csrfToken && csrfHeader) {
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader(csrfHeader, csrfToken);
}
});
}
let pedidoId = $(this).data('id');
let amount = $(this).data('amount');
$.ajax({
url: `/pagos/redsys/reintentar`,
method: 'POST',
data: {
amountCents: amount,
orderId: pedidoId
},
success: function (response) {
if (response && response.formHtml) {
$('body').append(response.formHtml);
$('#redsys-payment-form').submit();
} else {
alert('Error al procesar el pago. Por favor, inténtelo de nuevo.');
}
},
error: function () {
alert('Error al procesar el pago. Por favor, inténtelo de nuevo.');
}
});
});
})

View File

@ -18,7 +18,7 @@ public class envioCarroTest {
void addPedido(){
Locale locale = Locale.forLanguageTag("es-ES");
cartService.crearPedido(carritoId, null, locale);
//cartService.crearPedido(carritoId, null, locale);
}

View File

@ -17,10 +17,10 @@ public class savePresupuestosTest {
@Test
void testGuardarPresupuesto() {
Locale locale = new Locale("es", "ES");
Long resultado = cartService.crearPedido(9L, null, locale);
//Long resultado = cartService.crearPedido(9L, null, locale);
System.out.println("📦 Presupuesto guardado:");
System.out.println(resultado);
//System.out.println(resultado);
// Aquí irían las aserciones para verificar que el presupuesto se guardó correctamente
}