package com.imprimelibros.erp.redsys; import com.imprimelibros.erp.cart.Cart; import com.imprimelibros.erp.common.Utils; import com.imprimelibros.erp.payments.PaymentService; import com.imprimelibros.erp.payments.model.Payment; import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository; import com.imprimelibros.erp.pedidos.Pedido; import com.imprimelibros.erp.pedidos.PedidoService; import com.imprimelibros.erp.redsys.RedsysService.FormPayload; import groovy.util.logging.Log; import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.thymeleaf.context.WebContext; import org.thymeleaf.web.IWebExchange; import org.thymeleaf.web.servlet.JakartaServletWebApplication; import org.thymeleaf.spring6.SpringTemplateEngine; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.context.MessageSource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.Map; import java.util.UUID; @Controller @RequestMapping("/pagos/redsys") public class RedsysController { private final PaymentService paymentService; private final MessageSource messageSource; private final SpringTemplateEngine templateEngine; private final ServletContext servletContext; private final PedidoService pedidoService; public RedsysController(PaymentService paymentService, MessageSource messageSource, SpringTemplateEngine templateEngine, ServletContext servletContext, PedidoService pedidoService) { this.paymentService = paymentService; this.messageSource = messageSource; this.templateEngine = templateEngine; this.servletContext = servletContext; this.pedidoService = pedidoService; } @PostMapping(value = "/crear", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @ResponseBody public ResponseEntity crearPago(@RequestParam("amountCents") Long amountCents, @RequestParam("method") String method, @RequestParam("cartId") Long cartId, @RequestParam(value = "dirFactId", required = false) Long dirFactId, HttpServletRequest request, HttpServletResponse response, Locale locale) throws Exception { // Creamos el pedido inteno Pedido order = pedidoService.crearPedido(cartId, dirFactId, null, null); if ("bank-transfer".equalsIgnoreCase(method)) { // 1) Creamos el Payment interno SIN orderId (null) Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR", locale, order.getId()); pedidoService.markPedidoAsProcesingPayment(order.getId()); // 1️⃣ Crear la "aplicación" web de Thymeleaf (Jakarta) JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext); // 2️⃣ Construir el intercambio web desde request/response response.setContentType("text/html;charset=UTF-8"); response.setCharacterEncoding("UTF-8"); IWebExchange exchange = app.buildExchange(request, response); // 3️⃣ Crear el contexto WebContext con Locale WebContext ctx = new WebContext(exchange, locale); String importeFormateado = Utils.formatCurrency(amountCents / 100.0, locale); ctx.setVariable("importe", importeFormateado); ctx.setVariable("concepto", "TRANSF-" + p.getOrderId()); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); boolean isAuth = auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken); ctx.setVariable("isAuth", isAuth); // 3) Renderizamos la plantilla a HTML String html = templateEngine.process("imprimelibros/pagos/transfer", ctx); byte[] body = html.getBytes(StandardCharsets.UTF_8); return ResponseEntity.ok() .contentType(MediaType.TEXT_HTML) .body(body); } // Tarjeta o Bizum (Redsys) FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method, order.getId()); String html = """ Redirigiendo a Redsys…
""".formatted( form.action(), form.signatureVersion(), form.merchantParameters(), form.signature(), cartId); byte[] body = html.getBytes(StandardCharsets.UTF_8); return ResponseEntity.ok() .contentType(MediaType.TEXT_HTML) .body(body); } @PostMapping(value = "/reintentar", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @ResponseBody public ResponseEntity reintentarPago(@RequestParam("amountCents") Long amountCents, @RequestParam("method") String method, @RequestParam("orderId") Long orderId, HttpServletRequest request, HttpServletResponse response, Locale locale) throws Exception { // Creamos el pedido inteno Pedido order = pedidoService.findById(orderId); // Find the payment with orderId = order.getId() and status = failed Payment failedPayment = paymentService.findFailedPaymentByOrderId(order.getId()); if (failedPayment == null) { throw new Exception("No se encontró un pago fallido para el pedido " + order.getId()); } Long cartId = null; Long dirFactId = null; // Find payment transaction details from failedPayment if needed try { Map transactionDetails = paymentService.getPaymentTransactionData(failedPayment.getId()); cartId = transactionDetails.get("cartId"); dirFactId = transactionDetails.get("dirFactId"); } catch (Exception e) { throw new Exception( "No se pudieron obtener los detalles de la transacción para el pago " + failedPayment.getId()); } if ("bank-transfer".equalsIgnoreCase(method)) { // 1) Creamos el Payment interno SIN orderId (null) Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR", locale, order.getId()); pedidoService.markPedidoAsProcesingPayment(order.getId()); // 1️⃣ Crear la "aplicación" web de Thymeleaf (Jakarta) JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext); // 2️⃣ Construir el intercambio web desde request/response response.setContentType("text/html;charset=UTF-8"); response.setCharacterEncoding("UTF-8"); IWebExchange exchange = app.buildExchange(request, response); // 3️⃣ Crear el contexto WebContext con Locale WebContext ctx = new WebContext(exchange, locale); String importeFormateado = Utils.formatCurrency(amountCents / 100.0, locale); ctx.setVariable("importe", importeFormateado); ctx.setVariable("concepto", "TRANSF-" + p.getOrderId()); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); boolean isAuth = auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken); ctx.setVariable("isAuth", isAuth); // 3) Renderizamos la plantilla a HTML String html = templateEngine.process("imprimelibros/pagos/transfer", ctx); byte[] body = html.getBytes(StandardCharsets.UTF_8); return ResponseEntity.ok() .contentType(MediaType.TEXT_HTML) .body(body); } // Tarjeta o Bizum (Redsys) FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method, order.getId()); String html = """ Redirigiendo a Redsys…
""".formatted( form.action(), form.signatureVersion(), form.merchantParameters(), form.signature(), cartId); byte[] body = html.getBytes(StandardCharsets.UTF_8); return ResponseEntity.ok() .contentType(MediaType.TEXT_HTML) .body(body); } // GET: cuando el usuario cae aquí sin parámetros, o Redsys redirige por GET @GetMapping("/ok") public String okGet(RedirectAttributes redirectAttrs, Model model, Locale locale) { return "imprimelibros/pagos/pago-ok"; } // POST: si Redsys envía Ds_Signature y Ds_MerchantParameters (muchas // integraciones ni lo usan) @PostMapping(value = "/ok", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @ResponseBody public ResponseEntity okPost(@RequestParam("Ds_Signature") String signature, @RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) { try { // opcional: idempotente, si /notify ya ha hecho el trabajo no pasa nada paymentService.handleRedsysNotification(signature, merchantParameters, locale); return ResponseEntity.ok("

Pago realizado correctamente

Volver"); } catch (Exception e) { return ResponseEntity.badRequest() .body("

Error validando pago

" + e.getMessage() + "
"); } } @GetMapping("/ko") public String koGet(RedirectAttributes redirectAttrs, Model model, Locale locale) { String msg = messageSource.getMessage("checkout.error.payment", null, "Error al procesar el pago: el pago ha sido cancelado o rechazado Por favor, inténtelo de nuevo.", locale); model.addAttribute("errorPago", msg); redirectAttrs.addFlashAttribute("errorPago", msg); return "redirect:/cart"; } @PostMapping(value = "/ko", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @ResponseBody public ResponseEntity koPost( @RequestParam("Ds_Signature") String signature, @RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) { try { // Procesamos la notificación IGUAL que en /ok y /notify paymentService.handleRedsysNotification(signature, merchantParameters, locale); // Mensaje para el usuario (pago cancelado/rechazado) String html = "

Pago cancelado o rechazado

Volver"; return ResponseEntity.ok(html); } catch (Exception e) { // Si algo falla al validar/procesar, lo mostramos (útil en entorno de pruebas) String html = "

Error procesando notificación KO

" + e.getMessage() + "
"; return ResponseEntity.badRequest().body(html); } } @PostMapping(value = "/notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @ResponseBody public String notifyRedsys(@RequestParam("Ds_Signature") String signature, @RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) { try { paymentService.handleRedsysNotification(signature, merchantParameters, locale); return "OK"; } catch (Exception e) { e.printStackTrace(); // 👈 para ver el motivo del 500 en logs return "ERROR"; } } @PostMapping(value = "/refund/{paymentId}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @ResponseBody public ResponseEntity refund(@PathVariable Long paymentId, @RequestParam("amountCents") Long amountCents) { try { String idem = "refund-" + paymentId + "-" + amountCents + "-" + UUID.randomUUID(); paymentService.refundViaRedsys(paymentId, amountCents, idem); return ResponseEntity.ok("{\"success\":true}"); } catch (Exception e) { return ResponseEntity.badRequest().body("{\"success\":false, \"error\": \"" + e.getMessage() + "\"}"); } } }