Merge branch 'hotfix/save_with_DL' into 'main'

arreglado problema pago bizum

See merge request jjimenez/erp-imprimelibros!24
This commit is contained in:
2025-11-15 09:59:54 +00: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);
}
@Transactional
public Long crearPedido(Long cartId, Locale locale) {
@ -483,7 +482,9 @@ public class CartService {
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"));
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;
@ -538,11 +539,23 @@ public class CartService {
if (cart.getOnlyOneShipment()) {
List<CartDireccion> direcciones = cart.getDirecciones().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("deposito-legal")) {
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
presupuesto.getSelectedTirada()-4,
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(
@ -551,10 +564,7 @@ public class CartService {
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);
if (!direccionesPrueba.isEmpty())

View File

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

View File

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

View File

@ -37,7 +37,7 @@ public class PaymentService {
this.payRepo = payRepo;
this.txRepo = txRepo;
this.refundRepo = refundRepo;
this.redsysService = redsysService;
this.redsysService = redsysService;
this.webhookEventRepo = webhookEventRepo;
this.cartService = cartService;
}
@ -83,7 +83,8 @@ public class PaymentService {
}
@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
// y salimos.
@ -169,13 +170,20 @@ public class PaymentService {
? PaymentTransactionStatus.succeeded
: PaymentTransactionStatus.failed);
Object authCode = notif.raw.get("Ds_AuthorisationCode");
String gatewayTxId = null;
if (authCode != null) {
String trimmed = String.valueOf(authCode).trim();
// Redsys devuelve " " (espacios) cuando NO hay código de autorización.
// Eso lo consideramos "sin ID" → null, para no chocar con el índice único.
if (!trimmed.isEmpty()) {
// 1) Si es Bizum y tenemos Ds_Bizum_IdOper, úsalo como ID único
if (notif.isBizum()
&& notif.bizumIdOper != null
&& !notif.bizumIdOper.isBlank()) {
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;
}
}
@ -187,7 +195,14 @@ public class PaymentService {
txRepo.save(tx);
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.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents);
p.setAuthorizedAt(LocalDateTime.now());
@ -258,6 +273,14 @@ public class PaymentService {
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();
tx.setPayment(p);
tx.setType(PaymentTransactionType.REFUND);
@ -459,17 +482,16 @@ public class PaymentService {
*/
@Transactional
private Boolean processOrder(Long cartId, 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(), locale);
if(orderId == null){
if (orderId == null) {
return false;
}
else{
} else {
// envio de correo de confirmacion de pedido podria ir aqui
}

View File

@ -6,9 +6,6 @@ import java.time.LocalDateTime;
@Entity
@Table(
name = "payment_transactions",
uniqueConstraints = {
@UniqueConstraint(name = "uq_tx_gateway_txid", columnNames = {"gateway_transaction_id"})
},
indexes = {
@Index(name = "idx_tx_pay", columnList = "payment_id"),
@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.JpaSpecificationExecutor;
import java.util.List;
import java.util.Optional;
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> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc(
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.dto.Presupuesto;
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
@Service
public class PedidoService {

View File

@ -189,9 +189,9 @@ public class RedsysController {
try {
String idem = "refund-" + paymentId + "-" + amountCents + "-" + UUID.randomUUID();
paymentService.refundViaRedsys(paymentId, amountCents, idem);
return ResponseEntity.ok("{success:true}");
return ResponseEntity.ok("{\"success\":true}");
} 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 String currency;
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) {
this.raw = raw;
@ -203,6 +206,9 @@ public class RedsysService {
this.currency = str(raw.get("Ds_Currency"));
this.amountCents = parseLongSafe(raw.get("Ds_Amount"));
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) {
@ -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) {
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 amountCents Importe en céntimos a devolver.
@ -308,14 +319,35 @@ public class RedsysService {
// Decodificar MerchantParameters de la respuesta
Map<String, Object> decoded = decodeMerchantParametersToMap(dsMerchantParametersResp);
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);
}
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:
file: db/changelog/changesets/0010-drop-unique-tx-gateway.yml
- 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
updateCartCount();
// Si quieres refrescar cada 60s:
setInterval(updateCartCount, 60000);
// Si quieres refrescar cada 10s:
setInterval(updateCartCount, 10000);
// generate a custom event to update the cart count from other scripts
document.addEventListener("update-cart", updateCartCount);

View File

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