trabajando en devoluciones

This commit is contained in:
2025-11-05 15:09:26 +01:00
parent ed32f773a4
commit a4443763d8
13 changed files with 330 additions and 124 deletions

View File

@ -4,20 +4,15 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import org.springframework.context.MessageSource;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.imprimelibros.erp.payments.model.PaymentTransactionStatus.*;
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.DataTablesParser;
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.PaymentTransaction;
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.users.User;
import com.imprimelibros.erp.users.UserDao;
import jakarta.servlet.http.HttpServletRequest;
@Controller
@RequestMapping("/pagos")
@PreAuthorize("hasRole('SUPERADMIN')")
public class PaymentController {
private final MessageSource messageSource;
protected final PaymentTransactionRepository repoPaymentTransaction;
protected final UserDao repoUser;
public PaymentController(PaymentTransactionRepository repoPaymentTransaction, UserDao repoUser) {
public PaymentController(PaymentTransactionRepository repoPaymentTransaction, UserDao repoUser,
MessageSource messageSource) {
this.repoPaymentTransaction = repoPaymentTransaction;
this.repoUser = repoUser;
this.messageSource = messageSource;
}
@GetMapping()
public String index() {
return "imprimelibros/pagos/gestion-pagos";
}
@GetMapping(value = "datatable/redsys", produces = "application/json")
@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);
List<String> searchable = List.of(
);
"payment.gatewayOrderId",
"payment.orderId"
// "client" no, porque lo calculas a posteriori
);
// Campos ordenables
List<String> orderable = List.of(
);
"payment.gatewayOrderId",
"payment.orderId",
"amountCents",
"payment.amountRefundedCents",
"createdAt");
Specification<PaymentTransaction> base = Specification.allOf(
(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);
return DataTable
.of(repoPaymentTransaction, PaymentTransaction.class, dt, searchable) // 'searchable' en DataTable.java
// edita columnas "reales":
.of(repoPaymentTransaction, PaymentTransaction.class, dt, searchable)
.orderable(orderable)
.add("created_at", (pago) -> {
return Utils.formatDateTime(pago.getCreatedAt(), locale);
})
.add("client", (pago) -> {
.add("created_at", pago -> Utils.formatDateTime(pago.getCreatedAt(), locale))
.add("client", pago -> {
if (pago.getPayment() != null && pago.getPayment().getUserId() != null) {
Payment payment = pago.getPayment();
if(payment.getUserId() != null) {
Optional<User> user = repoUser.findById(payment.getUserId());
if (payment.getUserId() != null) {
Optional<User> user = repoUser.findById(payment.getUserId().longValue());
return user.map(User::getFullName).orElse("");
}
return "";
} else {
return "";
}
return "";
})
.add("gateway_order_id", (pago) -> {
.add("gateway_order_id", pago -> {
if (pago.getPayment() != null) {
return pago.getPayment().getGatewayOrderId();
} else {
return "";
}
})
.add("orderId", (pago) -> {
.add("orderId", pago -> {
if (pago.getPayment() != null && pago.getPayment().getOrderId() != null) {
return pago.getPayment().getOrderId().toString();
} else {
return "";
}
})
.add("amount_cents", (pago) -> {
return Utils.formatCurrency(pago.getAmountCents() / 100.0, locale);
.add("amount_cents", pago -> 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) -> {
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
" <a href=\"javascript:void(0);\" data-id=\"" + pago.getId()
+ "\" class=\"link-success btn-edit-pago fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
+ " <a href=\"javascript:void(0);\" data-id=\"" + pago.getId()
+ "\" class=\"link-danger btn-delete-pago fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>\n"
+ " </div>";
.add("actions", pago -> {
Payment p = pago.getPayment();
if (p != null) {
if (pago.getAmountCents() - p.getAmountRefundedCents() > 0) {
return "<span class=\'badge bg-secondary btn-refund-payment \' data-dsOrderId=\'"
+ p.getGatewayOrderId()
+ "\' data-transactionId=\'" + pago.getPayment().getId()
+ "\' style=\'cursor: pointer;\'>"
+ messageSource.getMessage("pagos.table.devuelto", null, locale) + "</span>";
}
return "";
} else {
return "";
}
})
.where(base)
// Filtros custom:
.toJson(total);
}
}

View File

@ -41,7 +41,6 @@ public class PaymentService {
this.webhookEventRepo = webhookEventRepo;
this.cartService = cartService;
}
/**
* Crea el Payment en BD y construye el formulario de Redsys usando la API
@ -54,7 +53,7 @@ public class PaymentService {
p.setOrderId(null);
Cart cart = this.cartService.findById(cartId);
if(cart != null && cart.getUserId() != null) {
if (cart != null && cart.getUserId() != null) {
p.setUserId(cart.getUserId());
}
p.setCurrency(currency);
@ -198,21 +197,22 @@ public class PaymentService {
p.setFailedAt(LocalDateTime.now());
}
if(authorized) {
if (authorized) {
// GENERAR PEDIDO A PARTIR DEL CARRITO
Cart cart = this.cartService.findById(notif.cartId);
if(cart != null) {
if (cart != null) {
// Bloqueamos el carrito
this.cartService.lockCartById(cart.getId());
// order ID es generado dentro de createOrderFromCart donde se marcan los presupuestos como no editables
// Long orderId = this.cartService.pedidoService.createOrderFromCart(cart.getId(), p.getId());
// order ID es generado dentro de createOrderFromCart donde se marcan los
// presupuestos como no editables
// Long orderId =
// this.cartService.pedidoService.createOrderFromCart(cart.getId(), p.getId());
// p.setOrderId(orderId);
}
}
payRepo.save(p);
if (!authorized) {
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
public void refundViaRedsys(Long paymentId, long amountCents, String idempotencyKey) {
Payment p = payRepo.findById(paymentId)
@ -240,6 +239,7 @@ public class PaymentService {
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");
@ -256,8 +256,18 @@ public class PaymentService {
r.setRequestedAt(LocalDateTime.now());
r = refundRepo.save(r);
String gatewayRefundId = "REF-" + UUID.randomUUID(); // aquí iría el ID real si alguna vez llamas a un API de
// devoluciones
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);
}
PaymentTransaction tx = new PaymentTransaction();
tx.setPayment(p);
@ -285,13 +295,14 @@ public class PaymentService {
payRepo.save(p);
}
@Transactional
public Payment createBankTransferPayment(Long cartId, long amountCents, String currency) {
Payment p = new Payment();
p.setOrderId(null);
Cart cart = this.cartService.findById(cartId);
if(cart != null && cart.getUserId() != null) {
if (cart != null && cart.getUserId() != null) {
p.setUserId(cart.getUserId());
}