diff --git a/src/main/java/com/imprimelibros/erp/payments/PaymentController.java b/src/main/java/com/imprimelibros/erp/payments/PaymentController.java index 2cfe1ca..c2907da 100644 --- a/src/main/java/com/imprimelibros/erp/payments/PaymentController.java +++ b/src/main/java/com/imprimelibros/erp/payments/PaymentController.java @@ -4,20 +4,15 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; - +import org.springframework.context.MessageSource; import org.springframework.data.jpa.domain.Specification; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; -import com.imprimelibros.erp.payments.model.PaymentTransactionStatus.*; - import com.imprimelibros.erp.common.Utils; -import com.imprimelibros.erp.configuracion.margenes_presupuestos.MargenPresupuesto; import com.imprimelibros.erp.datatables.DataTable; import com.imprimelibros.erp.datatables.DataTablesParser; import com.imprimelibros.erp.datatables.DataTablesRequest; @@ -25,100 +20,128 @@ import com.imprimelibros.erp.datatables.DataTablesResponse; import com.imprimelibros.erp.payments.model.Payment; import com.imprimelibros.erp.payments.model.PaymentTransaction; import com.imprimelibros.erp.payments.model.PaymentTransactionStatus; +import com.imprimelibros.erp.payments.model.PaymentTransactionType; import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository; import com.imprimelibros.erp.users.User; import com.imprimelibros.erp.users.UserDao; import jakarta.servlet.http.HttpServletRequest; - - @Controller @RequestMapping("/pagos") @PreAuthorize("hasRole('SUPERADMIN')") public class PaymentController { + private final MessageSource messageSource; + protected final PaymentTransactionRepository repoPaymentTransaction; protected final UserDao repoUser; - public PaymentController(PaymentTransactionRepository repoPaymentTransaction, UserDao repoUser) { + public PaymentController(PaymentTransactionRepository repoPaymentTransaction, UserDao repoUser, + MessageSource messageSource) { this.repoPaymentTransaction = repoPaymentTransaction; this.repoUser = repoUser; + this.messageSource = messageSource; } @GetMapping() public String index() { return "imprimelibros/pagos/gestion-pagos"; } - @GetMapping(value = "datatable/redsys", produces = "application/json") @ResponseBody - public DataTablesResponse> getDatatableRedsys(HttpServletRequest request,Locale locale) { - + public DataTablesResponse> getDatatableRedsys(HttpServletRequest request, Locale locale) { + DataTablesRequest dt = DataTablesParser.from(request); List searchable = List.of( - ); + "payment.gatewayOrderId", + "payment.orderId" + // "client" no, porque lo calculas a posteriori + ); + // Campos ordenables List orderable = List.of( - - ); - + "payment.gatewayOrderId", + "payment.orderId", + "amountCents", + "payment.amountRefundedCents", + "createdAt"); + Specification base = Specification.allOf( (root, query, cb) -> cb.equal(root.get("status"), PaymentTransactionStatus.succeeded)); - + base = base.and((root, query, cb) -> cb.equal(root.get("type"), PaymentTransactionType.CAPTURE)); + + String clientSearch = dt.getColumnSearch("client"); + + // 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification + if (clientSearch != null) { + List userIds = repoUser.findIdsByFullNameLike(clientSearch.trim()); + + if (userIds.isEmpty()) { + // Ningún usuario coincide → forzamos 0 resultados + base = base.and((root, query, cb) -> cb.disjunction()); + } else { + base = base.and((root, query, cb) -> root.join("payment").get("userId").in(userIds)); + } + } Long total = repoPaymentTransaction.count(base); return DataTable - .of(repoPaymentTransaction, PaymentTransaction.class, dt, searchable) // 'searchable' en DataTable.java - // edita columnas "reales": + .of(repoPaymentTransaction, PaymentTransaction.class, dt, searchable) .orderable(orderable) - .add("created_at", (pago) -> { - return Utils.formatDateTime(pago.getCreatedAt(), locale); - }) - .add("client", (pago) -> { + .add("created_at", pago -> Utils.formatDateTime(pago.getCreatedAt(), locale)) + .add("client", pago -> { if (pago.getPayment() != null && pago.getPayment().getUserId() != null) { Payment payment = pago.getPayment(); - if(payment.getUserId() != null) { - Optional user = repoUser.findById(payment.getUserId()); + if (payment.getUserId() != null) { + Optional user = repoUser.findById(payment.getUserId().longValue()); return user.map(User::getFullName).orElse(""); } - return ""; - } else { - return ""; } + return ""; }) - .add("gateway_order_id", (pago) -> { + .add("gateway_order_id", pago -> { if (pago.getPayment() != null) { return pago.getPayment().getGatewayOrderId(); } else { return ""; } }) - .add("orderId", (pago) -> { + .add("orderId", pago -> { if (pago.getPayment() != null && pago.getPayment().getOrderId() != null) { return pago.getPayment().getOrderId().toString(); } else { return ""; } }) - .add("amount_cents", (pago) -> { - return Utils.formatCurrency(pago.getAmountCents() / 100.0, locale); + .add("amount_cents", pago -> Utils.formatCurrency(pago.getAmountCents() / 100.0, locale)) + .add("amount_cents_refund", pago -> { + Payment payment = pago.getPayment(); + if (payment != null) { + return Utils.formatCurrency(payment.getAmountRefundedCents() / 100.0, locale); + } + return ""; }) - .add("actions", (pago) -> { - return "
\n" + - " \n" - + " \n" - + "
"; + .add("actions", pago -> { + Payment p = pago.getPayment(); + if (p != null) { + if (pago.getAmountCents() - p.getAmountRefundedCents() > 0) { + return "" + + messageSource.getMessage("pagos.table.devuelto", null, locale) + ""; + } + return ""; + } else { + return ""; + } }) .where(base) - // Filtros custom: .toJson(total); - + } - - + } diff --git a/src/main/java/com/imprimelibros/erp/payments/PaymentService.java b/src/main/java/com/imprimelibros/erp/payments/PaymentService.java index 0b98dc5..b5e6f2a 100644 --- a/src/main/java/com/imprimelibros/erp/payments/PaymentService.java +++ b/src/main/java/com/imprimelibros/erp/payments/PaymentService.java @@ -41,7 +41,6 @@ public class PaymentService { this.webhookEventRepo = webhookEventRepo; this.cartService = cartService; } - /** * Crea el Payment en BD y construye el formulario de Redsys usando la API @@ -54,7 +53,7 @@ public class PaymentService { p.setOrderId(null); Cart cart = this.cartService.findById(cartId); - if(cart != null && cart.getUserId() != null) { + if (cart != null && cart.getUserId() != null) { p.setUserId(cart.getUserId()); } p.setCurrency(currency); @@ -198,21 +197,22 @@ public class PaymentService { p.setFailedAt(LocalDateTime.now()); } - if(authorized) { + if (authorized) { // GENERAR PEDIDO A PARTIR DEL CARRITO Cart cart = this.cartService.findById(notif.cartId); - if(cart != null) { + if (cart != null) { // Bloqueamos el carrito this.cartService.lockCartById(cart.getId()); - // order ID es generado dentro de createOrderFromCart donde se marcan los presupuestos como no editables - // Long orderId = this.cartService.pedidoService.createOrderFromCart(cart.getId(), p.getId()); + // order ID es generado dentro de createOrderFromCart donde se marcan los + // presupuestos como no editables + // Long orderId = + // this.cartService.pedidoService.createOrderFromCart(cart.getId(), p.getId()); // p.setOrderId(orderId); - + } } - + payRepo.save(p); - if (!authorized) { ev.setLastError("Payment declined (Ds_Response=" + notif.response + ")"); @@ -230,9 +230,8 @@ public class PaymentService { } } - // ---- refundViaRedsys y bank_transfer igual que antes, no tocan RedsysService + // ---- refundViaRedsys // ---- - @Transactional public void refundViaRedsys(Long paymentId, long amountCents, String idempotencyKey) { Payment p = payRepo.findById(paymentId) @@ -240,6 +239,7 @@ public class PaymentService { if (amountCents <= 0) throw new IllegalArgumentException("Importe inválido"); + long maxRefundable = p.getAmountCapturedCents() - p.getAmountRefundedCents(); if (amountCents > maxRefundable) throw new IllegalStateException("Importe de devolución supera lo capturado"); @@ -256,8 +256,18 @@ public class PaymentService { r.setRequestedAt(LocalDateTime.now()); r = refundRepo.save(r); - String gatewayRefundId = "REF-" + UUID.randomUUID(); // aquí iría el ID real si alguna vez llamas a un API de - // devoluciones + String gatewayRefundId; + try { + // ⚠️ Usa aquí el mismo valor que mandaste en Ds_Merchant_Order al cobrar + // por ejemplo, p.getGatewayOrderId() o similar + String originalOrder = p.getGatewayOrderId(); // ajusta al nombre real del campo + gatewayRefundId = redsysService.requestRefund(originalOrder, amountCents); + } catch (Exception e) { + r.setStatus(RefundStatus.failed); + r.setProcessedAt(LocalDateTime.now()); + refundRepo.save(r); + throw new IllegalStateException("Error al solicitar la devolución a Redsys", e); + } PaymentTransaction tx = new PaymentTransaction(); tx.setPayment(p); @@ -285,13 +295,14 @@ public class PaymentService { payRepo.save(p); } + @Transactional public Payment createBankTransferPayment(Long cartId, long amountCents, String currency) { Payment p = new Payment(); p.setOrderId(null); Cart cart = this.cartService.findById(cartId); - if(cart != null && cart.getUserId() != null) { + if (cart != null && cart.getUserId() != null) { p.setUserId(cart.getUserId()); } diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java index d6390a6..3638faa 100644 --- a/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java +++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java @@ -8,6 +8,11 @@ import sis.redsys.api.ApiMacSha256; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Base64; @@ -18,6 +23,10 @@ import java.util.Objects; public class RedsysService { // ---------- CONFIG ---------- + @Value("${redsys.url}") + private String url; + @Value("${redsys.refund.url}") + private String urlRefund; @Value("${redsys.merchant-code}") private String merchantCode; @Value("${redsys.terminal}") @@ -37,6 +46,8 @@ public class RedsysService { @Value("${redsys.environment}") private String env; + private final HttpClient httpClient = HttpClient.newHttpClient(); + // ---------- RECORDS ---------- // Pedido a Redsys public record PaymentRequest(String order, long amountCents, String description, Long cartId) { @@ -89,9 +100,11 @@ public class RedsysService { 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"; + String action = url; + /* + * ? "https://sis-t.redsys.es:25443/sis/realizarPago" + * : "https://sis.redsys.es/sis/realizarPago"; + */ return new FormPayload(action, "HMAC_SHA256_V1", merchantParameters, signature); } @@ -231,4 +244,84 @@ public class RedsysService { } } } + + /** + * 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. + * @return gatewayRefundId (p.ej. Ds_AuthorisationCode o Ds_Order) + */ + public String requestRefund(String order, long amountCents) throws Exception { + ApiMacSha256 api = new ApiMacSha256(); + + // Montar parámetros para el refund + api.setParameter("DS_MERCHANT_MERCHANTCODE", merchantCode); + api.setParameter("DS_MERCHANT_TERMINAL", terminal); + api.setParameter("DS_MERCHANT_ORDER", order); + api.setParameter("DS_MERCHANT_AMOUNT", String.valueOf(amountCents)); + api.setParameter("DS_MERCHANT_CURRENCY", currency); + api.setParameter("DS_MERCHANT_TRANSACTIONTYPE", "3"); // 3 = devolución + api.setParameter("DS_MERCHANT_MERCHANTURL", ""); + api.setParameter("DS_MERCHANT_URLOK", ""); + api.setParameter("DS_MERCHANT_URLKO", ""); + + // Crear parámetros y firma (como en tu PHP) + String merchantParameters = api.createMerchantParameters(); + String signature = api.createMerchantSignature(secretKeyBase64); + + // Montar el JSON para Redsys REST + String json = """ + { + "Ds_MerchantParameters": "%s", + "Ds_Signature": "%s", + "Ds_SignatureVersion": "HMAC_SHA256_V1" + } + """.formatted(merchantParameters, signature); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(urlRefund)) + .header("Content-Type", "application/json; charset=UTF-8") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + System.out.println("### Redsys refund REST request:\n" + json); + System.out.println("### HTTP " + response.statusCode()); + System.out.println("### Redsys refund REST response:\n" + response.body()); + + if (response.statusCode() / 100 != 2) + throw new IllegalStateException("HTTP error Redsys refund: " + response.statusCode()); + + if (response.body() == null || response.body().isBlank()) + throw new IllegalStateException("Respuesta vacía de Redsys refund REST"); + + // Parsear la respuesta JSON + Map respMap = MAPPER.readValue(response.body(), new TypeReference<>() { + }); + + // Redsys puede devolver "Ds_MerchantParameters" o "errorCode" + if (respMap.containsKey("errorCode")) { + throw new IllegalStateException("Error Redsys refund: " + respMap.get("errorCode")); + } + + String dsMerchantParametersResp = (String) respMap.get("Ds_MerchantParameters"); + if (dsMerchantParametersResp == null) { + throw new IllegalStateException("Respuesta Redsys refund sin Ds_MerchantParameters"); + } + + // Decodificar MerchantParameters de la respuesta + Map decoded = decodeMerchantParametersToMap(dsMerchantParametersResp); + System.out.println("### Redsys refund decoded response:\n" + decoded); + + String dsResponse = String.valueOf(decoded.get("Ds_Response")); + if (!"0900".equals(dsResponse)) { + throw new IllegalStateException("Devolución rechazada, Ds_Response=" + dsResponse); + } + + return String.valueOf(decoded.getOrDefault("Ds_AuthorisationCode", order)); + } + + } diff --git a/src/main/java/com/imprimelibros/erp/users/UserDao.java b/src/main/java/com/imprimelibros/erp/users/UserDao.java index de2645e..ec206de 100644 --- a/src/main/java/com/imprimelibros/erp/users/UserDao.java +++ b/src/main/java/com/imprimelibros/erp/users/UserDao.java @@ -19,60 +19,63 @@ import org.springframework.lang.Nullable; @Repository public interface UserDao extends JpaRepository, JpaSpecificationExecutor { - // Aplicamos EntityGraph a la versión con Specification+Pageable - @Override - @EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" }) - @NonNull - Page findAll(@Nullable Specification spec, @NonNull Pageable pageable); + // Aplicamos EntityGraph a la versión con Specification+Pageable + @Override + @EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" }) + @NonNull + Page findAll(@Nullable Specification spec, @NonNull Pageable pageable); - Optional findByUserNameIgnoreCase(String userName); + Optional findByUserNameIgnoreCase(String userName); - boolean existsByUserNameIgnoreCase(String userName); + boolean existsByUserNameIgnoreCase(String userName); - // Para comprobar si existe al hacer signup - @Query(value = """ - SELECT id, deleted, enabled - FROM users - WHERE LOWER(username) = LOWER(:userName) - LIMIT 1 - """, nativeQuery = true) - Optional findLiteByUserNameIgnoreCase(@Param("userName") String userName); + // Para comprobar si existe al hacer signup + @Query(value = """ + SELECT id, deleted, enabled + FROM users + WHERE LOWER(username) = LOWER(:userName) + LIMIT 1 + """, nativeQuery = true) + Optional findLiteByUserNameIgnoreCase(@Param("userName") String userName); - boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id); + boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id); - // Nuevo: para login/negocio "activo" - @EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" }) - Optional findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName); + // Nuevo: para login/negocio "activo" + @EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" }) + Optional findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName); - // Para poder restaurar, necesitas leer ignorando @Where (native): - @Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true) - Optional findByIdIncludingDeleted(@Param("id") Long id); + // Para poder restaurar, necesitas leer ignorando @Where (native): + @Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true) + Optional findByIdIncludingDeleted(@Param("id") Long id); - @Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true) - List findAllDeleted(); + @Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true) + List findAllDeleted(); - @Query("select u.id from User u where lower(u.userName) = lower(:userName)") - Optional findIdByUserNameIgnoreCase(@Param("userName") String userName); + @Query("select u.id from User u where lower(u.userName) = lower(:userName)") + Optional findIdByUserNameIgnoreCase(@Param("userName") String userName); - @Query(value = """ - SELECT DISTINCT u - FROM User u - JOIN u.rolesLink rl - JOIN rl.role r - WHERE (:role IS NULL OR r.name = :role) - AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%')) - OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%'))) - """, countQuery = """ - SELECT COUNT(DISTINCT u.id) - FROM User u - JOIN u.rolesLink rl - JOIN rl.role r - WHERE (:role IS NULL OR r.name = :role) - AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%')) - OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%'))) - """) - Page searchUsers(@Param("role") String role, - @Param("q") String q, - Pageable pageable); + @Query(value = """ + SELECT DISTINCT u + FROM User u + JOIN u.rolesLink rl + JOIN rl.role r + WHERE (:role IS NULL OR r.name = :role) + AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%')) + OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%'))) + """, countQuery = """ + SELECT COUNT(DISTINCT u.id) + FROM User u + JOIN u.rolesLink rl + JOIN rl.role r + WHERE (:role IS NULL OR r.name = :role) + AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%')) + OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%'))) + """) + Page searchUsers(@Param("role") String role, + @Param("q") String q, + Pageable pageable); + + @Query("select u.id from User u where lower(u.fullName) like lower(concat('%', :name, '%'))") + List findIdsByFullNameLike(@Param("name") String name); } diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index f6b1c86..318538b 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -20,6 +20,8 @@ safekat.api.password=Safekat2024 # Configuración Redsys redsys.environment=test +redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago +redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST redsys.urls.ok=http://localhost:8080/pagos/redsys/ok redsys.urls.ko=http://localhost:8080/pagos/redsys/ko redsys.urls.notify=https://orological-sacrilegiously-lucille.ngrok-free.dev/pagos/redsys/notify \ No newline at end of file diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 40ae4a5..43d6eef 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -20,6 +20,8 @@ safekat.api.password=Safekat2024 # Configuración Redsys redsys.environment=test +redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago +redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST redsys.urls.ok=https://imprimelibros.jjimenez.eu/pagos/redsys/ok redsys.urls.ko=https://imprimelibros.jjimenez.eu/pagos/redsys/ko redsys.urls.notify=https://imprimelibros.jjimenez.eu/pagos/redsys/notify \ No newline at end of file diff --git a/src/main/resources/db/changelog/changesets/0009-add-composite-unique-txid-type b/src/main/resources/db/changelog/changesets/0009-add-composite-unique-txid-type new file mode 100644 index 0000000..651e126 --- /dev/null +++ b/src/main/resources/db/changelog/changesets/0009-add-composite-unique-txid-type @@ -0,0 +1,15 @@ +databaseChangeLog: + - changeSet: + id: 0009-add-composite-unique-txid-type + author: JJO + changes: + # 1️⃣ Eliminar el índice único anterior + - dropUniqueConstraint: + constraintName: uq_tx_gateway_txid + tableName: payment_transactions + + # 2️⃣ Crear índice único compuesto por gateway_transaction_id + type + - addUniqueConstraint: + tableName: payment_transactions + columnNames: gateway_transaction_id, type + constraintName: uq_tx_gateway_txid_type diff --git a/src/main/resources/db/changelog/master.yml b/src/main/resources/db/changelog/master.yml index eef96c7..1d976e5 100644 --- a/src/main/resources/db/changelog/master.yml +++ b/src/main/resources/db/changelog/master.yml @@ -14,4 +14,6 @@ databaseChangeLog: - include: file: db/changelog/changesets/0007-payments-core.yml - include: - file: db/changelog/changesets/0008-update-cart-status-constraint.yml \ No newline at end of file + file: db/changelog/changesets/0008-update-cart-status-constraint.yml + - include: + file: db/changelog/changesets/0009-add-composite-unique-txid-type.yml \ No newline at end of file diff --git a/src/main/resources/i18n/pagos_es.properties b/src/main/resources/i18n/pagos_es.properties index bf2e2aa..c1bb8ef 100644 --- a/src/main/resources/i18n/pagos_es.properties +++ b/src/main/resources/i18n/pagos_es.properties @@ -7,6 +7,7 @@ pagos.table.cliente.nombre=Nombre Cliente pagos.table.redsys.id=Cod. Redsys pagos.table.pedido.id=Pedido pagos.table.cantidad=Cantidad +pagos.table.devuelto=Devolución pagos.table.fecha=Fecha pagos.table.estado=Estado pagos.table.acciones=Acciones diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/pagos/pagos.js b/src/main/resources/static/assets/js/pages/imprimelibros/pagos/pagos.js index 68c5eb0..400aab6 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/pagos/pagos.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/pagos/pagos.js @@ -23,9 +23,10 @@ $(() => { serverSide: true, orderCellsTop: true, pageLength: 50, + lengthMenu: [10, 25, 50, 100, 500], language: { url: '/assets/libs/datatables/i18n/' + language + '.json' }, responsive: true, - dom: 'lrBtip', + dom: 'lBrtip', buttons: { dom: { button: { @@ -45,16 +46,66 @@ $(() => { url: '/pagos/datatable/redsys', method: 'GET', }, - order: [[4, 'asc']], // Ordena por fecha por defecto + order: [[5, 'desc']], // Ordena por fecha por defecto columns: [ - { data: 'client', name: 'user.fullName', orderable: true }, - { data: 'gateway_order_id', name: 'payments.gateway_order_id', orderable: true }, - { data: 'orderId', name: 'order.id', orderable: true }, - { data: 'amount_cents', name: 'amount_cents', orderable: true }, - { data: 'created_at', name: 'created_at', orderable: true }, - { data: 'actions', name: 'actions' } + { data: 'client', name: 'client', orderable: true }, + { data: 'gateway_order_id', name: 'payment.gatewayOrderId', orderable: true }, + { data: 'orderId', name: 'payment.orderId', orderable: true }, + { data: 'amount_cents', name: 'amountCents', orderable: true }, + { data: 'amount_cents_refund', name: 'amountCentsRefund', orderable: true }, + { data: 'created_at', name: 'createdAt', orderable: true }, + { data: 'actions', name: 'actions', orderable: false, searchable: false } + ], columnDefs: [{ targets: -1, orderable: false, searchable: false }] }); -}) \ No newline at end of file + // Fila de filtros = segunda fila del thead (index 1) + $('#pagos-redsys-datatable thead tr:eq(1) th').each(function (colIdx) { + const input = $(this).find('input'); + if (input.length === 0) return; // columnas sin filtro + + input.on('keyup change', function () { + const value = this.value; + if (table.column(colIdx).search() !== value) { + table.column(colIdx).search(value).draw(); + } + }); + }); + + $(document).on('click', '.btn-refund-payment', function () { + const dsOrderId = $(this).data('dsorderid'); + const transactionId = $(this).data('transactionid'); + // show swal confirmation with input for amount to refund + Swal.fire({ + title: '¿Estás seguro de que deseas devolver este pago?', + text: 'Introduce la cantidad a devolver (en euros):', + input: 'number', + inputAttributes: { + min: 0, + } + }).then((result) => { + if (result.isConfirmed) { + const amountToRefund = parseFloat(result.value); + if (isNaN(amountToRefund) || amountToRefund <= 0) { + Swal.fire('Error', 'Cantidad inválida para la devolución.', 'error'); + return; + } + $.ajax({ + url: '/pagos/redsys/refund/' + transactionId, + method: 'POST', + data: { + amountCents: amountToRefund*100 + } + }).then((response) => { + if (response.success) { + Swal.fire('Éxito', 'Pago devuelto con éxito.', 'success'); + table.draw(); + } else { + Swal.fire('Error', 'No se pudo procesar la devolución.', 'error'); + } + }); + } + }); + }); +}); diff --git a/src/main/resources/templates/imprimelibros/pagos/gestion-pagos.html b/src/main/resources/templates/imprimelibros/pagos/gestion-pagos.html index 09059bb..635c48c 100644 --- a/src/main/resources/templates/imprimelibros/pagos/gestion-pagos.html +++ b/src/main/resources/templates/imprimelibros/pagos/gestion-pagos.html @@ -8,8 +8,6 @@ - - @@ -68,6 +66,7 @@
+
diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuesto-list-items/tabla-anonimos.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuesto-list-items/tabla-anonimos.html index 25b474f..3e7936a 100644 --- a/src/main/resources/templates/imprimelibros/presupuestos/presupuesto-list-items/tabla-anonimos.html +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuesto-list-items/tabla-anonimos.html @@ -17,7 +17,8 @@ Actualizado el Acciones - + + @@ -48,7 +49,8 @@ - + @@ -68,7 +70,7 @@ - +