Compare commits

...

5 Commits

Author SHA1 Message Date
84a822db22 Merge branch 'hotfix/save_with_DL' into 'main'
arreglado problema pago bizum

See merge request jjimenez/erp-imprimelibros!24
2025-11-15 09:59:54 +00:00
69f27df98b arreglados varios temas además del DL (redsys, etc) 2025-11-15 10:59:05 +01:00
6bd36dbe8c arreglado problema pago bizum
A
2025-11-14 18:35:06 +01:00
3086a6de41 añadido test 2025-11-14 13:55:29 +01:00
4f1b3f2bb6 haciendo pruebas 2025-11-13 21:40:08 +01:00
17 changed files with 850 additions and 2793 deletions

File diff suppressed because it is too large Load Diff

View File

@ -445,7 +445,6 @@ public class CartService {
cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE); cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE);
} }
@Transactional @Transactional
public Long crearPedido(Long cartId, Locale locale) { public Long crearPedido(Long cartId, Locale locale) {
@ -483,7 +482,9 @@ public class CartService {
Map<String, Object> result = skApiClient.savePresupuesto(data_to_send); Map<String, Object> result = skApiClient.savePresupuesto(data_to_send);
if (result.containsKey("error")) { if (result.containsKey("error")) {
System.out.println("Error al guardar presupuesto en SK: " + result.get("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: // decide si seguir con otros items o abortar:
// continue; o bien throw ... // continue; o bien throw ...
continue; continue;
@ -538,11 +539,23 @@ public class CartService {
if (cart.getOnlyOneShipment()) { if (cart.getOnlyOneShipment()) {
List<CartDireccion> direcciones = cart.getDirecciones().stream().limit(1).toList(); List<CartDireccion> direcciones = cart.getDirecciones().stream().limit(1).toList();
if (!direcciones.isEmpty()) { if (!direcciones.isEmpty()) {
direccionesPresupuesto.add(direcciones.get(0).toSkMap( if (presupuesto.getServiciosJson() != null
presupuesto.getSelectedTirada(), && presupuesto.getServiciosJson().contains("deposito-legal")) {
presupuesto.getPeso(), direccionesPresupuesto.add(direcciones.get(0).toSkMap(
direcciones.get(0).getIsPalets(), presupuesto.getSelectedTirada()-4,
false)); 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 if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().contains("ejemplar-prueba")) { && presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
direccionesPrueba.add(direcciones.get(0).toSkMap( direccionesPrueba.add(direcciones.get(0).toSkMap(
@ -551,10 +564,7 @@ public class CartService {
false, false,
true)); true));
} }
if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
direccionesPresupuesto.add(direcciones.get(0).toSkMapDepositoLegal());
}
Map<String, Object> direccionesRet = new HashMap<>(); Map<String, Object> direccionesRet = new HashMap<>();
direccionesRet.put("direcciones", direccionesPresupuesto); direccionesRet.put("direcciones", direccionesPresupuesto);
if (!direccionesPrueba.isEmpty()) if (!direccionesPrueba.isEmpty())

View File

@ -2,8 +2,6 @@ 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

@ -32,7 +32,6 @@ import com.imprimelibros.erp.users.UserDao;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@Controller @Controller
@RequestMapping("/pagos") @RequestMapping("/pagos")

View File

@ -37,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;
} }
@ -83,7 +83,8 @@ public class PaymentService {
} }
@Transactional @Transactional
public void handleRedsysNotification(String dsSignature, String dsMerchantParameters, Locale locale) 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.
@ -169,13 +170,20 @@ public class PaymentService {
? PaymentTransactionStatus.succeeded ? PaymentTransactionStatus.succeeded
: PaymentTransactionStatus.failed); : PaymentTransactionStatus.failed);
Object authCode = notif.raw.get("Ds_AuthorisationCode");
String gatewayTxId = null; String gatewayTxId = null;
if (authCode != null) { // 1) Si es Bizum y tenemos Ds_Bizum_IdOper, úsalo como ID único
String trimmed = String.valueOf(authCode).trim(); if (notif.isBizum()
// Redsys devuelve " " (espacios) cuando NO hay código de autorización. && notif.bizumIdOper != null
// Eso lo consideramos "sin ID" → null, para no chocar con el índice único. && !notif.bizumIdOper.isBlank()) {
if (!trimmed.isEmpty()) {
gatewayTxId = notif.bizumIdOper.trim();
// 2) Si no es Bizum, intenta usar Ds_AuthorisationCode
} else if (notif.authorisationCode != null) {
String trimmed = notif.authorisationCode.trim();
// Redsys suele mandar "000000" para Bizum; por si acaso también lo filtramos
if (!trimmed.isEmpty() && !"000000".equals(trimmed)) {
gatewayTxId = trimmed; gatewayTxId = trimmed;
} }
} }
@ -187,7 +195,14 @@ public class PaymentService {
txRepo.save(tx); txRepo.save(tx);
if (authorized) { if (authorized) {
p.setAuthorizationCode(tx.getGatewayTransactionId()); if (notif.isBizum()) {
p.setAuthorizationCode(null); // o "000000" si te interesa mostrarlo
} else if (notif.authorisationCode != null
&& !"000000".equals(notif.authorisationCode.trim())
&& !notif.authorisationCode.isBlank()) {
p.setAuthorizationCode(notif.authorisationCode.trim());
}
p.setStatus(PaymentStatus.captured); p.setStatus(PaymentStatus.captured);
p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents); p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents);
p.setAuthorizedAt(LocalDateTime.now()); p.setAuthorizedAt(LocalDateTime.now());
@ -258,6 +273,14 @@ public class PaymentService {
throw new IllegalStateException("Error al solicitar la devolución a Redsys", e); throw new IllegalStateException("Error al solicitar la devolución a Redsys", e);
} }
// 🔧 NORMALIZAR ANTES DE GUARDAR
if (gatewayRefundId != null) {
gatewayRefundId = gatewayRefundId.trim();
if (gatewayRefundId.isEmpty() || "000000".equals(gatewayRefundId)) {
gatewayRefundId = null; // → múltiples NULL NO rompen el UNIQUE
}
}
PaymentTransaction tx = new PaymentTransaction(); PaymentTransaction tx = new PaymentTransaction();
tx.setPayment(p); tx.setPayment(p);
tx.setType(PaymentTransactionType.REFUND); tx.setType(PaymentTransactionType.REFUND);
@ -459,17 +482,16 @@ public class PaymentService {
*/ */
@Transactional @Transactional
private Boolean processOrder(Long cartId, Locale locale) { 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());
// Creamos el pedido // Creamos el pedido
Long orderId = this.cartService.crearPedido(cart.getId(), locale); Long orderId = this.cartService.crearPedido(cart.getId(), locale);
if(orderId == null){ if (orderId == null) {
return false; return false;
} } else {
else{
// envio de correo de confirmacion de pedido podria ir aqui // envio de correo de confirmacion de pedido podria ir aqui
} }

View File

@ -6,9 +6,6 @@ import java.time.LocalDateTime;
@Entity @Entity
@Table( @Table(
name = "payment_transactions", name = "payment_transactions",
uniqueConstraints = {
@UniqueConstraint(name = "uq_tx_gateway_txid", columnNames = {"gateway_transaction_id"})
},
indexes = { indexes = {
@Index(name = "idx_tx_pay", columnList = "payment_id"), @Index(name = "idx_tx_pay", columnList = "payment_id"),
@Index(name = "idx_tx_type_status", columnList = "type,status"), @Index(name = "idx_tx_type_status", columnList = "type,status"),

View File

@ -8,10 +8,11 @@ import com.imprimelibros.erp.payments.model.PaymentTransactionType;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface PaymentTransactionRepository extends JpaRepository<PaymentTransaction, Long>, JpaSpecificationExecutor<PaymentTransaction> { public interface PaymentTransactionRepository extends JpaRepository<PaymentTransaction, Long>, JpaSpecificationExecutor<PaymentTransaction> {
Optional<PaymentTransaction> findByGatewayTransactionId(String gatewayTransactionId); List<PaymentTransaction> findByGatewayTransactionId(String gatewayTransactionId);
Optional<PaymentTransaction> findByIdempotencyKey(String idempotencyKey); Optional<PaymentTransaction> findByIdempotencyKey(String idempotencyKey);
Optional<PaymentTransaction> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc( Optional<PaymentTransaction> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc(
Long paymentId, Long paymentId,

View File

@ -9,7 +9,6 @@ import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository; import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
@Service @Service
public class PedidoService { public class PedidoService {

View File

@ -189,9 +189,9 @@ public class RedsysController {
try { try {
String idem = "refund-" + paymentId + "-" + amountCents + "-" + UUID.randomUUID(); String idem = "refund-" + paymentId + "-" + amountCents + "-" + UUID.randomUUID();
paymentService.refundViaRedsys(paymentId, amountCents, idem); paymentService.refundViaRedsys(paymentId, amountCents, idem);
return ResponseEntity.ok("{success:true}"); return ResponseEntity.ok("{\"success\":true}");
} catch (Exception e) { } catch (Exception e) {
return ResponseEntity.badRequest().body("{success:false, error: '" + e.getMessage() + "'}"); return ResponseEntity.badRequest().body("{\"success\":false, \"error\": \"" + e.getMessage() + "\"}");
} }
} }
} }

View File

@ -195,6 +195,9 @@ public class RedsysService {
public final long amountCents; public final long amountCents;
public final String currency; public final String currency;
public final Long cartId; public final Long cartId;
public final String processedPayMethod; // Ds_ProcessedPayMethod
public final String bizumIdOper; // Ds_Bizum_IdOper
public final String authorisationCode; // Ds_AuthorisationCode
public RedsysNotification(Map<String, Object> raw) { public RedsysNotification(Map<String, Object> raw) {
this.raw = raw; this.raw = raw;
@ -203,6 +206,9 @@ public class RedsysService {
this.currency = str(raw.get("Ds_Currency")); this.currency = str(raw.get("Ds_Currency"));
this.amountCents = parseLongSafe(raw.get("Ds_Amount")); this.amountCents = parseLongSafe(raw.get("Ds_Amount"));
this.cartId = extractCartId(raw.get("Ds_MerchantData")); this.cartId = extractCartId(raw.get("Ds_MerchantData"));
this.processedPayMethod = str(raw.get("Ds_ProcessedPayMethod"));
this.bizumIdOper = str(raw.get("Ds_Bizum_IdOper"));
this.authorisationCode = str(raw.get("Ds_AuthorisationCode"));
} }
private static Long extractCartId(Object merchantDataObj) { private static Long extractCartId(Object merchantDataObj) {
@ -231,6 +237,11 @@ public class RedsysService {
} }
} }
public boolean isBizum() {
// Redsys suele usar 68 para Bizum; ajustable si tu banco usa otro código.
return "68".equals(processedPayMethod);
}
private static String str(Object o) { private static String str(Object o) {
return o == null ? null : String.valueOf(o); return o == null ? null : String.valueOf(o);
} }
@ -245,7 +256,7 @@ public class RedsysService {
} }
/** /**
* Solicita a Redsys una devolución (TransactionType = 3) * Solicita a Redsys una devolución (TransactionType = 3)
* *
* @param order El mismo Ds_Merchant_Order que se usó en el cobro. * @param order El mismo Ds_Merchant_Order que se usó en el cobro.
* @param amountCents Importe en céntimos a devolver. * @param amountCents Importe en céntimos a devolver.
@ -308,14 +319,35 @@ public class RedsysService {
// Decodificar MerchantParameters de la respuesta // Decodificar MerchantParameters de la respuesta
Map<String, Object> decoded = decodeMerchantParametersToMap(dsMerchantParametersResp); Map<String, Object> decoded = decodeMerchantParametersToMap(dsMerchantParametersResp);
String dsResponse = String.valueOf(decoded.get("Ds_Response")); String dsResponse = String.valueOf(decoded.get("Ds_Response"));
if (!"0900".equals(dsResponse)) { if (dsResponse == null) {
throw new IllegalStateException("Respuesta Redsys refund sin Ds_Response");
}
int code;
try {
code = Integer.parseInt(dsResponse);
} catch (NumberFormatException e) {
throw new IllegalStateException("Código Ds_Response no numérico en refund: " + dsResponse, e);
}
// ✅ Consideramos OK: 099 (éxito típico) o 900 (0900)
boolean ok = (code >= 0 && code <= 99) || code == 900;
if (!ok) {
throw new IllegalStateException("Devolución rechazada, Ds_Response=" + dsResponse); throw new IllegalStateException("Devolución rechazada, Ds_Response=" + dsResponse);
} }
return String.valueOf(decoded.getOrDefault("Ds_AuthorisationCode", order)); // Devolvemos algún identificador razonable para la transacción de refund
Object authCodeObj = decoded.get("Ds_AuthorisationCode");
String authCode = authCodeObj != null ? String.valueOf(authCodeObj).trim() : null;
if (authCode == null || authCode.isEmpty()) {
// Fallback: usa el Ds_Order original como ID de refund
return order;
}
return authCode;
} }
} }

View File

@ -0,0 +1,31 @@
databaseChangeLog:
- changeSet:
id: 0012--drop-unique-gateway-txid-2
author: jjo
changes:
# 1) Eliminar el índice UNIQUE actual
- dropIndex:
indexName: uq_tx_gateway_txid
tableName: payment_transactions
# 2) Crear un índice normal (no único) sobre gateway_transaction_id
- createIndex:
indexName: idx_tx_gateway_txid
tableName: payment_transactions
columns:
- column:
name: gateway_transaction_id
rollback:
# Rollback: volver al índice UNIQUE como estaba antes
- dropIndex:
indexName: idx_tx_gateway_txid
tableName: payment_transactions
- createIndex:
indexName: uq_tx_gateway_txid
tableName: payment_transactions
unique: true
columns:
- column:
name: gateway_transaction_id

View File

@ -1,28 +0,0 @@
databaseChangeLog:
- changeSet:
id: 0012-drop-unique-tx-gateway
author: JJO
# ✅ Solo ejecuta el changeSet si existe la UNIQUE constraint
preConditions:
- onFail: MARK_RAN
- uniqueConstraintExists:
tableName: payment_transactions
constraintName: idx_payment_tx_gateway_txid
changes:
# 1⃣ Eliminar la UNIQUE constraint si existe
- dropIndex:
tableName: payment_transactions
indexName: idx_payment_tx_gateway_txid
rollback:
# 🔙 1) Eliminar el índice normal creado en este changeSet
- createIndex:
tableName: payment_transactions
indexName: idx_payment_tx_gateway_txid
columns:
- column:
name: gateway_transaction_id

View File

@ -0,0 +1,33 @@
databaseChangeLog:
- changeSet:
id: 0013-drop-unique-refund-gateway-id
author: jjo
changes:
# 1) Eliminar el índice UNIQUE actual sobre gateway_refund_id
- dropIndex:
indexName: uq_refund_gateway_id
tableName: refunds
# 2) Crear un índice normal (no único) sobre gateway_refund_id
- createIndex:
indexName: idx_refund_gateway_id
tableName: refunds
columns:
- column:
name: gateway_refund_id
rollback:
# Rollback: quitar el índice normal
- dropIndex:
indexName: idx_refund_gateway_id
tableName: refunds
# y restaurar el UNIQUE como estaba antes
- createIndex:
indexName: uq_refund_gateway_id
tableName: refunds
unique: true
columns:
- column:
name: gateway_refund_id

View File

@ -20,4 +20,8 @@ databaseChangeLog:
- include: - include:
file: db/changelog/changesets/0010-drop-unique-tx-gateway.yml file: db/changelog/changesets/0010-drop-unique-tx-gateway.yml
- include: - include:
file: db/changelog/changesets/0011-update-pedidos-presupuesto.yml file: db/changelog/changesets/0011-update-pedidos-presupuesto.yml
- include:
file: db/changelog/changesets/0012--drop-unique-gateway-txid-2.yml
- include:
file: db/changelog/changesets/0013-drop-unique-refund-gateway-id.yml

View File

@ -20,8 +20,8 @@ $(() => {
// Actualizar al cargar // Actualizar al cargar
updateCartCount(); updateCartCount();
// Si quieres refrescar cada 60s: // Si quieres refrescar cada 10s:
setInterval(updateCartCount, 60000); setInterval(updateCartCount, 10000);
// generate a custom event to update the cart count from other scripts // generate a custom event to update the cart count from other scripts
document.addEventListener("update-cart", updateCartCount); document.addEventListener("update-cart", updateCartCount);

View File

@ -24,6 +24,7 @@ $(() => {
orderCellsTop: true, orderCellsTop: true,
pageLength: 50, pageLength: 50,
lengthMenu: [10, 25, 50, 100, 500], lengthMenu: [10, 25, 50, 100, 500],
order: [[5, 'desc']], // Ordena por fecha por defecto
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' }, language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
responsive: true, responsive: true,
dom: 'lBrtip', dom: 'lBrtip',
@ -139,6 +140,7 @@ $(() => {
orderCellsTop: true, orderCellsTop: true,
pageLength: 50, pageLength: 50,
lengthMenu: [10, 25, 50, 100, 500], lengthMenu: [10, 25, 50, 100, 500],
order: [[5, 'desc']],
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' }, language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
responsive: true, responsive: true,
dom: 'lBrtip', dom: 'lBrtip',
@ -161,7 +163,7 @@ $(() => {
url: '/pagos/datatable/transferencias', url: '/pagos/datatable/transferencias',
method: 'GET', method: 'GET',
}, },
order: [[7, 'desc']], // Ordena por fecha por defecto order: [[6, 'desc']], // Ordena por fecha por defecto
columns: [ columns: [
{ data: 'client', name: 'client', orderable: true }, { data: 'client', name: 'client', orderable: true },
{ data: 'transfer_id', name: 'transfer_id', orderable: true }, { data: 'transfer_id', name: 'transfer_id', orderable: true },

View File

@ -0,0 +1,25 @@
package com.imprimelibros.erp.cart;
import java.util.Locale;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class envioCarroTest {
@Autowired
CartService cartService;
private final Long carritoId = 64L;
@Test
void addPedido(){
Locale locale = Locale.forLanguageTag("es-ES");
cartService.crearPedido(carritoId, locale);
}
}