mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 00:48:49 +00:00
trabajando en devoluciones
This commit is contained in:
@ -4,20 +4,15 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
import com.imprimelibros.erp.payments.model.PaymentTransactionStatus.*;
|
|
||||||
|
|
||||||
import com.imprimelibros.erp.common.Utils;
|
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.DataTable;
|
||||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||||
@ -25,100 +20,128 @@ import com.imprimelibros.erp.datatables.DataTablesResponse;
|
|||||||
import com.imprimelibros.erp.payments.model.Payment;
|
import com.imprimelibros.erp.payments.model.Payment;
|
||||||
import com.imprimelibros.erp.payments.model.PaymentTransaction;
|
import com.imprimelibros.erp.payments.model.PaymentTransaction;
|
||||||
import com.imprimelibros.erp.payments.model.PaymentTransactionStatus;
|
import com.imprimelibros.erp.payments.model.PaymentTransactionStatus;
|
||||||
|
import com.imprimelibros.erp.payments.model.PaymentTransactionType;
|
||||||
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
||||||
import com.imprimelibros.erp.users.User;
|
import com.imprimelibros.erp.users.User;
|
||||||
import com.imprimelibros.erp.users.UserDao;
|
import com.imprimelibros.erp.users.UserDao;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/pagos")
|
@RequestMapping("/pagos")
|
||||||
@PreAuthorize("hasRole('SUPERADMIN')")
|
@PreAuthorize("hasRole('SUPERADMIN')")
|
||||||
public class PaymentController {
|
public class PaymentController {
|
||||||
|
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
|
||||||
protected final PaymentTransactionRepository repoPaymentTransaction;
|
protected final PaymentTransactionRepository repoPaymentTransaction;
|
||||||
protected final UserDao repoUser;
|
protected final UserDao repoUser;
|
||||||
|
|
||||||
public PaymentController(PaymentTransactionRepository repoPaymentTransaction, UserDao repoUser) {
|
public PaymentController(PaymentTransactionRepository repoPaymentTransaction, UserDao repoUser,
|
||||||
|
MessageSource messageSource) {
|
||||||
this.repoPaymentTransaction = repoPaymentTransaction;
|
this.repoPaymentTransaction = repoPaymentTransaction;
|
||||||
this.repoUser = repoUser;
|
this.repoUser = repoUser;
|
||||||
|
this.messageSource = messageSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping()
|
@GetMapping()
|
||||||
public String index() {
|
public String index() {
|
||||||
return "imprimelibros/pagos/gestion-pagos";
|
return "imprimelibros/pagos/gestion-pagos";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping(value = "datatable/redsys", produces = "application/json")
|
@GetMapping(value = "datatable/redsys", produces = "application/json")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public DataTablesResponse<Map<String, Object>> getDatatableRedsys(HttpServletRequest request,Locale locale) {
|
public DataTablesResponse<Map<String, Object>> getDatatableRedsys(HttpServletRequest request, Locale locale) {
|
||||||
|
|
||||||
DataTablesRequest dt = DataTablesParser.from(request);
|
DataTablesRequest dt = DataTablesParser.from(request);
|
||||||
|
|
||||||
List<String> searchable = List.of(
|
List<String> searchable = List.of(
|
||||||
);
|
"payment.gatewayOrderId",
|
||||||
|
"payment.orderId"
|
||||||
|
// "client" no, porque lo calculas a posteriori
|
||||||
|
);
|
||||||
|
|
||||||
|
// Campos ordenables
|
||||||
List<String> orderable = List.of(
|
List<String> orderable = List.of(
|
||||||
|
"payment.gatewayOrderId",
|
||||||
);
|
"payment.orderId",
|
||||||
|
"amountCents",
|
||||||
|
"payment.amountRefundedCents",
|
||||||
|
"createdAt");
|
||||||
|
|
||||||
Specification<PaymentTransaction> base = Specification.allOf(
|
Specification<PaymentTransaction> base = Specification.allOf(
|
||||||
(root, query, cb) -> cb.equal(root.get("status"), PaymentTransactionStatus.succeeded));
|
(root, query, cb) -> cb.equal(root.get("status"), PaymentTransactionStatus.succeeded));
|
||||||
|
base = base.and((root, query, cb) -> cb.equal(root.get("type"), PaymentTransactionType.CAPTURE));
|
||||||
|
|
||||||
|
String clientSearch = dt.getColumnSearch("client");
|
||||||
|
|
||||||
|
// 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification
|
||||||
|
if (clientSearch != null) {
|
||||||
|
List<Long> userIds = repoUser.findIdsByFullNameLike(clientSearch.trim());
|
||||||
|
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
// Ningún usuario coincide → forzamos 0 resultados
|
||||||
|
base = base.and((root, query, cb) -> cb.disjunction());
|
||||||
|
} else {
|
||||||
|
base = base.and((root, query, cb) -> root.join("payment").get("userId").in(userIds));
|
||||||
|
}
|
||||||
|
}
|
||||||
Long total = repoPaymentTransaction.count(base);
|
Long total = repoPaymentTransaction.count(base);
|
||||||
|
|
||||||
return DataTable
|
return DataTable
|
||||||
.of(repoPaymentTransaction, PaymentTransaction.class, dt, searchable) // 'searchable' en DataTable.java
|
.of(repoPaymentTransaction, PaymentTransaction.class, dt, searchable)
|
||||||
// edita columnas "reales":
|
|
||||||
.orderable(orderable)
|
.orderable(orderable)
|
||||||
.add("created_at", (pago) -> {
|
.add("created_at", pago -> Utils.formatDateTime(pago.getCreatedAt(), locale))
|
||||||
return Utils.formatDateTime(pago.getCreatedAt(), locale);
|
.add("client", pago -> {
|
||||||
})
|
|
||||||
.add("client", (pago) -> {
|
|
||||||
if (pago.getPayment() != null && pago.getPayment().getUserId() != null) {
|
if (pago.getPayment() != null && pago.getPayment().getUserId() != null) {
|
||||||
Payment payment = pago.getPayment();
|
Payment payment = pago.getPayment();
|
||||||
if(payment.getUserId() != null) {
|
if (payment.getUserId() != null) {
|
||||||
Optional<User> user = repoUser.findById(payment.getUserId());
|
Optional<User> user = repoUser.findById(payment.getUserId().longValue());
|
||||||
return user.map(User::getFullName).orElse("");
|
return user.map(User::getFullName).orElse("");
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
})
|
})
|
||||||
.add("gateway_order_id", (pago) -> {
|
.add("gateway_order_id", pago -> {
|
||||||
if (pago.getPayment() != null) {
|
if (pago.getPayment() != null) {
|
||||||
return pago.getPayment().getGatewayOrderId();
|
return pago.getPayment().getGatewayOrderId();
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.add("orderId", (pago) -> {
|
.add("orderId", pago -> {
|
||||||
if (pago.getPayment() != null && pago.getPayment().getOrderId() != null) {
|
if (pago.getPayment() != null && pago.getPayment().getOrderId() != null) {
|
||||||
return pago.getPayment().getOrderId().toString();
|
return pago.getPayment().getOrderId().toString();
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.add("amount_cents", (pago) -> {
|
.add("amount_cents", pago -> Utils.formatCurrency(pago.getAmountCents() / 100.0, locale))
|
||||||
return Utils.formatCurrency(pago.getAmountCents() / 100.0, locale);
|
.add("amount_cents_refund", pago -> {
|
||||||
|
Payment payment = pago.getPayment();
|
||||||
|
if (payment != null) {
|
||||||
|
return Utils.formatCurrency(payment.getAmountRefundedCents() / 100.0, locale);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
})
|
})
|
||||||
.add("actions", (pago) -> {
|
.add("actions", pago -> {
|
||||||
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
Payment p = pago.getPayment();
|
||||||
" <a href=\"javascript:void(0);\" data-id=\"" + pago.getId()
|
if (p != null) {
|
||||||
+ "\" class=\"link-success btn-edit-pago fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
|
if (pago.getAmountCents() - p.getAmountRefundedCents() > 0) {
|
||||||
+ " <a href=\"javascript:void(0);\" data-id=\"" + pago.getId()
|
return "<span class=\'badge bg-secondary btn-refund-payment \' data-dsOrderId=\'"
|
||||||
+ "\" class=\"link-danger btn-delete-pago fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>\n"
|
+ p.getGatewayOrderId()
|
||||||
+ " </div>";
|
+ "\' data-transactionId=\'" + pago.getPayment().getId()
|
||||||
|
+ "\' style=\'cursor: pointer;\'>"
|
||||||
|
+ messageSource.getMessage("pagos.table.devuelto", null, locale) + "</span>";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.where(base)
|
.where(base)
|
||||||
// Filtros custom:
|
|
||||||
.toJson(total);
|
.toJson(total);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,6 @@ public class PaymentService {
|
|||||||
this.webhookEventRepo = webhookEventRepo;
|
this.webhookEventRepo = webhookEventRepo;
|
||||||
this.cartService = cartService;
|
this.cartService = cartService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crea el Payment en BD y construye el formulario de Redsys usando la API
|
* Crea el Payment en BD y construye el formulario de Redsys usando la API
|
||||||
@ -54,7 +53,7 @@ public class PaymentService {
|
|||||||
p.setOrderId(null);
|
p.setOrderId(null);
|
||||||
|
|
||||||
Cart cart = this.cartService.findById(cartId);
|
Cart cart = this.cartService.findById(cartId);
|
||||||
if(cart != null && cart.getUserId() != null) {
|
if (cart != null && cart.getUserId() != null) {
|
||||||
p.setUserId(cart.getUserId());
|
p.setUserId(cart.getUserId());
|
||||||
}
|
}
|
||||||
p.setCurrency(currency);
|
p.setCurrency(currency);
|
||||||
@ -198,21 +197,22 @@ public class PaymentService {
|
|||||||
p.setFailedAt(LocalDateTime.now());
|
p.setFailedAt(LocalDateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
if(authorized) {
|
if (authorized) {
|
||||||
// GENERAR PEDIDO A PARTIR DEL CARRITO
|
// GENERAR PEDIDO A PARTIR DEL CARRITO
|
||||||
Cart cart = this.cartService.findById(notif.cartId);
|
Cart cart = this.cartService.findById(notif.cartId);
|
||||||
if(cart != null) {
|
if (cart != null) {
|
||||||
// Bloqueamos el carrito
|
// Bloqueamos el carrito
|
||||||
this.cartService.lockCartById(cart.getId());
|
this.cartService.lockCartById(cart.getId());
|
||||||
// order ID es generado dentro de createOrderFromCart donde se marcan los presupuestos como no editables
|
// order ID es generado dentro de createOrderFromCart donde se marcan los
|
||||||
// Long orderId = this.cartService.pedidoService.createOrderFromCart(cart.getId(), p.getId());
|
// presupuestos como no editables
|
||||||
|
// Long orderId =
|
||||||
|
// this.cartService.pedidoService.createOrderFromCart(cart.getId(), p.getId());
|
||||||
// p.setOrderId(orderId);
|
// p.setOrderId(orderId);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
payRepo.save(p);
|
payRepo.save(p);
|
||||||
|
|
||||||
|
|
||||||
if (!authorized) {
|
if (!authorized) {
|
||||||
ev.setLastError("Payment declined (Ds_Response=" + notif.response + ")");
|
ev.setLastError("Payment declined (Ds_Response=" + notif.response + ")");
|
||||||
@ -230,9 +230,8 @@ public class PaymentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- refundViaRedsys y bank_transfer igual que antes, no tocan RedsysService
|
// ---- refundViaRedsys
|
||||||
// ----
|
// ----
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void refundViaRedsys(Long paymentId, long amountCents, String idempotencyKey) {
|
public void refundViaRedsys(Long paymentId, long amountCents, String idempotencyKey) {
|
||||||
Payment p = payRepo.findById(paymentId)
|
Payment p = payRepo.findById(paymentId)
|
||||||
@ -240,6 +239,7 @@ public class PaymentService {
|
|||||||
|
|
||||||
if (amountCents <= 0)
|
if (amountCents <= 0)
|
||||||
throw new IllegalArgumentException("Importe inválido");
|
throw new IllegalArgumentException("Importe inválido");
|
||||||
|
|
||||||
long maxRefundable = p.getAmountCapturedCents() - p.getAmountRefundedCents();
|
long maxRefundable = p.getAmountCapturedCents() - p.getAmountRefundedCents();
|
||||||
if (amountCents > maxRefundable)
|
if (amountCents > maxRefundable)
|
||||||
throw new IllegalStateException("Importe de devolución supera lo capturado");
|
throw new IllegalStateException("Importe de devolución supera lo capturado");
|
||||||
@ -256,8 +256,18 @@ public class PaymentService {
|
|||||||
r.setRequestedAt(LocalDateTime.now());
|
r.setRequestedAt(LocalDateTime.now());
|
||||||
r = refundRepo.save(r);
|
r = refundRepo.save(r);
|
||||||
|
|
||||||
String gatewayRefundId = "REF-" + UUID.randomUUID(); // aquí iría el ID real si alguna vez llamas a un API de
|
String gatewayRefundId;
|
||||||
// devoluciones
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
PaymentTransaction tx = new PaymentTransaction();
|
PaymentTransaction tx = new PaymentTransaction();
|
||||||
tx.setPayment(p);
|
tx.setPayment(p);
|
||||||
@ -285,13 +295,14 @@ public class PaymentService {
|
|||||||
payRepo.save(p);
|
payRepo.save(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Payment createBankTransferPayment(Long cartId, long amountCents, String currency) {
|
public Payment createBankTransferPayment(Long cartId, long amountCents, String currency) {
|
||||||
Payment p = new Payment();
|
Payment p = new Payment();
|
||||||
p.setOrderId(null);
|
p.setOrderId(null);
|
||||||
|
|
||||||
Cart cart = this.cartService.findById(cartId);
|
Cart cart = this.cartService.findById(cartId);
|
||||||
if(cart != null && cart.getUserId() != null) {
|
if (cart != null && cart.getUserId() != null) {
|
||||||
p.setUserId(cart.getUserId());
|
p.setUserId(cart.getUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,11 @@ import sis.redsys.api.ApiMacSha256;
|
|||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
@ -18,6 +23,10 @@ import java.util.Objects;
|
|||||||
public class RedsysService {
|
public class RedsysService {
|
||||||
|
|
||||||
// ---------- CONFIG ----------
|
// ---------- CONFIG ----------
|
||||||
|
@Value("${redsys.url}")
|
||||||
|
private String url;
|
||||||
|
@Value("${redsys.refund.url}")
|
||||||
|
private String urlRefund;
|
||||||
@Value("${redsys.merchant-code}")
|
@Value("${redsys.merchant-code}")
|
||||||
private String merchantCode;
|
private String merchantCode;
|
||||||
@Value("${redsys.terminal}")
|
@Value("${redsys.terminal}")
|
||||||
@ -37,6 +46,8 @@ public class RedsysService {
|
|||||||
@Value("${redsys.environment}")
|
@Value("${redsys.environment}")
|
||||||
private String env;
|
private String env;
|
||||||
|
|
||||||
|
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
|
||||||
// ---------- RECORDS ----------
|
// ---------- RECORDS ----------
|
||||||
// Pedido a Redsys
|
// Pedido a Redsys
|
||||||
public record PaymentRequest(String order, long amountCents, String description, Long cartId) {
|
public record PaymentRequest(String order, long amountCents, String description, Long cartId) {
|
||||||
@ -89,9 +100,11 @@ public class RedsysService {
|
|||||||
String merchantParameters = api.createMerchantParameters();
|
String merchantParameters = api.createMerchantParameters();
|
||||||
String signature = api.createMerchantSignature(secretKeyBase64);
|
String signature = api.createMerchantSignature(secretKeyBase64);
|
||||||
|
|
||||||
String action = "test".equalsIgnoreCase(env)
|
String action = url;
|
||||||
? "https://sis-t.redsys.es:25443/sis/realizarPago"
|
/*
|
||||||
: "https://sis.redsys.es/sis/realizarPago";
|
* ? "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||||
|
* : "https://sis.redsys.es/sis/realizarPago";
|
||||||
|
*/
|
||||||
|
|
||||||
return new FormPayload(action, "HMAC_SHA256_V1", merchantParameters, signature);
|
return new FormPayload(action, "HMAC_SHA256_V1", merchantParameters, signature);
|
||||||
}
|
}
|
||||||
@ -231,4 +244,84 @@ public class RedsysService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solicita a Redsys una devolución (TransactionType = 3)
|
||||||
|
*
|
||||||
|
* @param order El mismo Ds_Merchant_Order que se usó en el cobro.
|
||||||
|
* @param amountCents Importe en céntimos a devolver.
|
||||||
|
* @return gatewayRefundId (p.ej. Ds_AuthorisationCode o Ds_Order)
|
||||||
|
*/
|
||||||
|
public String requestRefund(String order, long amountCents) throws Exception {
|
||||||
|
ApiMacSha256 api = new ApiMacSha256();
|
||||||
|
|
||||||
|
// Montar parámetros para el refund
|
||||||
|
api.setParameter("DS_MERCHANT_MERCHANTCODE", merchantCode);
|
||||||
|
api.setParameter("DS_MERCHANT_TERMINAL", terminal);
|
||||||
|
api.setParameter("DS_MERCHANT_ORDER", order);
|
||||||
|
api.setParameter("DS_MERCHANT_AMOUNT", String.valueOf(amountCents));
|
||||||
|
api.setParameter("DS_MERCHANT_CURRENCY", currency);
|
||||||
|
api.setParameter("DS_MERCHANT_TRANSACTIONTYPE", "3"); // 3 = devolución
|
||||||
|
api.setParameter("DS_MERCHANT_MERCHANTURL", "");
|
||||||
|
api.setParameter("DS_MERCHANT_URLOK", "");
|
||||||
|
api.setParameter("DS_MERCHANT_URLKO", "");
|
||||||
|
|
||||||
|
// Crear parámetros y firma (como en tu PHP)
|
||||||
|
String merchantParameters = api.createMerchantParameters();
|
||||||
|
String signature = api.createMerchantSignature(secretKeyBase64);
|
||||||
|
|
||||||
|
// Montar el JSON para Redsys REST
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"Ds_MerchantParameters": "%s",
|
||||||
|
"Ds_Signature": "%s",
|
||||||
|
"Ds_SignatureVersion": "HMAC_SHA256_V1"
|
||||||
|
}
|
||||||
|
""".formatted(merchantParameters, signature);
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(urlRefund))
|
||||||
|
.header("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
|
||||||
|
System.out.println("### Redsys refund REST request:\n" + json);
|
||||||
|
System.out.println("### HTTP " + response.statusCode());
|
||||||
|
System.out.println("### Redsys refund REST response:\n" + response.body());
|
||||||
|
|
||||||
|
if (response.statusCode() / 100 != 2)
|
||||||
|
throw new IllegalStateException("HTTP error Redsys refund: " + response.statusCode());
|
||||||
|
|
||||||
|
if (response.body() == null || response.body().isBlank())
|
||||||
|
throw new IllegalStateException("Respuesta vacía de Redsys refund REST");
|
||||||
|
|
||||||
|
// Parsear la respuesta JSON
|
||||||
|
Map<String, Object> respMap = MAPPER.readValue(response.body(), new TypeReference<>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redsys puede devolver "Ds_MerchantParameters" o "errorCode"
|
||||||
|
if (respMap.containsKey("errorCode")) {
|
||||||
|
throw new IllegalStateException("Error Redsys refund: " + respMap.get("errorCode"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String dsMerchantParametersResp = (String) respMap.get("Ds_MerchantParameters");
|
||||||
|
if (dsMerchantParametersResp == null) {
|
||||||
|
throw new IllegalStateException("Respuesta Redsys refund sin Ds_MerchantParameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decodificar MerchantParameters de la respuesta
|
||||||
|
Map<String, Object> decoded = decodeMerchantParametersToMap(dsMerchantParametersResp);
|
||||||
|
System.out.println("### Redsys refund decoded response:\n" + decoded);
|
||||||
|
|
||||||
|
String dsResponse = String.valueOf(decoded.get("Ds_Response"));
|
||||||
|
if (!"0900".equals(dsResponse)) {
|
||||||
|
throw new IllegalStateException("Devolución rechazada, Ds_Response=" + dsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.valueOf(decoded.getOrDefault("Ds_AuthorisationCode", order));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,60 +19,63 @@ import org.springframework.lang.Nullable;
|
|||||||
@Repository
|
@Repository
|
||||||
public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
||||||
|
|
||||||
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
||||||
@Override
|
@Override
|
||||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||||
@NonNull
|
@NonNull
|
||||||
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
||||||
|
|
||||||
Optional<User> findByUserNameIgnoreCase(String userName);
|
Optional<User> findByUserNameIgnoreCase(String userName);
|
||||||
|
|
||||||
boolean existsByUserNameIgnoreCase(String userName);
|
boolean existsByUserNameIgnoreCase(String userName);
|
||||||
|
|
||||||
// Para comprobar si existe al hacer signup
|
// Para comprobar si existe al hacer signup
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT id, deleted, enabled
|
SELECT id, deleted, enabled
|
||||||
FROM users
|
FROM users
|
||||||
WHERE LOWER(username) = LOWER(:userName)
|
WHERE LOWER(username) = LOWER(:userName)
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
Optional<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
|
Optional<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
|
||||||
|
|
||||||
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
||||||
|
|
||||||
// Nuevo: para login/negocio "activo"
|
// Nuevo: para login/negocio "activo"
|
||||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||||
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
||||||
|
|
||||||
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
||||||
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
||||||
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
||||||
|
|
||||||
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
||||||
List<User> findAllDeleted();
|
List<User> findAllDeleted();
|
||||||
|
|
||||||
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
||||||
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT DISTINCT u
|
SELECT DISTINCT u
|
||||||
FROM User u
|
FROM User u
|
||||||
JOIN u.rolesLink rl
|
JOIN u.rolesLink rl
|
||||||
JOIN rl.role r
|
JOIN rl.role r
|
||||||
WHERE (:role IS NULL OR r.name = :role)
|
WHERE (:role IS NULL OR r.name = :role)
|
||||||
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||||
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||||
""", countQuery = """
|
""", countQuery = """
|
||||||
SELECT COUNT(DISTINCT u.id)
|
SELECT COUNT(DISTINCT u.id)
|
||||||
FROM User u
|
FROM User u
|
||||||
JOIN u.rolesLink rl
|
JOIN u.rolesLink rl
|
||||||
JOIN rl.role r
|
JOIN rl.role r
|
||||||
WHERE (:role IS NULL OR r.name = :role)
|
WHERE (:role IS NULL OR r.name = :role)
|
||||||
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||||
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||||
""")
|
""")
|
||||||
Page<User> searchUsers(@Param("role") String role,
|
Page<User> searchUsers(@Param("role") String role,
|
||||||
@Param("q") String q,
|
@Param("q") String q,
|
||||||
Pageable pageable);
|
Pageable pageable);
|
||||||
|
|
||||||
|
@Query("select u.id from User u where lower(u.fullName) like lower(concat('%', :name, '%'))")
|
||||||
|
List<Long> findIdsByFullNameLike(@Param("name") String name);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ safekat.api.password=Safekat2024
|
|||||||
|
|
||||||
# Configuración Redsys
|
# Configuración Redsys
|
||||||
redsys.environment=test
|
redsys.environment=test
|
||||||
|
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
||||||
|
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
||||||
redsys.urls.ok=http://localhost:8080/pagos/redsys/ok
|
redsys.urls.ok=http://localhost:8080/pagos/redsys/ok
|
||||||
redsys.urls.ko=http://localhost:8080/pagos/redsys/ko
|
redsys.urls.ko=http://localhost:8080/pagos/redsys/ko
|
||||||
redsys.urls.notify=https://orological-sacrilegiously-lucille.ngrok-free.dev/pagos/redsys/notify
|
redsys.urls.notify=https://orological-sacrilegiously-lucille.ngrok-free.dev/pagos/redsys/notify
|
||||||
@ -20,6 +20,8 @@ safekat.api.password=Safekat2024
|
|||||||
|
|
||||||
# Configuración Redsys
|
# Configuración Redsys
|
||||||
redsys.environment=test
|
redsys.environment=test
|
||||||
|
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
||||||
|
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
||||||
redsys.urls.ok=https://imprimelibros.jjimenez.eu/pagos/redsys/ok
|
redsys.urls.ok=https://imprimelibros.jjimenez.eu/pagos/redsys/ok
|
||||||
redsys.urls.ko=https://imprimelibros.jjimenez.eu/pagos/redsys/ko
|
redsys.urls.ko=https://imprimelibros.jjimenez.eu/pagos/redsys/ko
|
||||||
redsys.urls.notify=https://imprimelibros.jjimenez.eu/pagos/redsys/notify
|
redsys.urls.notify=https://imprimelibros.jjimenez.eu/pagos/redsys/notify
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
databaseChangeLog:
|
||||||
|
- changeSet:
|
||||||
|
id: 0009-add-composite-unique-txid-type
|
||||||
|
author: JJO
|
||||||
|
changes:
|
||||||
|
# 1️⃣ Eliminar el índice único anterior
|
||||||
|
- dropUniqueConstraint:
|
||||||
|
constraintName: uq_tx_gateway_txid
|
||||||
|
tableName: payment_transactions
|
||||||
|
|
||||||
|
# 2️⃣ Crear índice único compuesto por gateway_transaction_id + type
|
||||||
|
- addUniqueConstraint:
|
||||||
|
tableName: payment_transactions
|
||||||
|
columnNames: gateway_transaction_id, type
|
||||||
|
constraintName: uq_tx_gateway_txid_type
|
||||||
@ -14,4 +14,6 @@ databaseChangeLog:
|
|||||||
- include:
|
- include:
|
||||||
file: db/changelog/changesets/0007-payments-core.yml
|
file: db/changelog/changesets/0007-payments-core.yml
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/changesets/0008-update-cart-status-constraint.yml
|
file: db/changelog/changesets/0008-update-cart-status-constraint.yml
|
||||||
|
- include:
|
||||||
|
file: db/changelog/changesets/0009-add-composite-unique-txid-type.yml
|
||||||
@ -7,6 +7,7 @@ pagos.table.cliente.nombre=Nombre Cliente
|
|||||||
pagos.table.redsys.id=Cod. Redsys
|
pagos.table.redsys.id=Cod. Redsys
|
||||||
pagos.table.pedido.id=Pedido
|
pagos.table.pedido.id=Pedido
|
||||||
pagos.table.cantidad=Cantidad
|
pagos.table.cantidad=Cantidad
|
||||||
|
pagos.table.devuelto=Devolución
|
||||||
pagos.table.fecha=Fecha
|
pagos.table.fecha=Fecha
|
||||||
pagos.table.estado=Estado
|
pagos.table.estado=Estado
|
||||||
pagos.table.acciones=Acciones
|
pagos.table.acciones=Acciones
|
||||||
|
|||||||
@ -23,9 +23,10 @@ $(() => {
|
|||||||
serverSide: true,
|
serverSide: true,
|
||||||
orderCellsTop: true,
|
orderCellsTop: true,
|
||||||
pageLength: 50,
|
pageLength: 50,
|
||||||
|
lengthMenu: [10, 25, 50, 100, 500],
|
||||||
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||||
responsive: true,
|
responsive: true,
|
||||||
dom: 'lrBtip',
|
dom: 'lBrtip',
|
||||||
buttons: {
|
buttons: {
|
||||||
dom: {
|
dom: {
|
||||||
button: {
|
button: {
|
||||||
@ -45,16 +46,66 @@ $(() => {
|
|||||||
url: '/pagos/datatable/redsys',
|
url: '/pagos/datatable/redsys',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
order: [[4, 'asc']], // Ordena por fecha por defecto
|
order: [[5, 'desc']], // Ordena por fecha por defecto
|
||||||
columns: [
|
columns: [
|
||||||
{ data: 'client', name: 'user.fullName', orderable: true },
|
{ data: 'client', name: 'client', orderable: true },
|
||||||
{ data: 'gateway_order_id', name: 'payments.gateway_order_id', orderable: true },
|
{ data: 'gateway_order_id', name: 'payment.gatewayOrderId', orderable: true },
|
||||||
{ data: 'orderId', name: 'order.id', orderable: true },
|
{ data: 'orderId', name: 'payment.orderId', orderable: true },
|
||||||
{ data: 'amount_cents', name: 'amount_cents', orderable: true },
|
{ data: 'amount_cents', name: 'amountCents', orderable: true },
|
||||||
{ data: 'created_at', name: 'created_at', orderable: true },
|
{ data: 'amount_cents_refund', name: 'amountCentsRefund', orderable: true },
|
||||||
{ data: 'actions', name: 'actions' }
|
{ data: 'created_at', name: 'createdAt', orderable: true },
|
||||||
|
{ data: 'actions', name: 'actions', orderable: false, searchable: false }
|
||||||
|
|
||||||
],
|
],
|
||||||
columnDefs: [{ targets: -1, orderable: false, searchable: false }]
|
columnDefs: [{ targets: -1, orderable: false, searchable: false }]
|
||||||
});
|
});
|
||||||
|
|
||||||
})
|
// Fila de filtros = segunda fila del thead (index 1)
|
||||||
|
$('#pagos-redsys-datatable thead tr:eq(1) th').each(function (colIdx) {
|
||||||
|
const input = $(this).find('input');
|
||||||
|
if (input.length === 0) return; // columnas sin filtro
|
||||||
|
|
||||||
|
input.on('keyup change', function () {
|
||||||
|
const value = this.value;
|
||||||
|
if (table.column(colIdx).search() !== value) {
|
||||||
|
table.column(colIdx).search(value).draw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.btn-refund-payment', function () {
|
||||||
|
const dsOrderId = $(this).data('dsorderid');
|
||||||
|
const transactionId = $(this).data('transactionid');
|
||||||
|
// show swal confirmation with input for amount to refund
|
||||||
|
Swal.fire({
|
||||||
|
title: '¿Estás seguro de que deseas devolver este pago?',
|
||||||
|
text: 'Introduce la cantidad a devolver (en euros):',
|
||||||
|
input: 'number',
|
||||||
|
inputAttributes: {
|
||||||
|
min: 0,
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
const amountToRefund = parseFloat(result.value);
|
||||||
|
if (isNaN(amountToRefund) || amountToRefund <= 0) {
|
||||||
|
Swal.fire('Error', 'Cantidad inválida para la devolución.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: '/pagos/redsys/refund/' + transactionId,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
amountCents: amountToRefund*100
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.success) {
|
||||||
|
Swal.fire('Éxito', 'Pago devuelto con éxito.', 'success');
|
||||||
|
table.draw();
|
||||||
|
} else {
|
||||||
|
Swal.fire('Error', 'No se pudo procesar la devolución.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -8,8 +8,6 @@
|
|||||||
<th:block layout:fragment="pagecss">
|
<th:block layout:fragment="pagecss">
|
||||||
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block layout:fragment="pagecss">
|
|
||||||
</th:block>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -68,6 +66,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="arrow-transferencias" role="tabpanel">
|
<div class="tab-pane" id="arrow-transferencias" role="tabpanel">
|
||||||
|
<div></div>
|
||||||
<!---
|
<!---
|
||||||
<div
|
<div
|
||||||
th:insert="~{imprimelibros/presupuestos/presupuesto-list-items/tabla-anonimos :: tabla-anonimos}">
|
th:insert="~{imprimelibros/presupuestos/presupuesto-list-items/tabla-anonimos :: tabla-anonimos}">
|
||||||
|
|||||||
@ -6,13 +6,15 @@
|
|||||||
<th scope="col" th:text="#{pagos.table.redsys.id}">Cliente</th>
|
<th scope="col" th:text="#{pagos.table.redsys.id}">Cliente</th>
|
||||||
<th scope="col" th:text="#{pagos.table.pedido.id}">Pedido</th>
|
<th scope="col" th:text="#{pagos.table.pedido.id}">Pedido</th>
|
||||||
<th scope="col" th:text="#{pagos.table.cantidad}">Cantidad</th>
|
<th scope="col" th:text="#{pagos.table.cantidad}">Cantidad</th>
|
||||||
|
<th scope="col" th:text="#{pagos.table.devuelto}">Devolución</th>
|
||||||
<th scope="col" th:text="#{pagos.table.fecha}">Fecha</th>
|
<th scope="col" th:text="#{pagos.table.fecha}">Fecha</th>
|
||||||
<th scope="col" th:text="#{pagos.table.acciones}">Acciones</th>
|
<th scope="col" th:text="#{pagos.table.acciones}">Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="user.fullName" /></th>
|
<th><input type="text" class="form-control form-control-sm redsys-filter" /></th>
|
||||||
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="user.fullName" /></th>
|
<th><input type="text" class="form-control form-control-sm redsys-filter" /></th>
|
||||||
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="payments.gateway_order_id" /></th>
|
<th><input type="text" class="form-control form-control-sm redsys-filter" /></th>
|
||||||
|
<th></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th></th> <!-- Acciones (sin filtro) -->
|
<th></th> <!-- Acciones (sin filtro) -->
|
||||||
|
|||||||
@ -17,7 +17,8 @@
|
|||||||
<th scope="col" th:text="#{presupuesto.tabla.updated-at}">Actualizado el</th>
|
<th scope="col" th:text="#{presupuesto.tabla.updated-at}">Actualizado el</th>
|
||||||
<th scope="col" th:text="#{presupuesto.tabla.acciones}">Acciones</th>
|
<th scope="col" th:text="#{presupuesto.tabla.acciones}">Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="id" /></th>
|
<tr>
|
||||||
|
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="id" /></th>
|
||||||
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="titulo" /></th>
|
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="titulo" /></th>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
@ -48,7 +49,8 @@
|
|||||||
<option value="colorhq" th:text="#{presupuesto.color-premium}">Color HQ</option>
|
<option value="colorhq" th:text="#{presupuesto.color-premium}">Color HQ</option>
|
||||||
</select>
|
</select>
|
||||||
</th>
|
</th>
|
||||||
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="selectedTirada" /></th>
|
<th><input type="text" class="form-control form-control-sm presupuesto-filter"
|
||||||
|
data-col="selectedTirada" /></th>
|
||||||
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="paginas" />
|
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="paginas" />
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
@ -68,7 +70,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<th></th> <!-- Actualizado el (sin filtro) -->
|
<th></th> <!-- Actualizado el (sin filtro) -->
|
||||||
<th></th> <!-- Acciones (sin filtro) -->
|
<th></th> <!-- Acciones (sin filtro) -->
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user