diff --git a/pom.xml b/pom.xml index 2e5351a..f882da2 100644 --- a/pom.xml +++ b/pom.xml @@ -151,6 +151,41 @@ ${liquibase.version} + + + com.redsys + apiSha512V2 + 2.0 + system + ${project.basedir}/src/main/resources/lib/apiSha512V2.jar + + + + + org.bouncycastle + bcprov-jdk15on + 1.47 + system + ${project.basedir}/src/main/resources/lib/bcprov-jdk15on-1.4.7.jar + + + + commons-codec + commons-codec + 1.3 + system + ${project.basedir}/src/main/resources/lib/commons-codec-1.3.jar + + + + org.json + json + 1.0 + system + ${project.basedir}/src/main/resources/lib/org.json.jar + + + diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java new file mode 100644 index 0000000..6a7b43b --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java @@ -0,0 +1,53 @@ +package com.imprimelibros.erp.redsys; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping("/pagos/redsys") +public class RedsysController { + + private final RedsysService service; + public RedsysController(RedsysService service) { this.service = service; } + + @PostMapping("/crear") + public String crearPago(@RequestParam String order, + @RequestParam long amountCents, + Model model) { + var payReq = new RedsysService.PaymentRequest(order, amountCents, "Compra en ImprimeLibros"); + var form = service.buildRedirectForm(payReq); + model.addAttribute("action", form.action()); + model.addAttribute("signatureVersion", form.signatureVersion()); + model.addAttribute("merchantParameters", form.merchantParameters()); + model.addAttribute("signature", form.signature()); + return "payments/redsys-redirect"; // Thymeleaf + } + + @PostMapping("/notify") + @ResponseBody + public ResponseEntity notifyRedsys(@RequestParam("Ds_Signature") String dsSig, + @RequestParam("Ds_SignatureVersion") String dsSigVer, + @RequestParam("Ds_MerchantParameters") String dsParams) throws Exception { + var notif = service.validateAndParse(dsSig, dsSigVer, dsParams); + + // 1) Idempotencia: marca el pedido si aún no procesado. + // 2) Verifica importe/moneda/pedido contra tu base de datos. + // 3) Autoriza en tu sistema si notif.authorized() == true. + + return ResponseEntity.ok("OK"); + } + + @GetMapping("/ok") + public String ok() { return "payments/success"; } + + @GetMapping("/ko") + public String ko() { return "payments/failure"; } + +} + diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java new file mode 100644 index 0000000..33ac9eb --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java @@ -0,0 +1,89 @@ +package com.imprimelibros.erp.redsys; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class RedsysService { + + @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 secretKey; + @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; + + public record PaymentRequest(String order, long amountCents, String description) {} + + public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) {} + + public FormPayload buildRedirectForm(PaymentRequest req) { + // RedsysAPI proviene del JAR oficial + com.redsys.api.RedsysAPI api = new com.redsys.api.RedsysAPI(); + + Map mp = new HashMap<>(); + mp.put("DS_MERCHANT_AMOUNT", String.valueOf(req.amountCents())); + mp.put("DS_MERCHANT_ORDER", req.order()); + mp.put("DS_MERCHANT_MERCHANTCODE", merchantCode); + mp.put("DS_MERCHANT_CURRENCY", currency); + mp.put("DS_MERCHANT_TRANSACTIONTYPE", txType); + mp.put("DS_MERCHANT_TERMINAL", terminal); + mp.put("DS_MERCHANT_MERCHANTNAME", "Tu Comercio"); + mp.put("DS_MERCHANT_PRODUCTDESCRIPTION", req.description()); + mp.put("DS_MERCHANT_URLOK", urlOk); + mp.put("DS_MERCHANT_URLKO", urlKo); + mp.put("DS_MERCHANT_MERCHANTURL", urlNotify); + + String merchantParameters = api.createMerchantParameters(mp); + String signature = api.createMerchantSignature(secretKey); + + 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); + } + + // Validación de la notificación on-line (webhook). + public RedsysNotification validateAndParse(String dsSignature, String dsSignatureVersion, String dsMerchantParametersB64) { + com.redsys.api.RedsysAPI api = new com.redsys.api.RedsysAPI(); + + // 1) Validar firma + String calc = api.createMerchantSignatureNotif(secretKey, dsMerchantParametersB64); + if (!Objects.equals(calc, dsSignature)) { + throw new IllegalArgumentException("Firma Redsys no válida"); + } + + // 2) Decodificar parámetros + String json = api.decodeMerchantParameters(dsMerchantParametersB64); + Map params = new com.fasterxml.jackson.databind.ObjectMapper() + .readValue(json, new com.fasterxml.jackson.core.type.TypeReference<>() {}); + // Campos típicos: Ds_Order, Ds_Amount, Ds_Currency, Ds_Response, etc. + return RedsysNotification.from(params); + } + + public static record RedsysNotification(String order, String dsResponse, long amountCents, String currency) { + static RedsysNotification from(Map p) { + String order = (String) p.get("Ds_Order"); + String resp = String.valueOf(p.get("Ds_Response")); + long amount = Long.parseLong((String) p.get("Ds_Amount")); + String curr = String.valueOf(p.get("Ds_Currency")); + return new RedsysNotification(order, resp, amount, curr); + } + // Éxito si 0–99. + public boolean authorized() { + try { + int r = Integer.parseInt(dsResponse); + return r >= 0 && r <= 99; + } catch (Exception e) { return false; } + } + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 321a344..f089b65 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -105,3 +105,16 @@ spring.liquibase.change-log=classpath:db/changelog/master.yml # spring.liquibase.url=jdbc:mysql://localhost:3306/imprimelibros # spring.liquibase.user=tu_user # spring.liquibase.password=tu_pass + + +# Redsys +redsys.environment=test +redsys.merchant-code=124760810 +redsys.terminal=1 +redsys.currency=978 +redsys.transaction-type=0 +redsys.secret-key=sq7HjrUOBfKmC576ILgskD5srU870gJ7 +redsys.urls.ok=https://localhost:8080/pagos/redsys/ok +redsys.urls.ko=https://localhost:8080/pagos/redsys/ko +redsys.urls.notify=https://localhost:8080/pagos/redsys/notify + diff --git a/src/main/resources/lib/apiSha512V2.jar b/src/main/resources/lib/apiSha512V2.jar new file mode 100644 index 0000000..5858b1d Binary files /dev/null and b/src/main/resources/lib/apiSha512V2.jar differ diff --git a/src/main/resources/lib/bcprov-jdk15on-1.4.7.jar b/src/main/resources/lib/bcprov-jdk15on-1.4.7.jar new file mode 100644 index 0000000..0b80922 Binary files /dev/null and b/src/main/resources/lib/bcprov-jdk15on-1.4.7.jar differ diff --git a/src/main/resources/lib/commons-codec-1.3.jar b/src/main/resources/lib/commons-codec-1.3.jar new file mode 100644 index 0000000..957b675 Binary files /dev/null and b/src/main/resources/lib/commons-codec-1.3.jar differ diff --git a/src/main/resources/lib/org.json.jar b/src/main/resources/lib/org.json.jar new file mode 100644 index 0000000..5372cb0 Binary files /dev/null and b/src/main/resources/lib/org.json.jar differ