diff --git a/src/main/java/com/imprimelibros/erp/ErpApplication.java b/src/main/java/com/imprimelibros/erp/ErpApplication.java index 0e589a8..97471b8 100644 --- a/src/main/java/com/imprimelibros/erp/ErpApplication.java +++ b/src/main/java/com/imprimelibros/erp/ErpApplication.java @@ -3,8 +3,10 @@ package com.imprimelibros.erp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling @ConfigurationPropertiesScan(basePackages = "com.imprimelibros.erp") public class ErpApplication { diff --git a/src/main/java/com/imprimelibros/erp/cart/CartCleanupService.java b/src/main/java/com/imprimelibros/erp/cart/CartCleanupService.java new file mode 100644 index 0000000..28af416 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/CartCleanupService.java @@ -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); + } +} diff --git a/src/main/java/com/imprimelibros/erp/cart/CartRepository.java b/src/main/java/com/imprimelibros/erp/cart/CartRepository.java index 481ff51..1547391 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartRepository.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartRepository.java @@ -1,8 +1,12 @@ package com.imprimelibros.erp.cart; 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.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; import java.util.Optional; public interface CartRepository extends JpaRepository { @@ -17,5 +21,15 @@ public interface CartRepository extends JpaRepository { where c.id = :id """) Optional 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); } diff --git a/src/main/java/com/imprimelibros/erp/cart/CartService.java b/src/main/java/com/imprimelibros/erp/cart/CartService.java index f900791..7b53eda 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartService.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartService.java @@ -53,6 +53,14 @@ public class CartService { 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. */ @Transactional public Cart getOrCreateActiveCart(Long userId) { @@ -136,6 +144,14 @@ public class CartService { 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 public long countItems(Long userId) { Cart cart = getOrCreateActiveCart(userId); @@ -293,6 +309,7 @@ public class CartService { summary.put("total", Utils.formatCurrency(total, locale)); summary.put("amountCents", Math.round(total * 100)); summary.put("errorShipmentCost", errorShipementCost); + summary.put("cartId", cart.getId()); return summary; } diff --git a/src/main/java/com/imprimelibros/erp/common/Utils.java b/src/main/java/com/imprimelibros/erp/common/Utils.java index a5262b7..d5aae69 100644 --- a/src/main/java/com/imprimelibros/erp/common/Utils.java +++ b/src/main/java/com/imprimelibros/erp/common/Utils.java @@ -4,6 +4,8 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.security.Principal; import java.text.NumberFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -320,4 +322,12 @@ public class Utils { resumen.put("servicios", serviciosExtras); 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); + } } diff --git a/src/main/java/com/imprimelibros/erp/pagos imprimelibros.zip b/src/main/java/com/imprimelibros/erp/pagos imprimelibros.zip deleted file mode 100644 index a23c4c8..0000000 Binary files a/src/main/java/com/imprimelibros/erp/pagos imprimelibros.zip and /dev/null differ diff --git a/src/main/java/com/imprimelibros/erp/payments/PaymentController.java b/src/main/java/com/imprimelibros/erp/payments/PaymentController.java index 85f946d..2cfe1ca 100644 --- a/src/main/java/com/imprimelibros/erp/payments/PaymentController.java +++ b/src/main/java/com/imprimelibros/erp/payments/PaymentController.java @@ -1,9 +1,36 @@ 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.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; +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 @@ -11,10 +38,87 @@ import org.springframework.web.bind.annotation.GetMapping; @PreAuthorize("hasRole('SUPERADMIN')") public class PaymentController { + protected final PaymentTransactionRepository repoPaymentTransaction; + protected final UserDao repoUser; + + public PaymentController(PaymentTransactionRepository repoPaymentTransaction, UserDao repoUser) { + this.repoPaymentTransaction = repoPaymentTransaction; + this.repoUser = repoUser; + } + @GetMapping() public String index() { return "imprimelibros/pagos/gestion-pagos"; } + + @GetMapping(value = "datatable/redsys", produces = "application/json") + @ResponseBody + public DataTablesResponse> getDatatableRedsys(HttpServletRequest request,Locale locale) { + + DataTablesRequest dt = DataTablesParser.from(request); + + List searchable = List.of( + ); + + List orderable = List.of( + + ); + + Specification 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 = 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 "
\n" + + " \n" + + " \n" + + "
"; + }) + .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 bba6420..0b98dc5 100644 --- a/src/main/java/com/imprimelibros/erp/payments/PaymentService.java +++ b/src/main/java/com/imprimelibros/erp/payments/PaymentService.java @@ -1,6 +1,8 @@ package com.imprimelibros.erp.payments; 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.repo.PaymentRepository; import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository; @@ -25,28 +27,36 @@ public class PaymentService { private final RedsysService redsysService; private final WebhookEventRepository webhookEventRepo; private final ObjectMapper om = new ObjectMapper(); + private final CartService cartService; public PaymentService(PaymentRepository payRepo, PaymentTransactionRepository txRepo, RefundRepository refundRepo, RedsysService redsysService, - WebhookEventRepository webhookEventRepo) { + WebhookEventRepository webhookEventRepo, CartService cartService) { this.payRepo = payRepo; this.txRepo = txRepo; this.refundRepo = refundRepo; this.redsysService = redsysService; this.webhookEventRepo = webhookEventRepo; + this.cartService = cartService; } + /** * Crea el Payment en BD y construye el formulario de Redsys usando la API * oficial (ApiMacSha256). */ @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 { 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.setAmountTotalCents(amountCents); p.setGateway("redsys"); @@ -64,7 +74,7 @@ public class PaymentService { payRepo.save(p); RedsysService.PaymentRequest req = new RedsysService.PaymentRequest(dsOrder, amountCents, - "Compra en Imprimelibros"); + "Compra en Imprimelibros", cartId); if ("bizum".equalsIgnoreCase(method)) { return redsysService.buildRedirectFormBizum(req); @@ -187,8 +197,22 @@ public class PaymentService { p.setStatus(PaymentStatus.failed); 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); + if (!authorized) { ev.setLastError("Payment declined (Ds_Response=" + notif.response + ")"); @@ -262,9 +286,15 @@ public class PaymentService { } @Transactional - public Payment createBankTransferPayment(Long orderId, long amountCents, String currency) { + public Payment createBankTransferPayment(Long cartId, long amountCents, String currency) { 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.setAmountTotalCents(amountCents); p.setGateway("bank_transfer"); diff --git a/src/main/java/com/imprimelibros/erp/payments/repo/PaymentTransactionRepository.java b/src/main/java/com/imprimelibros/erp/payments/repo/PaymentTransactionRepository.java index aac12ad..e0eb955 100644 --- a/src/main/java/com/imprimelibros/erp/payments/repo/PaymentTransactionRepository.java +++ b/src/main/java/com/imprimelibros/erp/payments/repo/PaymentTransactionRepository.java @@ -6,10 +6,11 @@ import com.imprimelibros.erp.payments.model.PaymentTransactionStatus; import com.imprimelibros.erp.payments.model.PaymentTransactionType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import java.util.Optional; -public interface PaymentTransactionRepository extends JpaRepository { +public interface PaymentTransactionRepository extends JpaRepository, JpaSpecificationExecutor { Optional findByGatewayTransactionId(String gatewayTransactionId); Optional findByIdempotencyKey(String idempotencyKey); Optional findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc( diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java index b0bcf07..56a9e61 100644 --- a/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java +++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java @@ -4,12 +4,16 @@ import com.imprimelibros.erp.payments.PaymentService; import com.imprimelibros.erp.payments.model.Payment; import com.imprimelibros.erp.redsys.RedsysService.FormPayload; +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.UUID; @Controller @@ -17,19 +21,21 @@ import java.util.UUID; public class RedsysController { private final PaymentService paymentService; + private final MessageSource messageSource; - public RedsysController(PaymentService paymentService) { + public RedsysController(PaymentService paymentService, MessageSource messageSource) { this.paymentService = paymentService; + this.messageSource = messageSource; } @PostMapping(value = "/crear", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @ResponseBody public ResponseEntity 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)) { // 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 String html = """ @@ -55,7 +61,7 @@ public class RedsysController { } // Tarjeta o Bizum (Redsys) - FormPayload form = paymentService.createRedsysPayment(null, amountCents, "EUR", method); + FormPayload form = paymentService.createRedsysPayment(cartId, amountCents, "EUR", method); String html = """ Redirigiendo a Redsys… @@ -64,6 +70,7 @@ public class RedsysController { +