package com.imprimelibros.erp.redsys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import sis.redsys.api.ApiMacSha256; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Base64; import java.util.Map; import java.util.Objects; @Service public class RedsysService { // ---------- CONFIG ---------- @Value("${redsys.merchant-code}") private String merchantCode; @Value("${redsys.terminal}") private String terminal; @Value("${redsys.currency}") private String currency; @Value("${redsys.transaction-type}") private String txType; @Value("${redsys.secret-key}") private String secretKeyBase64; @Value("${redsys.urls.ok}") private String urlOk; @Value("${redsys.urls.ko}") private String urlKo; @Value("${redsys.urls.notify}") private String urlNotify; @Value("${redsys.environment}") private String env; // ---------- RECORDS ---------- // Pedido a Redsys public record PaymentRequest(String order, long amountCents, String description) { } // Payload para el formulario public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) { } // ---------- MÉTODO PRINCIPAL (TARJETA) ---------- public FormPayload buildRedirectForm(PaymentRequest req) throws Exception { return buildRedirectFormInternal(req, false); // false = tarjeta (sin PAYMETHODS) } // ---------- NUEVO: MÉTODO PARA BIZUM ---------- public FormPayload buildRedirectFormBizum(PaymentRequest req) throws Exception { return buildRedirectFormInternal(req, true); // true = Bizum (PAYMETHODS = z) } // ---------- LÓGICA COMÚN ---------- private FormPayload buildRedirectFormInternal(PaymentRequest req, boolean bizum) throws Exception { ApiMacSha256 api = new ApiMacSha256(); api.setParameter("DS_MERCHANT_AMOUNT", String.valueOf(req.amountCents())); api.setParameter("DS_MERCHANT_ORDER", req.order()); // Usa 12 dígitos con ceros api.setParameter("DS_MERCHANT_MERCHANTCODE", merchantCode); api.setParameter("DS_MERCHANT_CURRENCY", currency); api.setParameter("DS_MERCHANT_TRANSACTIONTYPE", txType); api.setParameter("DS_MERCHANT_TERMINAL", terminal); api.setParameter("DS_MERCHANT_MERCHANTURL", urlNotify); api.setParameter("DS_MERCHANT_URLOK", urlOk); api.setParameter("DS_MERCHANT_URLKO", urlKo); if (req.description() != null && !req.description().isBlank()) { api.setParameter("DS_MERCHANT_PRODUCTDESCRIPTION", req.description()); } // 🔹 Bizum: PAYMETHODS = "z" según Redsys if (bizum) { api.setParameter("DS_MERCHANT_PAYMETHODS", "z"); } String merchantParameters = api.createMerchantParameters(); String signature = api.createMerchantSignature(secretKeyBase64); String action = "test".equalsIgnoreCase(env) ? "https://sis-t.redsys.es:25443/sis/realizarPago" : "https://sis.redsys.es/sis/realizarPago"; return new FormPayload(action, "HMAC_SHA256_V1", merchantParameters, signature); } // ---------- STEP 3: Decodificar Ds_MerchantParameters ---------- private static final ObjectMapper MAPPER = new ObjectMapper(); public Map decodeMerchantParametersToMap(String dsMerchantParametersB64) throws Exception { try { byte[] decoded = Base64.getDecoder().decode(dsMerchantParametersB64); String json = new String(decoded, StandardCharsets.UTF_8); return MAPPER.readValue(json, new TypeReference<>() { }); } catch (Exception e) { throw new IllegalArgumentException("No se pudo decodificar Ds_MerchantParameters", e); } } // ---------- STEP 4: Validar notificación ---------- public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64) throws Exception { // 1) Decodificamos a mapa solo para leer campos Map mp = decodeMerchantParametersToMap(dsMerchantParametersB64); RedsysNotification notif = new RedsysNotification(mp); if (notif.order == null || notif.order.isBlank()) { throw new IllegalArgumentException("Falta Ds_Order en Ds_MerchantParameters"); } // 2) Calculamos la firma esperada usando el B64 tal cual ApiMacSha256 api = new ApiMacSha256(); // Esta línea es opcional para createMerchantSignatureNotif, pero no molesta: api.setParameter("Ds_MerchantParameters", dsMerchantParametersB64); String expected = api.createMerchantSignatureNotif( secretKeyBase64, dsMerchantParametersB64 // 👈 AQUÍ va el B64, NO el JSON ); // 3) Comparamos en constante time, normalizando Base64 URL-safe if (!safeEqualsB64(dsSignature, expected)) { System.out.println("Firma Redsys no válida"); System.out.println("Ds_Signature (Redsys) = " + dsSignature); System.out.println("Expected (local) = " + expected); throw new SecurityException("Firma Redsys no válida"); } return notif; } // ---------- HELPERS ---------- private static boolean safeEqualsB64(String a, String b) { if (Objects.equals(a, b)) return true; try { String na = normalizeB64(a); String nb = normalizeB64(b); byte[] da = Base64.getDecoder().decode(na); byte[] db = Base64.getDecoder().decode(nb); return MessageDigest.isEqual(da, db); } catch (Exception e) { return false; } } private static String normalizeB64(String s) { if (s == null) return ""; String n = s.replace('-', '+').replace('_', '/'); int mod = n.length() % 4; if (mod == 2) n += "=="; else if (mod == 3) n += "="; else if (mod == 1) n += "==="; return n; } // ---------- MODELO DE NOTIFICACIÓN ---------- public static final class RedsysNotification { public final Map raw; public final String order; public final String response; public final long amountCents; public final String currency; public RedsysNotification(Map raw) { this.raw = raw; this.order = str(raw.get("Ds_Order")); this.response = str(raw.get("Ds_Response")); this.currency = str(raw.get("Ds_Currency")); this.amountCents = parseLongSafe(raw.get("Ds_Amount")); } public boolean authorized() { try { int r = Integer.parseInt(response); return r >= 0 && r <= 99; } catch (Exception e) { return false; } } private static String str(Object o) { return o == null ? null : String.valueOf(o); } private static long parseLongSafe(Object o) { try { return Long.parseLong(String.valueOf(o)); } catch (Exception e) { return 0L; } } } }