testeando el notify

This commit is contained in:
2025-11-03 19:31:28 +01:00
parent 88650fc5e8
commit 725cff9b51
10 changed files with 716 additions and 226 deletions

View File

@ -7,9 +7,7 @@ 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.Notification;
import com.imprimelibros.erp.redsys.RedsysService.PaymentRequest;
import org.springframework.beans.factory.annotation.Autowired;
import com.imprimelibros.erp.redsys.RedsysService.RedsysNotification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -27,91 +25,126 @@ public class PaymentService {
private final ObjectMapper om = new ObjectMapper();
public PaymentService(PaymentRepository payRepo,
PaymentTransactionRepository txRepo,
RefundRepository refundRepo,
RedsysService redsysService) {
PaymentTransactionRepository txRepo,
RefundRepository refundRepo,
RedsysService redsysService) {
this.payRepo = payRepo;
this.txRepo = txRepo;
this.refundRepo = refundRepo;
this.redsysService = redsysService;
}
/** Crea Payment y devuelve form auto-submit Redsys. Ds_Order = 12 dígitos con el ID. */
/**
* 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) throws Exception {
public FormPayload createRedsysPayment(Long orderId, long amountCents, String currency, String method)
throws Exception {
Payment p = new Payment();
p.setOrderId(orderId);
p.setOrderId(orderId); // <- ahora puede ser null
p.setCurrency(currency);
p.setAmountTotalCents(amountCents);
p.setGateway("redsys");
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());
p.setGatewayOrderId(dsOrder);
payRepo.save(p);
PaymentRequest req = new PaymentRequest(dsOrder, amountCents, "Compra en Imprimelibros", "card");
return redsysService.buildRedirectForm(req);
RedsysService.PaymentRequest req = new RedsysService.PaymentRequest(dsOrder, amountCents,
"Compra en Imprimelibros");
if ("bizum".equalsIgnoreCase(method)) {
return redsysService.buildRedirectFormBizum(req);
} else {
return redsysService.buildRedirectForm(req);
}
}
/** Procesa notificación Redsys (ok/notify). Idempotente. */
// si aún tienes la versión antigua sin method, puedes dejar este overload si te
// viene bien:
@Transactional
public void handleRedsysNotification(String dsSignature, String dsMerchantParameters) throws Exception {
Notification notif = redsysService.validateAndParseNotification(dsSignature, dsMerchantParameters);
public FormPayload createRedsysPayment(Long orderId, long amountCents, String currency) throws Exception {
return createRedsysPayment(orderId, amountCents, currency, "card");
}
Payment p = payRepo.findByGatewayAndGatewayOrderId("redsys", notif.getOrder())
.orElseThrow(() -> new IllegalStateException("Payment no encontrado para Ds_Order " + notif.getOrder()));
/**
* Procesa una notificación Redsys (OK/notify) con la API oficial:
* - validateAndParseNotification usa createMerchantSignatureNotif +
* decodeMerchantParameters
*/
@Transactional
public void handleRedsysNotification(String dsSignature, String dsMerchantParametersB64) throws Exception {
RedsysNotification notif = redsysService.validateAndParseNotification(dsSignature, dsMerchantParametersB64);
if (!Objects.equals(p.getCurrency(), notif.getCurrency()))
Payment p = payRepo.findByGatewayAndGatewayOrderId("redsys", notif.order)
.orElseThrow(() -> new IllegalStateException("Payment no encontrado para Ds_Order " + notif.order));
if (!Objects.equals(p.getCurrency(), notif.currency)) {
throw new IllegalStateException("Divisa inesperada");
if (!Objects.equals(p.getAmountTotalCents(), notif.getAmountCents()))
}
if (!Objects.equals(p.getAmountTotalCents(), notif.amountCents)) {
throw new IllegalStateException("Importe inesperado");
}
// ¿Ya registrado? Si ya capturaste, no repitas.
if (p.getStatus() == PaymentStatus.CAPTURED || p.getStatus() == PaymentStatus.PARTIALLY_REFUNDED || p.getStatus() == PaymentStatus.REFUNDED) {
return; // idempotencia simple a nivel Payment
// Idempotencia sencilla: si ya está capturado o reembolsado, no creamos otra
// transacción
if (p.getStatus() == PaymentStatus.CAPTURED
|| p.getStatus() == PaymentStatus.PARTIALLY_REFUNDED
|| p.getStatus() == PaymentStatus.REFUNDED) {
return;
}
PaymentTransaction tx = new PaymentTransaction();
tx.setPayment(p);
tx.setType(PaymentTransactionType.CAPTURE);
tx.setCurrency(p.getCurrency());
tx.setAmountCents(notif.getAmountCents());
tx.setStatus(notif.isAuthorized() ? PaymentTransactionStatus.SUCCEEDED : PaymentTransactionStatus.FAILED);
// En Redsys el authorization code suele estar en Ds_AuthorisationCode
Object authCode = notif.getRaw().get("Ds_AuthorisationCode");
tx.setAmountCents(notif.amountCents);
tx.setStatus(notif.authorized() ? PaymentTransactionStatus.SUCCEEDED
: PaymentTransactionStatus.FAILED);
Object authCode = notif.raw.get("Ds_AuthorisationCode");
tx.setGatewayTransactionId(authCode != null ? String.valueOf(authCode) : null);
tx.setGatewayResponseCode(notif.getResponse());
tx.setResponsePayload(om.writeValueAsString(notif.getRaw()));
tx.setGatewayResponseCode(notif.response);
tx.setResponsePayload(om.writeValueAsString(notif.raw));
tx.setProcessedAt(LocalDateTime.now());
txRepo.save(tx);
if (notif.isAuthorized()) {
if (notif.authorized()) {
p.setAuthorizationCode(tx.getGatewayTransactionId());
p.setStatus(PaymentStatus.CAPTURED);
p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.getAmountCents());
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);
}
/** Refund (simulado a nivel pasarela; actualiza BD). Sustituye gatewayRefundId por el real cuando lo tengas. */
// ---- refundViaRedsys y bank_transfer igual que antes, no tocan RedsysService
// ----
@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");
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");
if (amountCents > maxRefundable)
throw new IllegalStateException("Importe de devolución supera lo capturado");
txRepo.findByIdempotencyKey(idempotencyKey)
.ifPresent(t -> { throw new IllegalStateException("Reembolso ya procesado"); });
.ifPresent(t -> {
throw new IllegalStateException("Reembolso ya procesado");
});
Refund r = new Refund();
r.setPayment(p);
@ -120,7 +153,8 @@ public class PaymentService {
r.setRequestedAt(LocalDateTime.now());
r = refundRepo.save(r);
String gatewayRefundId = "REF-" + UUID.randomUUID(); // TODO: sustituir por el ID real de Redsys si usas su canal de devoluciones
String gatewayRefundId = "REF-" + UUID.randomUUID(); // aquí iría el ID real si alguna vez llamas a un API de
// devoluciones
PaymentTransaction tx = new PaymentTransaction();
tx.setPayment(p);
@ -148,23 +182,22 @@ public class PaymentService {
payRepo.save(p);
}
/** Transferencia bancaria: crea Payment en espera de ingreso. */
@Transactional
public Payment createBankTransferPayment(Long orderId, long amountCents, String currency) {
Payment p = new Payment();
p.setOrderId(orderId);
p.setOrderId(orderId); // null en tu caso actual
p.setCurrency(currency);
p.setAmountTotalCents(amountCents);
p.setGateway("bank_transfer");
p.setStatus(PaymentStatus.REQUIRES_ACTION);
p.setStatus(PaymentStatus.REQUIRES_ACTION); // pendiente de ingreso
return payRepo.save(p);
}
/** Marca transferencia como conciliada (capturada). */
@Transactional
public void markBankTransferAsCaptured(Long paymentId) {
Payment p = payRepo.findById(paymentId).orElseThrow();
if (!"bank_transfer".equals(p.getGateway())) throw new IllegalStateException("No es transferencia");
if (!"bank_transfer".equals(p.getGateway()))
throw new IllegalStateException("No es transferencia");
p.setAmountCapturedCents(p.getAmountTotalCents());
p.setCapturedAt(LocalDateTime.now());
p.setStatus(PaymentStatus.CAPTURED);