mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-12 16:38:48 +00:00
575 lines
23 KiB
Java
575 lines
23 KiB
Java
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.facturacion.SerieFactura;
|
||
import com.imprimelibros.erp.facturacion.TipoPago;
|
||
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||
import com.imprimelibros.erp.payments.model.*;
|
||
import com.imprimelibros.erp.payments.repo.PaymentRepository;
|
||
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
||
import com.imprimelibros.erp.payments.repo.RefundRepository;
|
||
import com.imprimelibros.erp.redsys.RedsysService;
|
||
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 com.imprimelibros.erp.pedidos.Pedido;
|
||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||
|
||
import java.time.LocalDateTime;
|
||
import java.util.Locale;
|
||
import java.util.Map;
|
||
import java.util.Objects;
|
||
|
||
@Service
|
||
public class PaymentService {
|
||
|
||
private final PaymentRepository payRepo;
|
||
private final PaymentTransactionRepository txRepo;
|
||
private final RefundRepository refundRepo;
|
||
private final RedsysService redsysService;
|
||
private final WebhookEventRepository webhookEventRepo;
|
||
private final ObjectMapper om = new ObjectMapper();
|
||
private final CartService cartService;
|
||
private final PedidoService pedidoService;
|
||
private final FacturacionService facturacionService;
|
||
|
||
public PaymentService(PaymentRepository payRepo,
|
||
PaymentTransactionRepository txRepo,
|
||
RefundRepository refundRepo,
|
||
RedsysService redsysService,
|
||
WebhookEventRepository webhookEventRepo,
|
||
CartService cartService,
|
||
PedidoService pedidoService,
|
||
FacturacionService facturacionService) {
|
||
this.payRepo = payRepo;
|
||
this.txRepo = txRepo;
|
||
this.refundRepo = refundRepo;
|
||
this.redsysService = redsysService;
|
||
this.webhookEventRepo = webhookEventRepo;
|
||
this.cartService = cartService;
|
||
this.pedidoService = pedidoService;
|
||
this.facturacionService = facturacionService;
|
||
}
|
||
|
||
public Payment findFailedPaymentByOrderId(Long orderId) {
|
||
return payRepo.findFirstByOrderIdAndStatusOrderByIdDesc(orderId, PaymentStatus.failed)
|
||
.orElse(null);
|
||
}
|
||
|
||
public Map<String, Long> getPaymentTransactionData(Long paymentId) {
|
||
PaymentTransaction tx = txRepo.findByPaymentIdAndType(
|
||
paymentId,
|
||
PaymentTransactionType.CAPTURE)
|
||
.orElse(null);
|
||
if (tx == null) {
|
||
return null;
|
||
}
|
||
String resp_payload = tx.getResponsePayload();
|
||
try {
|
||
ObjectMapper om = new ObjectMapper();
|
||
var node = om.readTree(resp_payload);
|
||
Long cartId = null;
|
||
Long dirFactId = null;
|
||
if (node.has("Ds_MerchantData")) {
|
||
// format: "Ds_MerchantData": "{"dirFactId":3,"cartId":90}"
|
||
String merchantData = node.get("Ds_MerchantData").asText();
|
||
merchantData = merchantData.replace(""", "\"");
|
||
var mdNode = om.readTree(merchantData);
|
||
if (mdNode.has("cartId")) {
|
||
cartId = mdNode.get("cartId").asLong();
|
||
}
|
||
if (mdNode.has("dirFactId")) {
|
||
dirFactId = mdNode.get("dirFactId").asLong();
|
||
}
|
||
}
|
||
return Map.of(
|
||
"cartId", cartId,
|
||
"dirFactId", dirFactId);
|
||
} catch (Exception e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Crea el Payment en BD y construye el formulario de Redsys usando la API
|
||
* oficial (ApiMacSha256).
|
||
*/
|
||
@Transactional
|
||
public FormPayload createRedsysPayment(Long cartId, Long dirFactId, Long amountCents, String currency, String method, Long orderId)
|
||
throws Exception {
|
||
Payment p = new Payment();
|
||
p.setOrderId(orderId);
|
||
|
||
Cart cart = this.cartService.findById(cartId);
|
||
if (cart != null && cart.getUserId() != null) {
|
||
p.setUserId(cart.getUserId());
|
||
this.cartService.lockCartById(cartId);
|
||
}
|
||
p.setCurrency(currency);
|
||
p.setAmountTotalCents(amountCents);
|
||
p.setGateway("redsys");
|
||
p.setStatus(PaymentStatus.requires_payment_method);
|
||
p = payRepo.saveAndFlush(p);
|
||
|
||
long now = System.currentTimeMillis();
|
||
String dsOrder = String.format("%012d", now % 1_000_000_000_000L);
|
||
|
||
p.setGatewayOrderId(dsOrder);
|
||
payRepo.save(p);
|
||
|
||
RedsysService.PaymentRequest req = new RedsysService.PaymentRequest(dsOrder, amountCents,
|
||
"Compra en Imprimelibros", cartId, dirFactId);
|
||
|
||
if ("bizum".equalsIgnoreCase(method)) {
|
||
return redsysService.buildRedirectFormBizum(req);
|
||
} else {
|
||
return redsysService.buildRedirectForm(req);
|
||
}
|
||
}
|
||
|
||
@Transactional
|
||
public void handleRedsysNotification(String dsSignature, String dsMerchantParameters, Locale locale)
|
||
throws Exception {
|
||
|
||
// 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);
|
||
|
||
// IMPORTANTE: NO re-lanzamos la excepción
|
||
// Simplemente salimos. Así se hace commit de este insert.
|
||
return;
|
||
}
|
||
|
||
// 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;
|
||
|
||
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 (Boolean.TRUE.equals(ev.getProcessed())) {
|
||
return;
|
||
}
|
||
|
||
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);
|
||
|
||
String gatewayTxId = null;
|
||
// 1) Si es Bizum y tenemos Ds_Bizum_IdOper, úsalo como ID único
|
||
if (notif.isBizum()
|
||
&& notif.bizumIdOper != null
|
||
&& !notif.bizumIdOper.isBlank()) {
|
||
|
||
gatewayTxId = notif.bizumIdOper.trim();
|
||
|
||
// 2) Si no es Bizum, intenta usar Ds_AuthorisationCode
|
||
} else if (notif.authorisationCode != null) {
|
||
String trimmed = notif.authorisationCode.trim();
|
||
|
||
// Redsys suele mandar "000000" para Bizum; por si acaso también lo filtramos
|
||
if (!trimmed.isEmpty() && !"000000".equals(trimmed)) {
|
||
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) {
|
||
if (notif.isBizum()) {
|
||
p.setAuthorizationCode(null); // o "000000" si te interesa mostrarlo
|
||
} else if (notif.authorisationCode != null
|
||
&& !"000000".equals(notif.authorisationCode.trim())
|
||
&& !notif.authorisationCode.isBlank()) {
|
||
p.setAuthorizationCode(notif.authorisationCode.trim());
|
||
}
|
||
|
||
p.setStatus(PaymentStatus.captured);
|
||
p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents);
|
||
p.setAuthorizedAt(LocalDateTime.now());
|
||
p.setCapturedAt(LocalDateTime.now());
|
||
pedidoService.setOrderAsPaid(p.getOrderId());
|
||
|
||
Pedido pedido = pedidoService.getPedidoById(p.getOrderId());
|
||
SerieFactura serie = facturacionService.getDefaultSerieFactura();
|
||
|
||
facturacionService.crearNuevaFacturaAuto(pedido, serie, notif.isBizum() ? TipoPago.tpv_bizum : TipoPago.tpv_tarjeta, locale);
|
||
|
||
} else {
|
||
p.setStatus(PaymentStatus.failed);
|
||
p.setFailedAt(LocalDateTime.now());
|
||
pedidoService.markPedidoAsPaymentDenied(p.getOrderId());
|
||
}
|
||
|
||
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
|
||
// ----
|
||
@Transactional
|
||
public void refundViaRedsys(Long paymentId, long amountCents, String idempotencyKey) {
|
||
Payment p = payRepo.findById(paymentId)
|
||
.orElseThrow(() -> new IllegalArgumentException("Payment no encontrado"));
|
||
|
||
if (amountCents <= 0)
|
||
throw new IllegalArgumentException("Importe inválido");
|
||
|
||
long maxRefundable = p.getAmountCapturedCents() - p.getAmountRefundedCents();
|
||
if (amountCents > maxRefundable)
|
||
throw new IllegalStateException("Importe de devolución supera lo capturado");
|
||
|
||
txRepo.findByIdempotencyKey(idempotencyKey)
|
||
.ifPresent(t -> {
|
||
throw new IllegalStateException("Reembolso ya procesado");
|
||
});
|
||
|
||
Refund r = new Refund();
|
||
r.setPayment(p);
|
||
r.setAmountCents(amountCents);
|
||
r.setStatus(RefundStatus.pending);
|
||
r.setRequestedAt(LocalDateTime.now());
|
||
r = refundRepo.save(r);
|
||
|
||
String gatewayRefundId;
|
||
try {
|
||
// ⚠️ Usa aquí el mismo valor que mandaste en Ds_Merchant_Order al cobrar
|
||
// por ejemplo, p.getGatewayOrderId() o similar
|
||
String originalOrder = p.getGatewayOrderId(); // ajusta al nombre real del campo
|
||
gatewayRefundId = redsysService.requestRefund(originalOrder, amountCents);
|
||
} catch (Exception e) {
|
||
r.setStatus(RefundStatus.failed);
|
||
r.setProcessedAt(LocalDateTime.now());
|
||
refundRepo.save(r);
|
||
throw new IllegalStateException("Error al solicitar la devolución a Redsys", e);
|
||
}
|
||
|
||
// 🔧 NORMALIZAR ANTES DE GUARDAR
|
||
if (gatewayRefundId != null) {
|
||
gatewayRefundId = gatewayRefundId.trim();
|
||
if (gatewayRefundId.isEmpty() || "000000".equals(gatewayRefundId)) {
|
||
gatewayRefundId = null; // → múltiples NULL NO rompen el UNIQUE
|
||
}
|
||
}
|
||
|
||
PaymentTransaction tx = new PaymentTransaction();
|
||
tx.setPayment(p);
|
||
tx.setType(PaymentTransactionType.REFUND);
|
||
tx.setStatus(PaymentTransactionStatus.succeeded);
|
||
tx.setAmountCents(amountCents);
|
||
tx.setCurrency(p.getCurrency());
|
||
tx.setGatewayTransactionId(gatewayRefundId);
|
||
tx.setIdempotencyKey(idempotencyKey);
|
||
tx.setProcessedAt(LocalDateTime.now());
|
||
txRepo.save(tx);
|
||
|
||
r.setStatus(RefundStatus.succeeded);
|
||
r.setTransaction(tx);
|
||
r.setGatewayRefundId(gatewayRefundId);
|
||
r.setProcessedAt(LocalDateTime.now());
|
||
refundRepo.save(r);
|
||
|
||
p.setAmountRefundedCents(p.getAmountRefundedCents() + amountCents);
|
||
if (p.getAmountRefundedCents().equals(p.getAmountCapturedCents())) {
|
||
p.setStatus(PaymentStatus.refunded);
|
||
} else {
|
||
p.setStatus(PaymentStatus.partially_refunded);
|
||
}
|
||
payRepo.save(p);
|
||
}
|
||
|
||
@Transactional
|
||
public Payment createBankTransferPayment(Long cartId, Long dirFactId, long amountCents, String currency, Locale locale, Long orderId) {
|
||
Payment p = new Payment();
|
||
p.setOrderId(null);
|
||
|
||
Cart cart = this.cartService.findById(cartId);
|
||
if (cart != null && cart.getUserId() != null) {
|
||
p.setUserId(cart.getUserId());
|
||
// Se bloquea el carrito para evitar modificaciones mientras se procesa el pago
|
||
this.cartService.lockCartById(cartId);
|
||
}
|
||
|
||
p.setCurrency(currency);
|
||
p.setAmountTotalCents(amountCents);
|
||
p.setGateway("bank_transfer");
|
||
p.setStatus(PaymentStatus.requires_action); // pendiente de ingreso
|
||
if (orderId != null) {
|
||
p.setOrderId(orderId);
|
||
}
|
||
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);
|
||
String payload = "";
|
||
if (cartId != null) {
|
||
payload = "{\"cartId\":" + cartId + "}";
|
||
}
|
||
if (dirFactId != null) {
|
||
if (!payload.isEmpty()) {
|
||
payload = payload.substring(0, payload.length() - 1) + ",\"dirFactId\":" + dirFactId + "}";
|
||
} else {
|
||
payload = "{\"dirFactId\":" + dirFactId + "}";
|
||
}
|
||
}
|
||
tx.setResponsePayload(payload);
|
||
// tx.setProcessedAt(null); // la dejas nula hasta que se confirme
|
||
txRepo.save(tx);
|
||
|
||
return p;
|
||
}
|
||
|
||
@Transactional
|
||
public void markBankTransferAsCaptured(Long paymentId, Locale locale) {
|
||
Payment p = payRepo.findById(paymentId)
|
||
.orElseThrow(() -> new IllegalArgumentException("Payment no encontrado: " + paymentId));
|
||
|
||
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);
|
||
|
||
Long cartId = null;
|
||
Long dirFactId = null;
|
||
try {
|
||
// Intentar extraer cartId del payload de la transacción
|
||
if (tx.getResponsePayload() != null && !tx.getResponsePayload().isBlank()) {
|
||
ObjectMapper om = new ObjectMapper();
|
||
var node = om.readTree(tx.getResponsePayload());
|
||
if (node.has("cartId")) {
|
||
cartId = node.get("cartId").asLong();
|
||
}
|
||
if (node.has("dirFactId")) {
|
||
dirFactId = node.get("dirFactId").asLong();
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
// ignorar
|
||
}
|
||
|
||
// 4) Procesar el pedido asociado al carrito (si existe) o marcar el pedido como pagado
|
||
if(p.getOrderId() != null) {
|
||
pedidoService.setOrderAsPaid(p.getOrderId());
|
||
|
||
Pedido pedido = pedidoService.getPedidoById(p.getOrderId());
|
||
SerieFactura serie = facturacionService.getDefaultSerieFactura();
|
||
|
||
facturacionService.crearNuevaFacturaAuto(pedido, serie, TipoPago.transferencia, locale);
|
||
}
|
||
/*else if (cartId != null) {
|
||
// Se procesa el pedido dejando el estado calculado en processOrder
|
||
Long orderId = processOrder(cartId, dirFactId, locale, null);
|
||
if (orderId != null) {
|
||
p.setOrderId(orderId);
|
||
}
|
||
}*/
|
||
payRepo.save(p);
|
||
}
|
||
|
||
/**
|
||
* Devuelve (total o parcialmente) un pago hecho por transferencia bancaria.
|
||
* - Solo permite gateway = "bank_transfer".
|
||
* - Crea un Refund + PaymentTransaction de tipo REFUND.
|
||
* - Actualiza amountRefundedCents y el estado del Payment.
|
||
*/
|
||
@Transactional
|
||
public Refund refundBankTransfer(Long paymentId, long amountCents) {
|
||
Payment p = payRepo.findById(paymentId)
|
||
.orElseThrow(() -> new IllegalArgumentException("Payment no encontrado: " + paymentId));
|
||
|
||
if (!"bank_transfer".equals(p.getGateway())) {
|
||
throw new IllegalStateException("El Payment " + paymentId + " no es de tipo bank_transfer");
|
||
}
|
||
|
||
if (amountCents <= 0) {
|
||
throw new IllegalArgumentException("El importe de devolución debe ser > 0");
|
||
}
|
||
|
||
// Solo tiene sentido devolver si está capturado o ya parcialmente devuelto
|
||
if (p.getStatus() != PaymentStatus.captured
|
||
&& p.getStatus() != PaymentStatus.partially_refunded) {
|
||
throw new IllegalStateException(
|
||
"El Payment " + paymentId + " no está capturado; estado actual: " + p.getStatus());
|
||
}
|
||
|
||
long maxRefundable = p.getAmountCapturedCents() - p.getAmountRefundedCents();
|
||
if (amountCents > maxRefundable) {
|
||
throw new IllegalStateException(
|
||
"Importe de devolución supera lo todavía reembolsable. " +
|
||
"maxRefundable=" + maxRefundable + " requested=" + amountCents);
|
||
}
|
||
|
||
LocalDateTime now = LocalDateTime.now();
|
||
|
||
// 1) Crear Refund (para transferencias lo marcamos como SUCCEEDED directamente)
|
||
Refund refund = new Refund();
|
||
refund.setPayment(p);
|
||
refund.setAmountCents(amountCents);
|
||
// reason usa el valor por defecto (customer_request); si quieres otro, cámbialo
|
||
// aquí
|
||
refund.setStatus(RefundStatus.succeeded);
|
||
refund.setRequestedAt(now);
|
||
refund.setProcessedAt(now);
|
||
// requestedByUserId, notes, metadata -> opcionales, déjalos en null si no los
|
||
// usas
|
||
refund = refundRepo.save(refund);
|
||
|
||
// 2) Crear transacción de tipo REFUND
|
||
PaymentTransaction tx = new PaymentTransaction();
|
||
tx.setPayment(p);
|
||
tx.setType(PaymentTransactionType.REFUND);
|
||
tx.setStatus(PaymentTransactionStatus.succeeded);
|
||
tx.setAmountCents(amountCents);
|
||
tx.setCurrency(p.getCurrency());
|
||
tx.setProcessedAt(now);
|
||
// gatewayTransactionId lo dejamos null → el índice UNIQUE permite múltiples
|
||
// NULL
|
||
tx = txRepo.save(tx);
|
||
|
||
// Vincular el Refund con la transacción
|
||
refund.setTransaction(tx);
|
||
refundRepo.save(refund);
|
||
|
||
// 3) Actualizar Payment: total devuelto y estado
|
||
p.setAmountRefundedCents(p.getAmountRefundedCents() + amountCents);
|
||
|
||
if (p.getAmountRefundedCents().equals(p.getAmountCapturedCents())) {
|
||
p.setStatus(PaymentStatus.refunded);
|
||
} else {
|
||
p.setStatus(PaymentStatus.partially_refunded);
|
||
}
|
||
|
||
payRepo.save(p);
|
||
|
||
return refund;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
|
||
}
|