mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-12 16:38:48 +00:00
204 lines
7.4 KiB
Java
204 lines
7.4 KiB
Java
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<String, Object> 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<String, Object> 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<String, Object> raw;
|
|
public final String order;
|
|
public final String response;
|
|
public final long amountCents;
|
|
public final String currency;
|
|
|
|
public RedsysNotification(Map<String, Object> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|