añadidos margenes presupuesto

This commit is contained in:
2025-10-02 00:07:42 +02:00
parent add4e43955
commit 460d2cfc01
17 changed files with 733 additions and 10 deletions

View File

@ -0,0 +1,35 @@
package com.imprimelibros.erp.shared.validation;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.*;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Documented
@Target(TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = NoRangeOverlapValidator.class)
public @interface NoRangeOverlap {
// Campos obligatorios
String min(); // nombre del campo min (Integer/Long/etc.)
String max(); // nombre del campo max
// Campos opcionales
String id() default "id"; // nombre del campo ID (para excluir self en update)
String[] partitionBy() default {}; // ej. {"tipoEncuadernacion","tipoCubierta"}
// Soft delete opcional
String deletedFlag() default ""; // ej. "deleted" (si vacío, no se aplica filtro)
boolean deletedActiveValue() default false; // qué valor significa "activo" (normalmente false)
// Mensajes
String message() default "{validation.range.overlaps}";
String invalidRangeMessage() default "{validation.range.invalid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,127 @@
package com.imprimelibros.erp.shared.validation;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.*;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class NoRangeOverlapValidator implements ConstraintValidator<NoRangeOverlap, Object> {
@PersistenceContext
private EntityManager em;
private String minField;
private String maxField;
private String idField;
private String[] partitionFields;
private String deletedFlag;
private boolean deletedActiveValue;
private String message;
private String invalidRangeMessage;
@Override
public void initialize(NoRangeOverlap ann) {
this.minField = ann.min();
this.maxField = ann.max();
this.idField = ann.id();
this.partitionFields = ann.partitionBy();
this.deletedFlag = ann.deletedFlag();
this.deletedActiveValue = ann.deletedActiveValue();
this.message = ann.message();
this.invalidRangeMessage = ann.invalidRangeMessage();
}
@Override
public boolean isValid(Object bean, ConstraintValidatorContext ctx) {
if (bean == null) return true;
try {
Class<?> entityClass = bean.getClass();
Number min = (Number) read(bean, minField);
Number max = (Number) read(bean, maxField);
Object id = safeRead(bean, idField); // puede ser null en INSERT
if (min == null || max == null) return true;
if (min.longValue() > max.longValue()) {
ctx.disableDefaultConstraintViolation();
ctx.buildConstraintViolationWithTemplate(invalidRangeMessage)
.addPropertyNode(maxField)
.addConstraintViolation();
return false;
}
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
Root<?> root = cq.from(entityClass);
cq.select(cb.count(root));
Predicate pred = cb.conjunction();
if (id != null) {
pred = cb.and(pred, cb.notEqual(root.get(idField), id));
}
for (String pf : partitionFields) {
Object val = read(bean, pf);
pred = cb.and(pred, cb.equal(root.get(pf), val));
}
if (!deletedFlag.isEmpty()) {
pred = cb.and(pred, cb.equal(root.get(deletedFlag), deletedActiveValue));
}
Expression<Number> eMin = root.get(minField);
Expression<Number> eMax = root.get(maxField);
Predicate noOverlap = cb.or(
cb.lt(eMax.as(Long.class), min.longValue()),
cb.gt(eMin.as(Long.class), max.longValue())
);
Predicate overlap = cb.not(noOverlap);
cq.where(cb.and(pred, overlap));
Long count = em.createQuery(cq).getSingleResult();
if (count != null && count > 0) {
ctx.disableDefaultConstraintViolation();
ctx.buildConstraintViolationWithTemplate(message)
.addPropertyNode(minField).addConstraintViolation();
ctx.buildConstraintViolationWithTemplate(message)
.addPropertyNode(maxField).addConstraintViolation();
return false;
}
return true;
} catch (Exception ex) {
// En caso de error inesperado, puedes loguear aquí
return true;
}
}
private Object read(Object bean, String name) throws Exception {
PropertyDescriptor pd = getPropertyDescriptor(bean.getClass(), name);
Method getter = pd.getReadMethod();
return getter.invoke(bean);
}
private Object safeRead(Object bean, String name) {
try {
return read(bean, name);
} catch (Exception ignore) {
return null;
}
}
private PropertyDescriptor getPropertyDescriptor(Class<?> clazz, String name) throws IntrospectionException {
return new PropertyDescriptor(name, clazz);
}
}