mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-29 15:18:50 +00:00
haciendo datatables de los pagos
This commit is contained in:
@ -3,8 +3,10 @@ package com.imprimelibros.erp;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
@ConfigurationPropertiesScan(basePackages = "com.imprimelibros.erp")
|
@ConfigurationPropertiesScan(basePackages = "com.imprimelibros.erp")
|
||||||
public class ErpApplication {
|
public class ErpApplication {
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.imprimelibros.erp.cart;
|
||||||
|
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CartCleanupService {
|
||||||
|
|
||||||
|
private final CartRepository cartRepository;
|
||||||
|
|
||||||
|
public CartCleanupService(CartRepository cartRepository) {
|
||||||
|
this.cartRepository = cartRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ejecuta cada noche a las 2:00 AM
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
@Scheduled(cron = "0 0 2 * * *") // cada día a las 02:00
|
||||||
|
public void markAbandonedCarts() {
|
||||||
|
LocalDateTime limite = LocalDateTime.now().minusDays(7);
|
||||||
|
int updated = cartRepository.markOldCartsAsAbandoned(limite);
|
||||||
|
System.out.println("Carritos abandonados marcados: " + updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,12 @@
|
|||||||
package com.imprimelibros.erp.cart;
|
package com.imprimelibros.erp.cart;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface CartRepository extends JpaRepository<Cart, Long> {
|
public interface CartRepository extends JpaRepository<Cart, Long> {
|
||||||
@ -17,5 +21,15 @@ public interface CartRepository extends JpaRepository<Cart, Long> {
|
|||||||
where c.id = :id
|
where c.id = :id
|
||||||
""")
|
""")
|
||||||
Optional<Cart> findByIdFetchAll(@Param("id") Long id);
|
Optional<Cart> findByIdFetchAll(@Param("id") Long id);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Transactional
|
||||||
|
@Query("""
|
||||||
|
UPDATE Cart c
|
||||||
|
SET c.status = 'ABANDONED'
|
||||||
|
WHERE c.status = 'ACTIVE'
|
||||||
|
AND c.updatedAt < :limite
|
||||||
|
""")
|
||||||
|
int markOldCartsAsAbandoned(LocalDateTime limite);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,14 @@ public class CartService {
|
|||||||
this.pedidoService = pedidoService;
|
this.pedidoService = pedidoService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Cart findById(Long cartId) {
|
||||||
|
return cartRepo.findById(cartId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Devuelve el carrito activo o lo crea si no existe. */
|
/** Devuelve el carrito activo o lo crea si no existe. */
|
||||||
@Transactional
|
@Transactional
|
||||||
public Cart getOrCreateActiveCart(Long userId) {
|
public Cart getOrCreateActiveCart(Long userId) {
|
||||||
@ -136,6 +144,14 @@ public class CartService {
|
|||||||
cartRepo.save(cart);
|
cartRepo.save(cart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void lockCartById(Long cartId) {
|
||||||
|
Cart cart = cartRepo.findById(cartId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||||
|
cart.setStatus(Cart.Status.LOCKED);
|
||||||
|
cartRepo.save(cart);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public long countItems(Long userId) {
|
public long countItems(Long userId) {
|
||||||
Cart cart = getOrCreateActiveCart(userId);
|
Cart cart = getOrCreateActiveCart(userId);
|
||||||
@ -293,6 +309,7 @@ public class CartService {
|
|||||||
summary.put("total", Utils.formatCurrency(total, locale));
|
summary.put("total", Utils.formatCurrency(total, locale));
|
||||||
summary.put("amountCents", Math.round(total * 100));
|
summary.put("amountCents", Math.round(total * 100));
|
||||||
summary.put("errorShipmentCost", errorShipementCost);
|
summary.put("errorShipmentCost", errorShipementCost);
|
||||||
|
summary.put("cartId", cart.getId());
|
||||||
|
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import java.math.BigDecimal;
|
|||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -320,4 +322,12 @@ public class Utils {
|
|||||||
resumen.put("servicios", serviciosExtras);
|
resumen.put("servicios", serviciosExtras);
|
||||||
return resumen;
|
return resumen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String formatDateTime(LocalDateTime dateTime, Locale locale) {
|
||||||
|
if (dateTime == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", locale);
|
||||||
|
return dateTime.format(formatter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@ -1,9 +1,36 @@
|
|||||||
package com.imprimelibros.erp.payments;
|
package com.imprimelibros.erp.payments;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
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;
|
||||||
|
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.repo.PaymentTransactionRepository;
|
||||||
|
import com.imprimelibros.erp.users.User;
|
||||||
|
import com.imprimelibros.erp.users.UserDao;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@ -11,10 +38,87 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
@PreAuthorize("hasRole('SUPERADMIN')")
|
@PreAuthorize("hasRole('SUPERADMIN')")
|
||||||
public class PaymentController {
|
public class PaymentController {
|
||||||
|
|
||||||
|
protected final PaymentTransactionRepository repoPaymentTransaction;
|
||||||
|
protected final UserDao repoUser;
|
||||||
|
|
||||||
|
public PaymentController(PaymentTransactionRepository repoPaymentTransaction, UserDao repoUser) {
|
||||||
|
this.repoPaymentTransaction = repoPaymentTransaction;
|
||||||
|
this.repoUser = repoUser;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping()
|
@GetMapping()
|
||||||
public String index() {
|
public String index() {
|
||||||
return "imprimelibros/pagos/gestion-pagos";
|
return "imprimelibros/pagos/gestion-pagos";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping(value = "datatable/redsys", produces = "application/json")
|
||||||
|
@ResponseBody
|
||||||
|
public DataTablesResponse<Map<String, Object>> getDatatableRedsys(HttpServletRequest request,Locale locale) {
|
||||||
|
|
||||||
|
DataTablesRequest dt = DataTablesParser.from(request);
|
||||||
|
|
||||||
|
List<String> searchable = List.of(
|
||||||
|
);
|
||||||
|
|
||||||
|
List<String> orderable = List.of(
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
Specification<PaymentTransaction> base = Specification.allOf(
|
||||||
|
(root, query, cb) -> cb.equal(root.get("status"), PaymentTransactionStatus.succeeded));
|
||||||
|
|
||||||
|
Long total = repoPaymentTransaction.count(base);
|
||||||
|
|
||||||
|
return DataTable
|
||||||
|
.of(repoPaymentTransaction, PaymentTransaction.class, dt, searchable) // 'searchable' en DataTable.java
|
||||||
|
// edita columnas "reales":
|
||||||
|
.orderable(orderable)
|
||||||
|
.add("created_at", (pago) -> {
|
||||||
|
return 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> user = repoUser.findById(payment.getUserId());
|
||||||
|
return user.map(User::getFullName).orElse("");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.add("gateway_order_id", (pago) -> {
|
||||||
|
if (pago.getPayment() != null) {
|
||||||
|
return pago.getPayment().getGatewayOrderId();
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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("actions", (pago) -> {
|
||||||
|
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
||||||
|
" <a href=\"javascript:void(0);\" data-id=\"" + pago.getId()
|
||||||
|
+ "\" class=\"link-success btn-edit-pago fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
|
||||||
|
+ " <a href=\"javascript:void(0);\" data-id=\"" + pago.getId()
|
||||||
|
+ "\" class=\"link-danger btn-delete-pago fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>\n"
|
||||||
|
+ " </div>";
|
||||||
|
})
|
||||||
|
.where(base)
|
||||||
|
// Filtros custom:
|
||||||
|
.toJson(total);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package com.imprimelibros.erp.payments;
|
package com.imprimelibros.erp.payments;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.imprimelibros.erp.cart.Cart;
|
||||||
|
import com.imprimelibros.erp.cart.CartService;
|
||||||
import com.imprimelibros.erp.payments.model.*;
|
import com.imprimelibros.erp.payments.model.*;
|
||||||
import com.imprimelibros.erp.payments.repo.PaymentRepository;
|
import com.imprimelibros.erp.payments.repo.PaymentRepository;
|
||||||
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
||||||
@ -25,28 +27,36 @@ public class PaymentService {
|
|||||||
private final RedsysService redsysService;
|
private final RedsysService redsysService;
|
||||||
private final WebhookEventRepository webhookEventRepo;
|
private final WebhookEventRepository webhookEventRepo;
|
||||||
private final ObjectMapper om = new ObjectMapper();
|
private final ObjectMapper om = new ObjectMapper();
|
||||||
|
private final CartService cartService;
|
||||||
|
|
||||||
public PaymentService(PaymentRepository payRepo,
|
public PaymentService(PaymentRepository payRepo,
|
||||||
PaymentTransactionRepository txRepo,
|
PaymentTransactionRepository txRepo,
|
||||||
RefundRepository refundRepo,
|
RefundRepository refundRepo,
|
||||||
RedsysService redsysService,
|
RedsysService redsysService,
|
||||||
WebhookEventRepository webhookEventRepo) {
|
WebhookEventRepository webhookEventRepo, CartService cartService) {
|
||||||
this.payRepo = payRepo;
|
this.payRepo = payRepo;
|
||||||
this.txRepo = txRepo;
|
this.txRepo = txRepo;
|
||||||
this.refundRepo = refundRepo;
|
this.refundRepo = refundRepo;
|
||||||
this.redsysService = redsysService;
|
this.redsysService = redsysService;
|
||||||
this.webhookEventRepo = webhookEventRepo;
|
this.webhookEventRepo = webhookEventRepo;
|
||||||
|
this.cartService = cartService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crea el Payment en BD y construye el formulario de Redsys usando la API
|
* Crea el Payment en BD y construye el formulario de Redsys usando la API
|
||||||
* oficial (ApiMacSha256).
|
* oficial (ApiMacSha256).
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public FormPayload createRedsysPayment(Long orderId, long amountCents, String currency, String method)
|
public FormPayload createRedsysPayment(Long cartId, long amountCents, String currency, String method)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
Payment p = new Payment();
|
Payment p = new Payment();
|
||||||
p.setOrderId(orderId);
|
p.setOrderId(null);
|
||||||
|
|
||||||
|
Cart cart = this.cartService.findById(cartId);
|
||||||
|
if(cart != null && cart.getUserId() != null) {
|
||||||
|
p.setUserId(cart.getUserId());
|
||||||
|
}
|
||||||
p.setCurrency(currency);
|
p.setCurrency(currency);
|
||||||
p.setAmountTotalCents(amountCents);
|
p.setAmountTotalCents(amountCents);
|
||||||
p.setGateway("redsys");
|
p.setGateway("redsys");
|
||||||
@ -64,7 +74,7 @@ public class PaymentService {
|
|||||||
payRepo.save(p);
|
payRepo.save(p);
|
||||||
|
|
||||||
RedsysService.PaymentRequest req = new RedsysService.PaymentRequest(dsOrder, amountCents,
|
RedsysService.PaymentRequest req = new RedsysService.PaymentRequest(dsOrder, amountCents,
|
||||||
"Compra en Imprimelibros");
|
"Compra en Imprimelibros", cartId);
|
||||||
|
|
||||||
if ("bizum".equalsIgnoreCase(method)) {
|
if ("bizum".equalsIgnoreCase(method)) {
|
||||||
return redsysService.buildRedirectFormBizum(req);
|
return redsysService.buildRedirectFormBizum(req);
|
||||||
@ -187,8 +197,22 @@ public class PaymentService {
|
|||||||
p.setStatus(PaymentStatus.failed);
|
p.setStatus(PaymentStatus.failed);
|
||||||
p.setFailedAt(LocalDateTime.now());
|
p.setFailedAt(LocalDateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(authorized) {
|
||||||
|
// GENERAR PEDIDO A PARTIR DEL CARRITO
|
||||||
|
Cart cart = this.cartService.findById(notif.cartId);
|
||||||
|
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());
|
||||||
|
// p.setOrderId(orderId);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
payRepo.save(p);
|
payRepo.save(p);
|
||||||
|
|
||||||
|
|
||||||
if (!authorized) {
|
if (!authorized) {
|
||||||
ev.setLastError("Payment declined (Ds_Response=" + notif.response + ")");
|
ev.setLastError("Payment declined (Ds_Response=" + notif.response + ")");
|
||||||
@ -262,9 +286,15 @@ public class PaymentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Payment createBankTransferPayment(Long orderId, long amountCents, String currency) {
|
public Payment createBankTransferPayment(Long cartId, long amountCents, String currency) {
|
||||||
Payment p = new Payment();
|
Payment p = new Payment();
|
||||||
p.setOrderId(orderId);
|
p.setOrderId(null);
|
||||||
|
|
||||||
|
Cart cart = this.cartService.findById(cartId);
|
||||||
|
if(cart != null && cart.getUserId() != null) {
|
||||||
|
p.setUserId(cart.getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
p.setCurrency(currency);
|
p.setCurrency(currency);
|
||||||
p.setAmountTotalCents(amountCents);
|
p.setAmountTotalCents(amountCents);
|
||||||
p.setGateway("bank_transfer");
|
p.setGateway("bank_transfer");
|
||||||
|
|||||||
@ -6,10 +6,11 @@ import com.imprimelibros.erp.payments.model.PaymentTransactionStatus;
|
|||||||
import com.imprimelibros.erp.payments.model.PaymentTransactionType;
|
import com.imprimelibros.erp.payments.model.PaymentTransactionType;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface PaymentTransactionRepository extends JpaRepository<PaymentTransaction, Long> {
|
public interface PaymentTransactionRepository extends JpaRepository<PaymentTransaction, Long>, JpaSpecificationExecutor<PaymentTransaction> {
|
||||||
Optional<PaymentTransaction> findByGatewayTransactionId(String gatewayTransactionId);
|
Optional<PaymentTransaction> findByGatewayTransactionId(String gatewayTransactionId);
|
||||||
Optional<PaymentTransaction> findByIdempotencyKey(String idempotencyKey);
|
Optional<PaymentTransaction> findByIdempotencyKey(String idempotencyKey);
|
||||||
Optional<PaymentTransaction> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc(
|
Optional<PaymentTransaction> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc(
|
||||||
|
|||||||
@ -4,12 +4,16 @@ import com.imprimelibros.erp.payments.PaymentService;
|
|||||||
import com.imprimelibros.erp.payments.model.Payment;
|
import com.imprimelibros.erp.payments.model.Payment;
|
||||||
import com.imprimelibros.erp.redsys.RedsysService.FormPayload;
|
import com.imprimelibros.erp.redsys.RedsysService.FormPayload;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@ -17,19 +21,21 @@ import java.util.UUID;
|
|||||||
public class RedsysController {
|
public class RedsysController {
|
||||||
|
|
||||||
private final PaymentService paymentService;
|
private final PaymentService paymentService;
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
|
||||||
public RedsysController(PaymentService paymentService) {
|
public RedsysController(PaymentService paymentService, MessageSource messageSource) {
|
||||||
this.paymentService = paymentService;
|
this.paymentService = paymentService;
|
||||||
|
this.messageSource = messageSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/crear", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
@PostMapping(value = "/crear", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public ResponseEntity<byte[]> crearPago(@RequestParam("amountCents") Long amountCents,
|
public ResponseEntity<byte[]> crearPago(@RequestParam("amountCents") Long amountCents,
|
||||||
@RequestParam("method") String method) throws Exception {
|
@RequestParam("method") String method, @RequestParam("cartId") Long cartId) throws Exception {
|
||||||
|
|
||||||
if ("bank-transfer".equalsIgnoreCase(method)) {
|
if ("bank-transfer".equalsIgnoreCase(method)) {
|
||||||
// 1) Creamos el Payment interno SIN orderId (null)
|
// 1) Creamos el Payment interno SIN orderId (null)
|
||||||
Payment p = paymentService.createBankTransferPayment(null, amountCents, "EUR");
|
Payment p = paymentService.createBankTransferPayment(cartId, amountCents, "EUR");
|
||||||
|
|
||||||
// 2) Mostramos instrucciones de transferencia
|
// 2) Mostramos instrucciones de transferencia
|
||||||
String html = """
|
String html = """
|
||||||
@ -55,7 +61,7 @@ public class RedsysController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tarjeta o Bizum (Redsys)
|
// Tarjeta o Bizum (Redsys)
|
||||||
FormPayload form = paymentService.createRedsysPayment(null, amountCents, "EUR", method);
|
FormPayload form = paymentService.createRedsysPayment(cartId, amountCents, "EUR", method);
|
||||||
|
|
||||||
String html = """
|
String html = """
|
||||||
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>
|
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>
|
||||||
@ -64,6 +70,7 @@ public class RedsysController {
|
|||||||
<input type="hidden" name="Ds_SignatureVersion" value="%s"/>
|
<input type="hidden" name="Ds_SignatureVersion" value="%s"/>
|
||||||
<input type="hidden" name="Ds_MerchantParameters" value="%s"/>
|
<input type="hidden" name="Ds_MerchantParameters" value="%s"/>
|
||||||
<input type="hidden" name="Ds_Signature" value="%s"/>
|
<input type="hidden" name="Ds_Signature" value="%s"/>
|
||||||
|
<input type="hidden" name="cartId" value="%d"/>
|
||||||
<noscript>
|
<noscript>
|
||||||
<p>Haz clic en pagar para continuar</p>
|
<p>Haz clic en pagar para continuar</p>
|
||||||
<button type="submit">Pagar</button>
|
<button type="submit">Pagar</button>
|
||||||
@ -74,7 +81,7 @@ public class RedsysController {
|
|||||||
form.action(),
|
form.action(),
|
||||||
form.signatureVersion(),
|
form.signatureVersion(),
|
||||||
form.merchantParameters(),
|
form.merchantParameters(),
|
||||||
form.signature());
|
form.signature(), cartId);
|
||||||
|
|
||||||
byte[] body = html.getBytes(StandardCharsets.UTF_8);
|
byte[] body = html.getBytes(StandardCharsets.UTF_8);
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
@ -84,14 +91,11 @@ public class RedsysController {
|
|||||||
|
|
||||||
// GET: cuando el usuario cae aquí sin parámetros, o Redsys redirige por GET
|
// GET: cuando el usuario cae aquí sin parámetros, o Redsys redirige por GET
|
||||||
@GetMapping("/ok")
|
@GetMapping("/ok")
|
||||||
@ResponseBody
|
public String okGet(RedirectAttributes redirectAttrs, Model model, Locale locale) {
|
||||||
public ResponseEntity<String> okGet() {
|
String msg = messageSource.getMessage("checkout.success.payment", null, "Pago realizado con éxito. Gracias por su compra.", locale);
|
||||||
String html = """
|
model.addAttribute("successPago", msg);
|
||||||
<h2>Pago procesado</h2>
|
redirectAttrs.addFlashAttribute("successPago", msg);
|
||||||
<p>Si el pago ha sido autorizado, verás el pedido en tu área de usuario o recibirás un email de confirmación.</p>
|
return "redirect:/cart";
|
||||||
<p><a href="/cart">Volver a la tienda</a></p>
|
|
||||||
""";
|
|
||||||
return ResponseEntity.ok(html);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: si Redsys envía Ds_Signature y Ds_MerchantParameters (muchas
|
// POST: si Redsys envía Ds_Signature y Ds_MerchantParameters (muchas
|
||||||
@ -111,9 +115,12 @@ public class RedsysController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/ko")
|
@GetMapping("/ko")
|
||||||
@ResponseBody
|
public String koGet(RedirectAttributes redirectAttrs, Model model, Locale locale) {
|
||||||
public ResponseEntity<String> koGet() {
|
|
||||||
return ResponseEntity.ok("<h2>Pago cancelado o rechazado</h2><a href=\"/checkout\">Volver</a>");
|
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)
|
@PostMapping(value = "/ko", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.imprimelibros.erp.redsys;
|
package com.imprimelibros.erp.redsys;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import sis.redsys.api.ApiMacSha256;
|
import sis.redsys.api.ApiMacSha256;
|
||||||
@ -38,7 +39,7 @@ public class RedsysService {
|
|||||||
|
|
||||||
// ---------- RECORDS ----------
|
// ---------- RECORDS ----------
|
||||||
// Pedido a Redsys
|
// Pedido a Redsys
|
||||||
public record PaymentRequest(String order, long amountCents, String description) {
|
public record PaymentRequest(String order, long amountCents, String description, Long cartId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payload para el formulario
|
// Payload para el formulario
|
||||||
@ -69,6 +70,13 @@ public class RedsysService {
|
|||||||
api.setParameter("DS_MERCHANT_URLOK", urlOk);
|
api.setParameter("DS_MERCHANT_URLOK", urlOk);
|
||||||
api.setParameter("DS_MERCHANT_URLKO", urlKo);
|
api.setParameter("DS_MERCHANT_URLKO", urlKo);
|
||||||
|
|
||||||
|
// ✅ Añadir contexto adicional (por ejemplo, cartId)
|
||||||
|
// Si tu PaymentRequest no lo lleva todavía, puedes pasarlo en description o
|
||||||
|
// crear otro campo.
|
||||||
|
JSONObject ctx = new JSONObject();
|
||||||
|
ctx.put("cartId", req.cartId()); // o req.cartId() si decides añadirlo al record
|
||||||
|
api.setParameter("DS_MERCHANT_MERCHANTDATA", ctx.toString());
|
||||||
|
|
||||||
if (req.description() != null && !req.description().isBlank()) {
|
if (req.description() != null && !req.description().isBlank()) {
|
||||||
api.setParameter("DS_MERCHANT_PRODUCTDESCRIPTION", req.description());
|
api.setParameter("DS_MERCHANT_PRODUCTDESCRIPTION", req.description());
|
||||||
}
|
}
|
||||||
@ -110,7 +118,7 @@ public class RedsysService {
|
|||||||
|
|
||||||
// 1) Decodificar Ds_MerchantParameters usando la librería oficial
|
// 1) Decodificar Ds_MerchantParameters usando la librería oficial
|
||||||
String json = api.decodeMerchantParameters(dsMerchantParametersB64);
|
String json = api.decodeMerchantParameters(dsMerchantParametersB64);
|
||||||
|
|
||||||
// 2) Convertir a Map para tu modelo
|
// 2) Convertir a Map para tu modelo
|
||||||
Map<String, Object> mp = MAPPER.readValue(json, new TypeReference<>() {
|
Map<String, Object> mp = MAPPER.readValue(json, new TypeReference<>() {
|
||||||
});
|
});
|
||||||
@ -174,6 +182,7 @@ public class RedsysService {
|
|||||||
public final String response;
|
public final String response;
|
||||||
public final long amountCents;
|
public final long amountCents;
|
||||||
public final String currency;
|
public final String currency;
|
||||||
|
public final Long cartId;
|
||||||
|
|
||||||
public RedsysNotification(Map<String, Object> raw) {
|
public RedsysNotification(Map<String, Object> raw) {
|
||||||
this.raw = raw;
|
this.raw = raw;
|
||||||
@ -181,6 +190,24 @@ public class RedsysService {
|
|||||||
this.response = str(raw.get("Ds_Response"));
|
this.response = str(raw.get("Ds_Response"));
|
||||||
this.currency = str(raw.get("Ds_Currency"));
|
this.currency = str(raw.get("Ds_Currency"));
|
||||||
this.amountCents = parseLongSafe(raw.get("Ds_Amount"));
|
this.amountCents = parseLongSafe(raw.get("Ds_Amount"));
|
||||||
|
this.cartId = extractCartId(raw.get("Ds_MerchantData"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long extractCartId(Object merchantDataObj) {
|
||||||
|
if (merchantDataObj == null)
|
||||||
|
return null;
|
||||||
|
try {
|
||||||
|
String json = String.valueOf(merchantDataObj);
|
||||||
|
|
||||||
|
// 👇 DES-ESCAPAR las comillas HTML que vienen de Redsys
|
||||||
|
json = json.replace(""", "\"");
|
||||||
|
|
||||||
|
org.json.JSONObject ctx = new org.json.JSONObject(json);
|
||||||
|
return ctx.optLong("cartId", 0L);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace(); // te ayudará si vuelve a fallar
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean authorized() {
|
public boolean authorized() {
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
databaseChangeLog:
|
||||||
|
- changeSet:
|
||||||
|
id: 0008-update-cart-status-constraint
|
||||||
|
author: jjo
|
||||||
|
changes:
|
||||||
|
# 1) Eliminar el índice único antiguo (user_id, status)
|
||||||
|
- sql:
|
||||||
|
sql: |
|
||||||
|
ALTER TABLE carts
|
||||||
|
DROP INDEX uq_carts_user_active;
|
||||||
|
|
||||||
|
# 2) Añadir columna generada 'active_flag'
|
||||||
|
# Será 1 si status = 'ACTIVE', y NULL en cualquier otro caso
|
||||||
|
- sql:
|
||||||
|
sql: |
|
||||||
|
ALTER TABLE carts
|
||||||
|
ADD COLUMN active_flag TINYINT(1)
|
||||||
|
GENERATED ALWAYS AS (
|
||||||
|
CASE WHEN status = 'ACTIVE' THEN 1 ELSE NULL END
|
||||||
|
);
|
||||||
|
|
||||||
|
# 3) Crear el nuevo índice único:
|
||||||
|
# solo limita (user_id, active_flag=1),
|
||||||
|
# se permiten muchos registros con active_flag NULL (LOCKED, COMPLETED, etc.)
|
||||||
|
- sql:
|
||||||
|
sql: |
|
||||||
|
CREATE UNIQUE INDEX uq_carts_user_active
|
||||||
|
ON carts (user_id, active_flag);
|
||||||
@ -12,4 +12,6 @@ databaseChangeLog:
|
|||||||
- include:
|
- include:
|
||||||
file: db/changelog/changesets/0006-add-cart-direcciones.yml
|
file: db/changelog/changesets/0006-add-cart-direcciones.yml
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/changesets/0007-payments-core.yml
|
file: db/changelog/changesets/0007-payments-core.yml
|
||||||
|
- include:
|
||||||
|
file: db/changelog/changesets/0008-update-cart-status-constraint.yml
|
||||||
@ -1,4 +1,12 @@
|
|||||||
pagos.module-title=Gestión de Pagos
|
pagos.module-title=Gestión de Pagos
|
||||||
|
|
||||||
pagos.tab.movimientos-redsys=Movimientos Redsys
|
pagos.tab.movimientos-redsys=Movimientos Redsys
|
||||||
pagos.tab.transferencias-bancarias=Transferencias Bancarias
|
pagos.tab.transferencias-bancarias=Transferencias Bancarias
|
||||||
|
|
||||||
|
pagos.table.cliente.nombre=Nombre Cliente
|
||||||
|
pagos.table.redsys.id=Cod. Redsys
|
||||||
|
pagos.table.pedido.id=Pedido
|
||||||
|
pagos.table.cantidad=Cantidad
|
||||||
|
pagos.table.fecha=Fecha
|
||||||
|
pagos.table.estado=Estado
|
||||||
|
pagos.table.acciones=Acciones
|
||||||
|
|||||||
@ -12,5 +12,7 @@ checkout.billing-address.errors.noAddressSelected=Debe seleccionar una direcció
|
|||||||
checkout.payment.card=Tarjeta de crédito / débito
|
checkout.payment.card=Tarjeta de crédito / débito
|
||||||
checkout.payment.bizum=Bizum
|
checkout.payment.bizum=Bizum
|
||||||
checkout.payment.bank-transfer=Transferencia bancaria
|
checkout.payment.bank-transfer=Transferencia bancaria
|
||||||
|
checkout.error.payment=Error al procesar el pago: el pago ha sido cancelado o rechazado Por favor, inténtelo de nuevo.
|
||||||
|
checkout.success.payment=Pago realizado con éxito. Gracias por su compra.
|
||||||
|
|
||||||
checkout.make-payment=Realizar el pago
|
checkout.make-payment=Realizar el pago
|
||||||
@ -57,3 +57,13 @@ body {
|
|||||||
color: #92b2a7;
|
color: #92b2a7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-fadeout {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 1s ease;
|
||||||
|
animation: fadeout 4s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
0%, 70% { opacity: 1; }
|
||||||
|
100% { opacity: 0; }
|
||||||
|
}
|
||||||
@ -256,4 +256,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Oculta los alerts cuando se termina la animacion:
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.querySelectorAll('.alert-fadeout').forEach(alert => {
|
||||||
|
alert.addEventListener('animationend', () => {
|
||||||
|
alert.classList.add('d-none');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -1,3 +1,60 @@
|
|||||||
$(()=>{
|
$(() => {
|
||||||
|
|
||||||
|
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||||
|
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||||
|
if (window.$ && csrfToken && csrfHeader) {
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function (xhr) {
|
||||||
|
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = document.documentElement.lang || 'es-ES';
|
||||||
|
|
||||||
|
// Comprueba dependencias antes de iniciar
|
||||||
|
if (!window.DataTable) {
|
||||||
|
console.error('DataTables no está cargado aún');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = new DataTable('#pagos-redsys-datatable', {
|
||||||
|
processing: true,
|
||||||
|
serverSide: true,
|
||||||
|
orderCellsTop: true,
|
||||||
|
pageLength: 50,
|
||||||
|
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||||
|
responsive: true,
|
||||||
|
dom: 'lrBtip',
|
||||||
|
buttons: {
|
||||||
|
dom: {
|
||||||
|
button: {
|
||||||
|
className: 'btn btn-sm btn-outline-primary me-1'
|
||||||
|
},
|
||||||
|
buttons: [
|
||||||
|
{ extend: 'copy' },
|
||||||
|
{ extend: 'csv' },
|
||||||
|
{ extend: 'excel' },
|
||||||
|
{ extend: 'pdf' },
|
||||||
|
{ extend: 'print' },
|
||||||
|
{ extend: 'colvis' }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ajax: {
|
||||||
|
url: '/pagos/datatable/redsys',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
order: [[4, 'asc']], // 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' }
|
||||||
|
],
|
||||||
|
columnDefs: [{ targets: -1, orderable: false, searchable: false }]
|
||||||
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
@ -7,9 +7,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="errorEnvio" th:class="${'alert alert-danger' + (errorEnvio ? '' : ' d-none')}" role="alert"
|
<div th:if="${errorPago}" class="alert alert-danger alert-fadeout my-1" role="alert" th:text="${errorPago}"></div>
|
||||||
|
|
||||||
|
<div id="errorEnvio" th:class="${'alert alert-danger my-1' + (errorEnvio ? '' : ' d-none')}" role="alert"
|
||||||
th:text="#{cart.errors.shipping}"></div>
|
th:text="#{cart.errors.shipping}"></div>
|
||||||
<div th:if="${!#strings.isEmpty(errorMessage) and items != null and !items.isEmpty()}" class="alert alert-danger "
|
<div th:if="${!#strings.isEmpty(errorMessage) and items != null and !items.isEmpty()}" class="alert alert-danger my-1 "
|
||||||
role="alert" th:text="${errorMessage}"></div>
|
role="alert" th:text="${errorMessage}"></div>
|
||||||
|
|
||||||
<div class="alert alert-danger alert-shipment d-none" role="alert"
|
<div class="alert alert-danger alert-shipment d-none" role="alert"
|
||||||
|
|||||||
@ -35,7 +35,9 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div th:if="${successPago}" class="alert alert-success alert-fadeout my-1" role="alert" th:text="${successPago}"></div>
|
||||||
|
|
||||||
<div th:if="${items.isEmpty()}">
|
<div th:if="${items.isEmpty()}">
|
||||||
<div id="alert-empty"class="alert alert-info" role="alert" th:text="#{cart.empty}"></div>
|
<div id="alert-empty"class="alert alert-info" role="alert" th:text="#{cart.empty}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -41,6 +41,7 @@
|
|||||||
<form th:action="@{/pagos/redsys/crear}" method="post">
|
<form th:action="@{/pagos/redsys/crear}" method="post">
|
||||||
<input type="hidden" name="amountCents" th:value="${summary.amountCents}" />
|
<input type="hidden" name="amountCents" th:value="${summary.amountCents}" />
|
||||||
<input type="hidden" name="method" value="card"/>
|
<input type="hidden" name="method" value="card"/>
|
||||||
|
<input type="hidden" name="cartId" th:value="${summary.cartId}" />
|
||||||
<button id="btn-checkout" type="submit" class="btn btn-secondary w-100 mt-2"
|
<button id="btn-checkout" type="submit" class="btn btn-secondary w-100 mt-2"
|
||||||
th:text="#{checkout.make-payment}" disabled>Checkout</button>
|
th:text="#{checkout.make-payment}" disabled>Checkout</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -62,11 +62,10 @@
|
|||||||
<!-- Tab panes -->
|
<!-- Tab panes -->
|
||||||
<div class="tab-content text-muted">
|
<div class="tab-content text-muted">
|
||||||
<div class="tab-pane active show" id="arrow-redsys" role="tabpanel">
|
<div class="tab-pane active show" id="arrow-redsys" role="tabpanel">
|
||||||
<!---
|
|
||||||
<div
|
<div th:insert="~{imprimelibros/pagos/tabla-redsys :: tabla-redsys}">
|
||||||
th:insert="~{imprimelibros/presupuestos/presupuesto-list-items/tabla-cliente :: tabla-cliente}">
|
</div>
|
||||||
</div>
|
|
||||||
--->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="arrow-transferencias" role="tabpanel">
|
<div class="tab-pane" id="arrow-transferencias" role="tabpanel">
|
||||||
<!---
|
<!---
|
||||||
@ -89,6 +88,19 @@
|
|||||||
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script th:src="@{/assets/libs/datatables/datatables.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/dataTables.bootstrap5.min.js}"></script>
|
||||||
|
|
||||||
|
<!-- JS de Buttons y dependencias -->
|
||||||
|
<script th:src="@{/assets/libs/datatables/dataTables.buttons.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/jszip/jszip.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/pdfmake/pdfmake.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/pdfmake/vfs_fonts.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/buttons.html5.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
|
||||||
|
|
||||||
|
|
||||||
<script type="module" th:src="@{/assets/js/pages/imprimelibros/pagos/pagos.js}"></script>
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/pagos/pagos.js}"></script>
|
||||||
</th:block>
|
</th:block>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
<div th:fragment="tabla-redsys">
|
||||||
|
<table id="pagos-redsys-datatable" class="table table-striped table-nowrap responsive w-100">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" th:text="#{pagos.table.cliente.nombre}">ID</th>
|
||||||
|
<th scope="col" th:text="#{pagos.table.redsys.id}">Cliente</th>
|
||||||
|
<th scope="col" th:text="#{pagos.table.pedido.id}">Pedido</th>
|
||||||
|
<th scope="col" th:text="#{pagos.table.cantidad}">Cantidad</th>
|
||||||
|
<th scope="col" th:text="#{pagos.table.fecha}">Fecha</th>
|
||||||
|
<th scope="col" th:text="#{pagos.table.acciones}">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="user.fullName" /></th>
|
||||||
|
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="user.fullName" /></th>
|
||||||
|
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="payments.gateway_order_id" /></th>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
<th></th> <!-- Acciones (sin filtro) -->
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user