mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 08:58:48 +00:00
falta vista de pagos
This commit is contained in:
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user