From 7516e9e91edd969b736cb45c350c149a5d99603c Mon Sep 17 00:00:00 2001 From: jjimenez Date: Tue, 4 Nov 2025 14:40:18 +0100 Subject: [PATCH] falta vista de pagos --- .../imprimelibros/erp/pagos imprimelibros.zip | Bin 0 -> 21240 bytes .../erp/payments/PaymentService.java | 289 ++++++++++++------ .../erp/payments/model/CaptureMethod.java | 3 +- .../erp/payments/model/IdempotencyKey.java | 66 ---- .../erp/payments/model/IdempotencyScope.java | 3 - .../erp/payments/model/Payment.java | 13 +- .../erp/payments/model/PaymentMethod.java | 100 ------ .../erp/payments/model/PaymentMethodType.java | 4 - .../erp/payments/model/PaymentStatus.java | 4 +- .../payments/model/PaymentTransaction.java | 9 + .../model/PaymentTransactionStatus.java | 2 +- .../erp/payments/model/Refund.java | 4 +- .../erp/payments/model/RefundReason.java | 2 +- .../erp/payments/model/RefundStatus.java | 2 +- .../erp/payments/model/ThreeDSStatus.java | 2 +- .../erp/payments/model/WebhookEvent.java | 8 + .../repo/IdempotencyKeyRepository.java | 12 - .../repo/PaymentMethodRepository.java | 7 - .../repo/PaymentTransactionRepository.java | 8 + .../erp/payments/repo/RefundRepository.java | 2 +- .../payments/repo/WebhookEventRepository.java | 7 +- .../erp/redsys/RedsysController.java | 23 +- .../erp/redsys/RedsysService.java | 26 +- .../changesets/0007-payments-core.yml | 122 +------- 24 files changed, 276 insertions(+), 442 deletions(-) create mode 100644 src/main/java/com/imprimelibros/erp/pagos imprimelibros.zip delete mode 100644 src/main/java/com/imprimelibros/erp/payments/model/IdempotencyKey.java delete mode 100644 src/main/java/com/imprimelibros/erp/payments/model/IdempotencyScope.java delete mode 100644 src/main/java/com/imprimelibros/erp/payments/model/PaymentMethod.java delete mode 100644 src/main/java/com/imprimelibros/erp/payments/model/PaymentMethodType.java delete mode 100644 src/main/java/com/imprimelibros/erp/payments/repo/IdempotencyKeyRepository.java delete mode 100644 src/main/java/com/imprimelibros/erp/payments/repo/PaymentMethodRepository.java diff --git a/src/main/java/com/imprimelibros/erp/pagos imprimelibros.zip b/src/main/java/com/imprimelibros/erp/pagos imprimelibros.zip new file mode 100644 index 0000000000000000000000000000000000000000..a23c4c8a8eaef3a5673a0fdd6c8dfcc95dbd16eb GIT binary patch literal 21240 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 literal 0 HcmV?d00001 diff --git a/src/main/java/com/imprimelibros/erp/payments/PaymentService.java b/src/main/java/com/imprimelibros/erp/payments/PaymentService.java index 573f0e2..ec64784 100644 --- a/src/main/java/com/imprimelibros/erp/payments/PaymentService.java +++ b/src/main/java/com/imprimelibros/erp/payments/PaymentService.java @@ -10,6 +10,7 @@ import com.imprimelibros.erp.redsys.RedsysService.FormPayload; import com.imprimelibros.erp.redsys.RedsysService.RedsysNotification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.imprimelibros.erp.payments.repo.WebhookEventRepository; import java.time.LocalDateTime; import java.util.Objects; @@ -22,16 +23,19 @@ public class PaymentService { private final PaymentTransactionRepository txRepo; private final RefundRepository refundRepo; private final RedsysService redsysService; + private final WebhookEventRepository webhookEventRepo; private final ObjectMapper om = new ObjectMapper(); public PaymentService(PaymentRepository payRepo, PaymentTransactionRepository txRepo, RefundRepository refundRepo, - RedsysService redsysService) { + RedsysService redsysService, + WebhookEventRepository webhookEventRepo) { this.payRepo = payRepo; this.txRepo = txRepo; this.refundRepo = refundRepo; this.redsysService = redsysService; + this.webhookEventRepo = webhookEventRepo; } /** @@ -42,15 +46,20 @@ public class PaymentService { public FormPayload createRedsysPayment(Long orderId, long amountCents, String currency, String method) throws Exception { Payment p = new Payment(); - p.setOrderId(orderId); // <- ahora puede ser null + p.setOrderId(orderId); p.setCurrency(currency); p.setAmountTotalCents(amountCents); p.setGateway("redsys"); - p.setStatus(PaymentStatus.REQUIRES_PAYMENT_METHOD); + p.setStatus(PaymentStatus.requires_payment_method); p = payRepo.saveAndFlush(p); - // Ds_Order = ID del Payment, 12 dígitos - String dsOrder = String.format("%012d", p.getId()); + // ANTES: + // String dsOrder = String.format("%012d", p.getId()); + + // AHORA: timestamp + long now = System.currentTimeMillis(); + String dsOrder = String.format("%012d", now % 1_000_000_000_000L); + p.setGatewayOrderId(dsOrder); payRepo.save(p); @@ -64,81 +73,137 @@ public class PaymentService { } } - // si aún tienes la versión antigua sin method, puedes dejar este overload si te - // viene bien: - @Transactional - public FormPayload createRedsysPayment(Long orderId, long amountCents, String currency) throws Exception { - return createRedsysPayment(orderId, amountCents, currency, "card"); - } - - /** - * Procesa una notificación Redsys (OK/notify) con la API oficial: - * - validateAndParseNotification usa createMerchantSignatureNotif + - * decodeMerchantParameters - */ @Transactional public void handleRedsysNotification(String dsSignature, String dsMerchantParameters) throws Exception { - RedsysNotification notif = redsysService.validateAndParseNotification(dsSignature, dsMerchantParameters); - // Log útil para depurar - System.out.println(">> Redsys notify: order=" + notif.order + - " amountCents=" + notif.amountCents + - " currency=" + notif.currency + - " response=" + notif.response); + // 0) Intentamos parsear la notificación. Si falla, registramos el webhook crudo + // y salimos. + RedsysNotification notif; + try { + notif = redsysService.validateAndParseNotification(dsSignature, dsMerchantParameters); + } catch (Exception ex) { + WebhookEvent e = new WebhookEvent(); + e.setProvider("redsys"); + e.setEventType("payment_notification_parse_error"); + e.setEventId("PARSE_ERROR_" + System.currentTimeMillis()); + e.setSignature(dsSignature); + e.setPayload(dsMerchantParameters); + e.setProcessed(false); + e.setAttempts(1); + e.setLastError("Error parsing/validating Redsys notification: " + ex.getMessage()); + webhookEventRepo.save(e); - Payment p = payRepo.findByGatewayAndGatewayOrderId("redsys", notif.order) - .orElseThrow(() -> new IllegalStateException("Payment no encontrado para Ds_Order " + notif.order)); - - // 🔹 Opción sencilla: sólo comprobar el importe - if (!Objects.equals(p.getAmountTotalCents(), notif.amountCents)) { - throw new IllegalStateException("Importe inesperado: esperado=" + - p.getAmountTotalCents() + " recibido=" + notif.amountCents); - } - - // Si quieres, puedes hacer un check mínimamente decente de divisa numérica: - // (si usas siempre EUR) - /* - * if (!"978".equals(notif.currency)) { - * throw new IllegalStateException("Divisa Redsys inesperada: " + - * notif.currency); - * } - */ - - // Idempotencia simple: si ya está capturado o reembolsado, no hacemos nada - if (p.getStatus() == PaymentStatus.CAPTURED - || p.getStatus() == PaymentStatus.PARTIALLY_REFUNDED - || p.getStatus() == PaymentStatus.REFUNDED) { + // IMPORTANTE: NO re-lanzamos la excepción + // Simplemente salimos. Así se hace commit de este insert. return; } - PaymentTransaction tx = new PaymentTransaction(); - tx.setPayment(p); - tx.setType(PaymentTransactionType.CAPTURE); - tx.setCurrency(p.getCurrency()); // "EUR" - tx.setAmountCents(notif.amountCents); - tx.setStatus(notif.authorized() - ? PaymentTransactionStatus.SUCCEEDED - : PaymentTransactionStatus.FAILED); + // 1) A partir de aquí, el parseo ha ido bien y tenemos notif.order, + // notif.amountCents, etc. + String provider = "redsys"; + String eventType = "payment_notification"; + String eventId = notif.order; - Object authCode = notif.raw.get("Ds_AuthorisationCode"); - tx.setGatewayTransactionId(authCode != null ? String.valueOf(authCode) : null); - tx.setGatewayResponseCode(notif.response); - tx.setResponsePayload(om.writeValueAsString(notif.raw)); - tx.setProcessedAt(LocalDateTime.now()); - txRepo.save(tx); + WebhookEvent ev = webhookEventRepo + .findByProviderAndEventId(provider, eventId) + .orElseGet(() -> { + WebhookEvent e = new WebhookEvent(); + e.setProvider(provider); + e.setEventType(eventType); + e.setEventId(eventId); + e.setSignature(dsSignature); + try { + e.setPayload(om.writeValueAsString(notif.raw)); + } catch (Exception ex) { + e.setPayload(dsMerchantParameters); + } + e.setProcessed(false); + e.setAttempts(0); + return webhookEventRepo.save(e); + }); - if (notif.authorized()) { - p.setAuthorizationCode(tx.getGatewayTransactionId()); - p.setStatus(PaymentStatus.CAPTURED); - p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents); - p.setAuthorizedAt(LocalDateTime.now()); - p.setCapturedAt(LocalDateTime.now()); - } else { - p.setStatus(PaymentStatus.FAILED); - p.setFailedAt(LocalDateTime.now()); + if (Boolean.TRUE.equals(ev.getProcessed())) { + return; } - payRepo.save(p); + Integer attempts = ev.getAttempts() == null ? 0 : ev.getAttempts(); + ev.setAttempts(attempts + 1); + ev.setLastError(null); + webhookEventRepo.save(ev); + + try { + Payment p = payRepo.findByGatewayAndGatewayOrderId("redsys", notif.order) + .orElseThrow(() -> new IllegalStateException("Payment no encontrado para Ds_Order " + notif.order)); + + if (!Objects.equals(p.getAmountTotalCents(), notif.amountCents)) { + throw new IllegalStateException("Importe inesperado: esperado=" + + p.getAmountTotalCents() + " recibido=" + notif.amountCents); + } + + if (p.getStatus() == PaymentStatus.captured + || p.getStatus() == PaymentStatus.partially_refunded + || p.getStatus() == PaymentStatus.refunded) { + ev.setProcessed(true); + ev.setProcessedAt(LocalDateTime.now()); + webhookEventRepo.save(ev); + return; + } + + boolean authorized = isRedsysAuthorized(notif); + + PaymentTransaction tx = new PaymentTransaction(); + tx.setPayment(p); + tx.setType(PaymentTransactionType.CAPTURE); + tx.setCurrency(p.getCurrency()); // "EUR" + tx.setAmountCents(notif.amountCents); + tx.setStatus(authorized + ? PaymentTransactionStatus.succeeded + : PaymentTransactionStatus.failed); + + Object authCode = notif.raw.get("Ds_AuthorisationCode"); + String gatewayTxId = null; + if (authCode != null) { + String trimmed = String.valueOf(authCode).trim(); + // Redsys devuelve " " (espacios) cuando NO hay código de autorización. + // Eso lo consideramos "sin ID" → null, para no chocar con el índice único. + if (!trimmed.isEmpty()) { + gatewayTxId = trimmed; + } + } + // MySQL permite múltiples NULL en un índice UNIQUE, así que es seguro. + tx.setGatewayTransactionId(gatewayTxId); + tx.setGatewayResponseCode(notif.response); + tx.setResponsePayload(om.writeValueAsString(notif.raw)); + tx.setProcessedAt(LocalDateTime.now()); + txRepo.save(tx); + + if (authorized) { + p.setAuthorizationCode(tx.getGatewayTransactionId()); + p.setStatus(PaymentStatus.captured); + p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents); + p.setAuthorizedAt(LocalDateTime.now()); + p.setCapturedAt(LocalDateTime.now()); + } else { + p.setStatus(PaymentStatus.failed); + p.setFailedAt(LocalDateTime.now()); + } + + payRepo.save(p); + + if (!authorized) { + ev.setLastError("Payment declined (Ds_Response=" + notif.response + ")"); + } + + ev.setProcessed(true); + ev.setProcessedAt(LocalDateTime.now()); + webhookEventRepo.save(ev); + } catch (Exception e) { + ev.setProcessed(false); + ev.setLastError(e.getMessage()); + ev.setProcessedAt(null); + webhookEventRepo.save(ev); + throw e; // aquí sí, porque queremos que si falla lógica de negocio el caller se entere + } } // ---- refundViaRedsys y bank_transfer igual que antes, no tocan RedsysService @@ -163,7 +228,7 @@ public class PaymentService { Refund r = new Refund(); r.setPayment(p); r.setAmountCents(amountCents); - r.setStatus(RefundStatus.PENDING); + r.setStatus(RefundStatus.pending); r.setRequestedAt(LocalDateTime.now()); r = refundRepo.save(r); @@ -173,7 +238,7 @@ public class PaymentService { PaymentTransaction tx = new PaymentTransaction(); tx.setPayment(p); tx.setType(PaymentTransactionType.REFUND); - tx.setStatus(PaymentTransactionStatus.SUCCEEDED); + tx.setStatus(PaymentTransactionStatus.succeeded); tx.setAmountCents(amountCents); tx.setCurrency(p.getCurrency()); tx.setGatewayTransactionId(gatewayRefundId); @@ -181,7 +246,7 @@ public class PaymentService { tx.setProcessedAt(LocalDateTime.now()); txRepo.save(tx); - r.setStatus(RefundStatus.SUCCEEDED); + r.setStatus(RefundStatus.succeeded); r.setTransaction(tx); r.setGatewayRefundId(gatewayRefundId); r.setProcessedAt(LocalDateTime.now()); @@ -189,9 +254,9 @@ public class PaymentService { p.setAmountRefundedCents(p.getAmountRefundedCents() + amountCents); if (p.getAmountRefundedCents().equals(p.getAmountCapturedCents())) { - p.setStatus(PaymentStatus.REFUNDED); + p.setStatus(PaymentStatus.refunded); } else { - p.setStatus(PaymentStatus.PARTIALLY_REFUNDED); + p.setStatus(PaymentStatus.partially_refunded); } payRepo.save(p); } @@ -199,31 +264,75 @@ public class PaymentService { @Transactional public Payment createBankTransferPayment(Long orderId, long amountCents, String currency) { Payment p = new Payment(); - p.setOrderId(orderId); // null en tu caso actual + p.setOrderId(orderId); p.setCurrency(currency); p.setAmountTotalCents(amountCents); p.setGateway("bank_transfer"); - p.setStatus(PaymentStatus.REQUIRES_ACTION); // pendiente de ingreso - return payRepo.save(p); + p.setStatus(PaymentStatus.requires_action); // pendiente de ingreso + p = payRepo.save(p); + + // Crear transacción pendiente + PaymentTransaction tx = new PaymentTransaction(); + tx.setPayment(p); + tx.setType(PaymentTransactionType.CAPTURE); // o AUTH si prefieres + tx.setStatus(PaymentTransactionStatus.pending); + tx.setAmountCents(amountCents); + tx.setCurrency(currency); + // tx.setProcessedAt(null); // la dejas nula hasta que se confirme + txRepo.save(tx); + + return p; } @Transactional public void markBankTransferAsCaptured(Long paymentId) { - Payment p = payRepo.findById(paymentId).orElseThrow(); - if (!"bank_transfer".equals(p.getGateway())) - throw new IllegalStateException("No es transferencia"); - p.setAmountCapturedCents(p.getAmountTotalCents()); - p.setCapturedAt(LocalDateTime.now()); - p.setStatus(PaymentStatus.CAPTURED); - payRepo.save(p); + Payment p = payRepo.findById(paymentId) + .orElseThrow(() -> new IllegalArgumentException("Payment no encontrado: " + paymentId)); - PaymentTransaction tx = new PaymentTransaction(); - tx.setPayment(p); - tx.setType(PaymentTransactionType.CAPTURE); - tx.setStatus(PaymentTransactionStatus.SUCCEEDED); - tx.setAmountCents(p.getAmountTotalCents()); - tx.setCurrency(p.getCurrency()); + if (!"bank_transfer".equals(p.getGateway())) { + throw new IllegalStateException("El Payment " + paymentId + " no es de tipo bank_transfer"); + } + + // Idempotencia simple: si ya está capturado no hacemos nada + if (p.getStatus() == PaymentStatus.captured + || p.getStatus() == PaymentStatus.partially_refunded + || p.getStatus() == PaymentStatus.refunded) { + return; + } + + // 1) Buscar la transacción pendiente de captura + PaymentTransaction tx = txRepo + .findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc( + paymentId, + PaymentTransactionType.CAPTURE, + PaymentTransactionStatus.pending) + .orElseThrow(() -> new IllegalStateException( + "No se ha encontrado transacción PENDING para la transferencia " + paymentId)); + + // 2) Actualizarla a SUCCEEDED y rellenar processedAt + tx.setStatus(PaymentTransactionStatus.succeeded); tx.setProcessedAt(LocalDateTime.now()); txRepo.save(tx); + + // 3) Actualizar el Payment + p.setAmountCapturedCents(p.getAmountTotalCents()); + p.setCapturedAt(LocalDateTime.now()); + p.setStatus(PaymentStatus.captured); + payRepo.save(p); } + + private boolean isRedsysAuthorized(RedsysService.RedsysNotification notif) { + if (notif.response == null) { + return false; + } + String r = notif.response.trim(); + // Si no es numérico, lo tratamos como no autorizado + if (!r.matches("\\d+")) { + return false; + } + int code = Integer.parseInt(r); + // Redsys: 0–99 → autorizado; >=100 → denegado / error + return code >= 0 && code <= 99; + } + } diff --git a/src/main/java/com/imprimelibros/erp/payments/model/CaptureMethod.java b/src/main/java/com/imprimelibros/erp/payments/model/CaptureMethod.java index 3239394..2d9c53e 100644 --- a/src/main/java/com/imprimelibros/erp/payments/model/CaptureMethod.java +++ b/src/main/java/com/imprimelibros/erp/payments/model/CaptureMethod.java @@ -1,4 +1,5 @@ package com.imprimelibros.erp.payments.model; -public enum CaptureMethod { AUTOMATIC, MANUAL } + +public enum CaptureMethod { automatic, manual } diff --git a/src/main/java/com/imprimelibros/erp/payments/model/IdempotencyKey.java b/src/main/java/com/imprimelibros/erp/payments/model/IdempotencyKey.java deleted file mode 100644 index bcf200e..0000000 --- a/src/main/java/com/imprimelibros/erp/payments/model/IdempotencyKey.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.imprimelibros.erp.payments.model; - -import jakarta.persistence.*; -import java.time.LocalDateTime; - -@Entity -@Table( - name = "idempotency_keys", - uniqueConstraints = { - @UniqueConstraint(name = "uq_idem_scope_key", columnNames = {"scope","idem_key"}) - }, - indexes = { - @Index(name = "idx_idem_resource", columnList = "resource_id") - } -) -public class IdempotencyKey { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Enumerated(EnumType.STRING) - @Column(name = "scope", nullable = false, length = 16) - private IdempotencyScope scope; - - @Column(name = "idem_key", nullable = false, length = 128) - private String idemKey; - - @Column(name = "resource_id") - private Long resourceId; - - @Column(name = "response_cache", columnDefinition = "json") - private String responseCache; - - @Column(name = "created_at", nullable = false, - columnDefinition = "datetime default current_timestamp") - private LocalDateTime createdAt; - - @Column(name = "expires_at") - private LocalDateTime expiresAt; - - public IdempotencyKey() {} - - // Getters & Setters - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } - - public IdempotencyScope getScope() { return scope; } - public void setScope(IdempotencyScope scope) { this.scope = scope; } - - public String getIdemKey() { return idemKey; } - public void setIdemKey(String idemKey) { this.idemKey = idemKey; } - - public Long getResourceId() { return resourceId; } - public void setResourceId(Long resourceId) { this.resourceId = resourceId; } - - public String getResponseCache() { return responseCache; } - public void setResponseCache(String responseCache) { this.responseCache = responseCache; } - - public LocalDateTime getCreatedAt() { return createdAt; } - public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } - - public LocalDateTime getExpiresAt() { return expiresAt; } - public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; } -} - diff --git a/src/main/java/com/imprimelibros/erp/payments/model/IdempotencyScope.java b/src/main/java/com/imprimelibros/erp/payments/model/IdempotencyScope.java deleted file mode 100644 index e7088d4..0000000 --- a/src/main/java/com/imprimelibros/erp/payments/model/IdempotencyScope.java +++ /dev/null @@ -1,3 +0,0 @@ -package com.imprimelibros.erp.payments.model; - -public enum IdempotencyScope { PAYMENT, REFUND, WEBHOOK } diff --git a/src/main/java/com/imprimelibros/erp/payments/model/Payment.java b/src/main/java/com/imprimelibros/erp/payments/model/Payment.java index 18267f7..5ca95b9 100644 --- a/src/main/java/com/imprimelibros/erp/payments/model/Payment.java +++ b/src/main/java/com/imprimelibros/erp/payments/model/Payment.java @@ -17,10 +17,6 @@ public class Payment { @Column(name = "user_id") private Long userId; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "payment_method_id") - private PaymentMethod paymentMethod; - @Column(nullable = false, length = 3) private String currency; @@ -35,11 +31,11 @@ public class Payment { @Enumerated(EnumType.STRING) @Column(nullable = false, length = 32) - private PaymentStatus status = PaymentStatus.REQUIRES_PAYMENT_METHOD; + private PaymentStatus status = PaymentStatus.requires_payment_method; @Enumerated(EnumType.STRING) @Column(name = "capture_method", nullable = false, length = 16) - private CaptureMethod captureMethod = CaptureMethod.AUTOMATIC; + private CaptureMethod captureMethod = CaptureMethod.automatic; @Column(nullable = false, length = 32) private String gateway; @@ -55,7 +51,7 @@ public class Payment { @Enumerated(EnumType.STRING) @Column(name = "three_ds_status", nullable = false, length = 32) - private ThreeDSStatus threeDsStatus = ThreeDSStatus.NOT_APPLICABLE; + private ThreeDSStatus threeDsStatus = ThreeDSStatus.not_applicable; @Column(length = 22) private String descriptor; @@ -99,9 +95,6 @@ public class Payment { public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } - public PaymentMethod getPaymentMethod() { return paymentMethod; } - public void setPaymentMethod(PaymentMethod paymentMethod) { this.paymentMethod = paymentMethod; } - public String getCurrency() { return currency; } public void setCurrency(String currency) { this.currency = currency; } diff --git a/src/main/java/com/imprimelibros/erp/payments/model/PaymentMethod.java b/src/main/java/com/imprimelibros/erp/payments/model/PaymentMethod.java deleted file mode 100644 index ab5833f..0000000 --- a/src/main/java/com/imprimelibros/erp/payments/model/PaymentMethod.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.imprimelibros.erp.payments.model; - -import jakarta.persistence.*; -import java.time.LocalDateTime; - -@Entity -@Table(name = "payment_methods") -public class PaymentMethod { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "user_id") - private Long userId; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 32) - private PaymentMethodType type; - - @Column(length = 32) - private String brand; - - @Column(length = 4) - private String last4; - - @Column(name = "exp_month") - private Integer expMonth; - - @Column(name = "exp_year") - private Integer expYear; - - @Column(length = 128) - private String fingerprint; - - @Column(length = 128, unique = true) - private String tokenId; - - @Column(length = 128) - private String sepaMandateId; - - @Column(length = 190) - private String payerEmail; - - @Column(columnDefinition = "json") - private String metadata; - - @Column(name = "created_at", nullable = false, - columnDefinition = "datetime default current_timestamp") - private LocalDateTime createdAt; - - @Column(name = "updated_at", nullable = false, - columnDefinition = "datetime default current_timestamp on update current_timestamp") - private LocalDateTime updatedAt; - - // ---- Getters/Setters ---- - public PaymentMethod() {} - - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } - - public Long getUserId() { return userId; } - public void setUserId(Long userId) { this.userId = userId; } - - public PaymentMethodType getType() { return type; } - public void setType(PaymentMethodType type) { this.type = type; } - - public String getBrand() { return brand; } - public void setBrand(String brand) { this.brand = brand; } - - public String getLast4() { return last4; } - public void setLast4(String last4) { this.last4 = last4; } - - public Integer getExpMonth() { return expMonth; } - public void setExpMonth(Integer expMonth) { this.expMonth = expMonth; } - - public Integer getExpYear() { return expYear; } - public void setExpYear(Integer expYear) { this.expYear = expYear; } - - public String getFingerprint() { return fingerprint; } - public void setFingerprint(String fingerprint) { this.fingerprint = fingerprint; } - - public String getTokenId() { return tokenId; } - public void setTokenId(String tokenId) { this.tokenId = tokenId; } - - public String getSepaMandateId() { return sepaMandateId; } - public void setSepaMandateId(String sepaMandateId) { this.sepaMandateId = sepaMandateId; } - - public String getPayerEmail() { return payerEmail; } - public void setPayerEmail(String payerEmail) { this.payerEmail = payerEmail; } - - public String getMetadata() { return metadata; } - public void setMetadata(String metadata) { this.metadata = metadata; } - - public LocalDateTime getCreatedAt() { return createdAt; } - public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } - - public LocalDateTime getUpdatedAt() { return updatedAt; } - public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } -} diff --git a/src/main/java/com/imprimelibros/erp/payments/model/PaymentMethodType.java b/src/main/java/com/imprimelibros/erp/payments/model/PaymentMethodType.java deleted file mode 100644 index e0ec386..0000000 --- a/src/main/java/com/imprimelibros/erp/payments/model/PaymentMethodType.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.imprimelibros.erp.payments.model; - -public enum PaymentMethodType { CARD, BIZUM, BANK_TRANSFER } - diff --git a/src/main/java/com/imprimelibros/erp/payments/model/PaymentStatus.java b/src/main/java/com/imprimelibros/erp/payments/model/PaymentStatus.java index 18604be..661660f 100644 --- a/src/main/java/com/imprimelibros/erp/payments/model/PaymentStatus.java +++ b/src/main/java/com/imprimelibros/erp/payments/model/PaymentStatus.java @@ -1,8 +1,8 @@ package com.imprimelibros.erp.payments.model; public enum PaymentStatus { - REQUIRES_PAYMENT_METHOD, REQUIRES_ACTION, AUTHORIZED, - CAPTURED, PARTIALLY_REFUNDED, REFUNDED, CANCELED, FAILED + requires_payment_method, requires_action, authorized, + captured, partially_refunded, refunded, canceled, failed } diff --git a/src/main/java/com/imprimelibros/erp/payments/model/PaymentTransaction.java b/src/main/java/com/imprimelibros/erp/payments/model/PaymentTransaction.java index a7fd404..f8ec70c 100644 --- a/src/main/java/com/imprimelibros/erp/payments/model/PaymentTransaction.java +++ b/src/main/java/com/imprimelibros/erp/payments/model/PaymentTransaction.java @@ -120,4 +120,13 @@ public class PaymentTransaction { public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + @PrePersist + public void prePersist() { + LocalDateTime now = LocalDateTime.now(); + if (createdAt == null) { + createdAt = now; + } + } + } diff --git a/src/main/java/com/imprimelibros/erp/payments/model/PaymentTransactionStatus.java b/src/main/java/com/imprimelibros/erp/payments/model/PaymentTransactionStatus.java index f495274..5ff279b 100644 --- a/src/main/java/com/imprimelibros/erp/payments/model/PaymentTransactionStatus.java +++ b/src/main/java/com/imprimelibros/erp/payments/model/PaymentTransactionStatus.java @@ -1,4 +1,4 @@ package com.imprimelibros.erp.payments.model; -public enum PaymentTransactionStatus { PENDING, SUCCEEDED, FAILED } +public enum PaymentTransactionStatus { pending, succeeded, failed } diff --git a/src/main/java/com/imprimelibros/erp/payments/model/Refund.java b/src/main/java/com/imprimelibros/erp/payments/model/Refund.java index 576e752..06a4516 100644 --- a/src/main/java/com/imprimelibros/erp/payments/model/Refund.java +++ b/src/main/java/com/imprimelibros/erp/payments/model/Refund.java @@ -33,11 +33,11 @@ public class Refund { @Enumerated(EnumType.STRING) @Column(name = "reason", nullable = false, length = 32) - private RefundReason reason = RefundReason.CUSTOMER_REQUEST; + private RefundReason reason = RefundReason.customer_request; @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false, length = 16) - private RefundStatus status = RefundStatus.PENDING; + private RefundStatus status = RefundStatus.pending; @Column(name = "requested_by_user_id") private Long requestedByUserId; diff --git a/src/main/java/com/imprimelibros/erp/payments/model/RefundReason.java b/src/main/java/com/imprimelibros/erp/payments/model/RefundReason.java index 95235a8..432e146 100644 --- a/src/main/java/com/imprimelibros/erp/payments/model/RefundReason.java +++ b/src/main/java/com/imprimelibros/erp/payments/model/RefundReason.java @@ -1,6 +1,6 @@ package com.imprimelibros.erp.payments.model; public enum RefundReason { - CUSTOMER_REQUEST, PARTIAL_RETURN, PRICING_ADJUSTMENT, DUPLICATE, FRAUD, OTHER + customer_request, partial_return, pricing_adjustment, duplicate, fraud, other } diff --git a/src/main/java/com/imprimelibros/erp/payments/model/RefundStatus.java b/src/main/java/com/imprimelibros/erp/payments/model/RefundStatus.java index e15fd1d..d7e6f79 100644 --- a/src/main/java/com/imprimelibros/erp/payments/model/RefundStatus.java +++ b/src/main/java/com/imprimelibros/erp/payments/model/RefundStatus.java @@ -1,4 +1,4 @@ package com.imprimelibros.erp.payments.model; -public enum RefundStatus { PENDING, SUCCEEDED, FAILED, CANCELED } +public enum RefundStatus { pending, succeeded, failed, canceled } diff --git a/src/main/java/com/imprimelibros/erp/payments/model/ThreeDSStatus.java b/src/main/java/com/imprimelibros/erp/payments/model/ThreeDSStatus.java index 8982ae1..1af1879 100644 --- a/src/main/java/com/imprimelibros/erp/payments/model/ThreeDSStatus.java +++ b/src/main/java/com/imprimelibros/erp/payments/model/ThreeDSStatus.java @@ -1,4 +1,4 @@ package com.imprimelibros.erp.payments.model; -public enum ThreeDSStatus { NOT_APPLICABLE, ATTEMPTED, CHALLENGE, SUCCEEDED, FAILED } +public enum ThreeDSStatus { not_applicable, attempted, challenge, succeeded, failed } diff --git a/src/main/java/com/imprimelibros/erp/payments/model/WebhookEvent.java b/src/main/java/com/imprimelibros/erp/payments/model/WebhookEvent.java index 201dc81..7dd8f0a 100644 --- a/src/main/java/com/imprimelibros/erp/payments/model/WebhookEvent.java +++ b/src/main/java/com/imprimelibros/erp/payments/model/WebhookEvent.java @@ -85,4 +85,12 @@ public class WebhookEvent { public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + @PrePersist + public void prePersist() { + LocalDateTime now = LocalDateTime.now(); + if (createdAt == null) { + createdAt = now; + } + } } diff --git a/src/main/java/com/imprimelibros/erp/payments/repo/IdempotencyKeyRepository.java b/src/main/java/com/imprimelibros/erp/payments/repo/IdempotencyKeyRepository.java deleted file mode 100644 index a04ea60..0000000 --- a/src/main/java/com/imprimelibros/erp/payments/repo/IdempotencyKeyRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -// IdempotencyKeyRepository.java -package com.imprimelibros.erp.payments.repo; - -import com.imprimelibros.erp.payments.model.IdempotencyKey; -import com.imprimelibros.erp.payments.model.IdempotencyScope; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface IdempotencyKeyRepository extends JpaRepository { - Optional findByScopeAndIdemKey(IdempotencyScope scope, String idemKey); -} diff --git a/src/main/java/com/imprimelibros/erp/payments/repo/PaymentMethodRepository.java b/src/main/java/com/imprimelibros/erp/payments/repo/PaymentMethodRepository.java deleted file mode 100644 index 397d1ef..0000000 --- a/src/main/java/com/imprimelibros/erp/payments/repo/PaymentMethodRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -// PaymentMethodRepository.java -package com.imprimelibros.erp.payments.repo; - -import com.imprimelibros.erp.payments.model.PaymentMethod; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface PaymentMethodRepository extends JpaRepository {} 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 2965178..aac12ad 100644 --- a/src/main/java/com/imprimelibros/erp/payments/repo/PaymentTransactionRepository.java +++ b/src/main/java/com/imprimelibros/erp/payments/repo/PaymentTransactionRepository.java @@ -2,6 +2,9 @@ package com.imprimelibros.erp.payments.repo; import com.imprimelibros.erp.payments.model.PaymentTransaction; +import com.imprimelibros.erp.payments.model.PaymentTransactionStatus; +import com.imprimelibros.erp.payments.model.PaymentTransactionType; + import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; @@ -9,4 +12,9 @@ import java.util.Optional; public interface PaymentTransactionRepository extends JpaRepository { Optional findByGatewayTransactionId(String gatewayTransactionId); Optional findByIdempotencyKey(String idempotencyKey); + Optional findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc( + Long paymentId, + PaymentTransactionType type, + PaymentTransactionStatus status + ); } diff --git a/src/main/java/com/imprimelibros/erp/payments/repo/RefundRepository.java b/src/main/java/com/imprimelibros/erp/payments/repo/RefundRepository.java index 6e7228d..57c6d4c 100644 --- a/src/main/java/com/imprimelibros/erp/payments/repo/RefundRepository.java +++ b/src/main/java/com/imprimelibros/erp/payments/repo/RefundRepository.java @@ -7,6 +7,6 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface RefundRepository extends JpaRepository { - @Query("select coalesce(sum(r.amountCents),0) from Refund r where r.payment.id = :paymentId and r.status = com.imprimelibros.erp.payments.model.RefundStatus.SUCCEEDED") + @Query("select coalesce(sum(r.amountCents),0) from Refund r where r.payment.id = :paymentId and r.status = com.imprimelibros.erp.payments.model.RefundStatus.succeeded") long sumSucceededByPaymentId(@Param("paymentId") Long paymentId); } diff --git a/src/main/java/com/imprimelibros/erp/payments/repo/WebhookEventRepository.java b/src/main/java/com/imprimelibros/erp/payments/repo/WebhookEventRepository.java index 9ba9488..c70070b 100644 --- a/src/main/java/com/imprimelibros/erp/payments/repo/WebhookEventRepository.java +++ b/src/main/java/com/imprimelibros/erp/payments/repo/WebhookEventRepository.java @@ -4,4 +4,9 @@ package com.imprimelibros.erp.payments.repo; import com.imprimelibros.erp.payments.model.WebhookEvent; import org.springframework.data.jpa.repository.JpaRepository; -public interface WebhookEventRepository extends JpaRepository {} +import java.util.Optional; + +public interface WebhookEventRepository extends JpaRepository { + + Optional findByProviderAndEventId(String provider, String eventId); +} diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java index a1d4d8c..b0bcf07 100644 --- a/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java +++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysController.java @@ -10,9 +10,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.nio.charset.StandardCharsets; -import java.util.Map; import java.util.UUID; -import org.springframework.transaction.annotation.Transactional; @Controller @RequestMapping("/pagos/redsys") @@ -100,7 +98,6 @@ public class RedsysController { // integraciones ni lo usan) @PostMapping(value = "/ok", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @ResponseBody - @jakarta.transaction.Transactional public ResponseEntity okPost(@RequestParam("Ds_Signature") String signature, @RequestParam("Ds_MerchantParameters") String merchantParameters) { try { @@ -121,14 +118,26 @@ public class RedsysController { @PostMapping(value = "/ko", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @ResponseBody - public ResponseEntity koPost(@RequestParam Map form) { - // Podrías loguear 'form' si quieres ver qué manda Redsys - return ResponseEntity.ok("

Pago cancelado o rechazado

Volver"); + public ResponseEntity koPost( + @RequestParam("Ds_Signature") String signature, + @RequestParam("Ds_MerchantParameters") String merchantParameters) { + + try { + // Procesamos la notificación IGUAL que en /ok y /notify + paymentService.handleRedsysNotification(signature, merchantParameters); + + // 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 - @jakarta.transaction.Transactional public String notifyRedsys(@RequestParam("Ds_Signature") String signature, @RequestParam("Ds_MerchantParameters") String merchantParameters) { try { diff --git a/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java index d6a4067..be317ee 100644 --- a/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java +++ b/src/main/java/com/imprimelibros/erp/redsys/RedsysService.java @@ -105,27 +105,31 @@ public class RedsysService { // ---------- STEP 4: Validar notificación ---------- public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64) throws Exception { - // 1) Decodificamos a mapa solo para leer campos - Map mp = decodeMerchantParametersToMap(dsMerchantParametersB64); + + ApiMacSha256 api = new ApiMacSha256(); + + // 1) Decodificar Ds_MerchantParameters usando la librería oficial + String json = api.decodeMerchantParameters(dsMerchantParametersB64); + + // 2) Convertir a Map para tu modelo + Map mp = MAPPER.readValue(json, new TypeReference<>() { + }); RedsysNotification notif = new RedsysNotification(mp); if (notif.order == null || notif.order.isBlank()) { + System.out.println("### ATENCIÓN: Ds_Order no viene en MerchantParameters"); throw new IllegalArgumentException("Falta Ds_Order en Ds_MerchantParameters"); } - // 2) Calculamos la firma esperada usando el B64 tal cual - ApiMacSha256 api = new ApiMacSha256(); - // Esta línea es opcional para createMerchantSignatureNotif, pero no molesta: - api.setParameter("Ds_MerchantParameters", dsMerchantParametersB64); - + // 3) Calcular firma esperada: clave comercio + MerchantParameters en B64 String expected = api.createMerchantSignatureNotif( - secretKeyBase64, - dsMerchantParametersB64 // 👈 AQUÍ va el B64, NO el JSON + secretKeyBase64, // 👈 La misma que usas para crear la firma del pago + dsMerchantParametersB64 // 👈 SIEMPRE el B64 tal cual llega de Redsys, sin tocar ); - // 3) Comparamos en constante time, normalizando Base64 URL-safe + // 4) Comparar firma Redsys vs firma calculada if (!safeEqualsB64(dsSignature, expected)) { - System.out.println("Firma Redsys no válida"); + System.out.println("### Firma Redsys no válida"); System.out.println("Ds_Signature (Redsys) = " + dsSignature); System.out.println("Expected (local) = " + expected); throw new SecurityException("Firma Redsys no válida"); diff --git a/src/main/resources/db/changelog/changesets/0007-payments-core.yml b/src/main/resources/db/changelog/changesets/0007-payments-core.yml index 2d3cd5c..4ef635f 100644 --- a/src/main/resources/db/changelog/changesets/0007-payments-core.yml +++ b/src/main/resources/db/changelog/changesets/0007-payments-core.yml @@ -3,70 +3,7 @@ databaseChangeLog: id: 0007-payments-core author: jjo changes: - # 1) payment_methods - - createTable: - tableName: payment_methods - columns: - - column: - name: id - type: BIGINT AUTO_INCREMENT - constraints: - primaryKey: true - nullable: false - - column: - name: user_id - type: BIGINT - - column: - name: type - type: "ENUM('card','bizum','bank_transfer')" - constraints: - nullable: false - - column: - name: brand - type: VARCHAR(32) - - column: - name: last4 - type: VARCHAR(4) - - column: - name: exp_month - type: TINYINT - - column: - name: exp_year - type: SMALLINT - - column: - name: fingerprint - type: VARCHAR(128) - - column: - name: token_id - type: VARCHAR(128) - - column: - name: sepa_mandate_id - type: VARCHAR(128) - - column: - name: payer_email - type: VARCHAR(190) - - column: - name: metadata - type: JSON - - column: - name: created_at - type: DATETIME - defaultValueComputed: CURRENT_TIMESTAMP - constraints: - nullable: false - - column: - name: updated_at - type: DATETIME - defaultValueComputed: "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" - constraints: - nullable: false - - - addUniqueConstraint: - tableName: payment_methods - columnNames: token_id - constraintName: uq_payment_methods_token - - # 2) payments + # 2) payments - createTable: tableName: payments columns: @@ -82,9 +19,6 @@ databaseChangeLog: - column: name: user_id type: BIGINT - - column: - name: payment_method_id - type: BIGINT - column: name: currency type: CHAR(3) @@ -173,13 +107,6 @@ databaseChangeLog: constraints: nullable: false - - addForeignKeyConstraint: - baseTableName: payments - baseColumnNames: payment_method_id - referencedTableName: payment_methods - referencedColumnNames: id - constraintName: fk_payments_payment_methods - onDelete: SET NULL - createIndex: tableName: payments @@ -474,50 +401,3 @@ databaseChangeLog: - column: name: processed - # 6) idempotency_keys - - createTable: - tableName: idempotency_keys - columns: - - column: - name: id - type: BIGINT AUTO_INCREMENT - constraints: - primaryKey: true - nullable: false - - column: - name: scope - type: "ENUM('payment','refund','webhook')" - constraints: - nullable: false - - column: - name: idem_key - type: VARCHAR(128) - constraints: - nullable: false - - column: - name: resource_id - type: BIGINT - - column: - name: response_cache - type: JSON - - column: - name: created_at - type: DATETIME - defaultValueComputed: CURRENT_TIMESTAMP - constraints: - nullable: false - - column: - name: expires_at - type: DATETIME - - - addUniqueConstraint: - tableName: idempotency_keys - columnNames: scope, idem_key - constraintName: uq_idem_scope_key - - - createIndex: - tableName: idempotency_keys - indexName: idx_idem_resource - columns: - - column: - name: resource_id