mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-12 16:38:48 +00:00
trabajando en guardar presupuestos publicos
This commit is contained in:
19
pom.xml
19
pom.xml
@ -120,6 +120,25 @@
|
||||
<version>8.10.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- GeoIP2 (MaxMind) -->
|
||||
<dependency>
|
||||
<groupId>com.maxmind.geoip2</groupId>
|
||||
<artifactId>geoip2</artifactId>
|
||||
<version>4.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP client (Spring Web) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -6,15 +6,10 @@ import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import com.imprimelibros.erp.common.email.EmailService;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
@ -26,26 +21,17 @@ public class PasswordResetService {
|
||||
private final PasswordResetTokenRepository tokenRepo;
|
||||
private final UserDao userRepo;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JavaMailSender mailSender;
|
||||
private final SpringTemplateEngine templateEngine;
|
||||
private final MessageSource messages;
|
||||
private final EmailService emailService;
|
||||
|
||||
public PasswordResetService(
|
||||
PasswordResetTokenRepository tokenRepo,
|
||||
UserDao userRepo,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JavaMailSender mailSender,
|
||||
SpringTemplateEngine templateEngine,
|
||||
MessageSource messages,
|
||||
EmailService emailService
|
||||
) {
|
||||
this.tokenRepo = tokenRepo;
|
||||
this.userRepo = userRepo;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.mailSender = mailSender;
|
||||
this.templateEngine = templateEngine;
|
||||
this.messages = messages;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.imprimelibros.erp.common.email;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
package com.imprimelibros.erp.common.jpa;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public abstract class AbstractAuditedSoftDeleteEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
// Auditoría temporal
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at")
|
||||
private Instant updatedAt;
|
||||
|
||||
// Auditoría por usuario (nullable si público anónimo)
|
||||
@CreatedBy
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "created_by")
|
||||
private User createdBy;
|
||||
|
||||
@LastModifiedBy
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "updated_by")
|
||||
private User updatedBy;
|
||||
|
||||
// Soft delete
|
||||
@Column(name = "deleted", nullable = false)
|
||||
private boolean deleted = false;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Instant deletedAt;
|
||||
|
||||
// Getters/Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public Instant getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
public User getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(User createdBy) { this.createdBy = createdBy; }
|
||||
|
||||
public User getUpdatedBy() { return updatedBy; }
|
||||
public void setUpdatedBy(User updatedBy) { this.updatedBy = updatedBy; }
|
||||
|
||||
public boolean isDeleted() { return deleted; }
|
||||
public void setDeleted(boolean deleted) { this.deleted = deleted; }
|
||||
|
||||
public Instant getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; }
|
||||
}
|
||||
33
src/main/java/com/imprimelibros/erp/common/web/IpUtils.java
Normal file
33
src/main/java/com/imprimelibros/erp/common/web/IpUtils.java
Normal file
@ -0,0 +1,33 @@
|
||||
package com.imprimelibros.erp.common.web;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class IpUtils {
|
||||
private IpUtils() {}
|
||||
|
||||
private static final List<String> HEADERS = Arrays.asList(
|
||||
"X-Forwarded-For",
|
||||
"X-Real-IP",
|
||||
"CF-Connecting-IP",
|
||||
"True-Client-IP",
|
||||
"X-Client-IP",
|
||||
"X-Forwarded",
|
||||
"Forwarded-For",
|
||||
"Forwarded"
|
||||
);
|
||||
|
||||
public static String getClientIp(HttpServletRequest request) {
|
||||
for (String h : HEADERS) {
|
||||
String v = request.getHeader(h);
|
||||
if (v != null && !v.isBlank() && !"unknown".equalsIgnoreCase(v)) {
|
||||
// X-Forwarded-For puede traer lista: "client, proxy1, proxy2"
|
||||
String first = v.split(",")[0].trim();
|
||||
if (!first.isBlank()) return first;
|
||||
}
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.imprimelibros.erp.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Configuration
|
||||
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
|
||||
public class JpaAuditConfig {
|
||||
|
||||
@Bean
|
||||
public AuditorAware<User> auditorAware() {
|
||||
return () -> {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || !auth.isAuthenticated()) return Optional.empty();
|
||||
|
||||
Object principal = auth.getPrincipal();
|
||||
if (principal instanceof User u) return Optional.of(u);
|
||||
|
||||
if (principal instanceof UserDetails ud) {
|
||||
// Si tu principal es UserDetails y no la entidad User,
|
||||
// aquí podrías cargar User por username si lo necesitas.
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.empty();
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GeoIpService {
|
||||
|
||||
class GeoData {
|
||||
public final String pais;
|
||||
public final String region;
|
||||
public final String ciudad;
|
||||
|
||||
public GeoData(String pais, String region, String ciudad) {
|
||||
this.pais = pais;
|
||||
this.region = region;
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public String getPais() {
|
||||
return pais;
|
||||
}
|
||||
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ip Ip original (no anonimizada) - la implementación debe manejar IPv4/IPv6.
|
||||
* @return GeoData si se pudo resolver; Optional.empty() en caso de error o IP privada.
|
||||
*/
|
||||
Optional<GeoData> lookup(String ip);
|
||||
}
|
||||
@ -9,14 +9,35 @@ import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups;
|
||||
import com.imprimelibros.erp.presupuesto.validation.Tamanio;
|
||||
|
||||
import com.imprimelibros.erp.common.HtmlStripConverter;
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedSoftDeleteEntity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import org.hibernate.annotations.SQLDelete;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
|
||||
@ConsistentTiradas(groups = PresupuestoValidationGroups.DatosGenerales.class)
|
||||
@Tamanio(groups = PresupuestoValidationGroups.DatosGenerales.class)
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Entity
|
||||
@Table(name = "presupuesto")
|
||||
public class Presupuesto implements Cloneable{
|
||||
@Table(name = "presupuesto", indexes = {
|
||||
@Index(name = "idx_presupuesto_origen_estado", columnList = "origen, estado"),
|
||||
@Index(name = "idx_presupuesto_session", columnList = "session_id"),
|
||||
@Index(name = "idx_presupuesto_user", columnList = "user_id"),
|
||||
@Index(name = "idx_presupuesto_deleted", columnList = "deleted"),
|
||||
@Index(name = "idx_presupuesto_geo", columnList = "pais, region, ciudad")
|
||||
})
|
||||
@SQLDelete(sql = "UPDATE presupuesto SET deleted = 1, deleted_at = NOW(3) WHERE id = ?")
|
||||
@SQLRestriction("deleted = 0")
|
||||
public class Presupuesto extends AbstractAuditedSoftDeleteEntity implements Cloneable {
|
||||
|
||||
public enum TipoEncuadernacion {
|
||||
fresado("presupuesto.fresado"),
|
||||
@ -69,6 +90,14 @@ public class Presupuesto implements Cloneable{
|
||||
}
|
||||
}
|
||||
|
||||
public enum Origen {
|
||||
publico, privado
|
||||
}
|
||||
|
||||
public enum Estado {
|
||||
borrador, aceptado, modificado
|
||||
}
|
||||
|
||||
@Override
|
||||
public Presupuesto clone() {
|
||||
try {
|
||||
@ -78,9 +107,89 @@ public class Presupuesto implements Cloneable{
|
||||
}
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
// ====== NUEVOS: Origen/Estado/Usuario/Session/Geo/IP/Totales/JSONs ======
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "origen", nullable = false)
|
||||
private Origen origen = Origen.publico;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "estado", nullable = false)
|
||||
private Estado estado = Estado.borrador;
|
||||
|
||||
// Usuario autenticado (nullable en público)
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
// Continuidad en público
|
||||
@Column(name = "session_id", length = 64)
|
||||
private String sessionId;
|
||||
|
||||
@Column(name = "visitor_id", length = 64)
|
||||
private String visitorId;
|
||||
|
||||
// IP anonimizada / truncada + geolocalización resumida (para estadísticas)
|
||||
@Column(name = "ip_hash", length = 88) // SHA-256 base64 ≈ 44 chars; dejamos margen
|
||||
private String ipHash;
|
||||
|
||||
@Column(name = "ip_trunc", length = 64)
|
||||
private String ipTrunc; // p.ej. "192.168.0.0" o "2a02:xxxx::"
|
||||
|
||||
@Column(name = "pais", length = 64)
|
||||
private String pais;
|
||||
|
||||
@Column(name = "region", length = 128)
|
||||
private String region;
|
||||
|
||||
@Column(name = "ciudad", length = 128)
|
||||
private String ciudad;
|
||||
|
||||
// Totales de la tirada seleccionada y del presupuesto
|
||||
@Column(name = "precio_unitario", precision = 12, scale = 4)
|
||||
private BigDecimal precioUnitario;
|
||||
|
||||
@Column(name = "precio_total_tirada", precision = 12, scale = 2)
|
||||
private BigDecimal precioTotalTirada;
|
||||
|
||||
@Column(name = "servicios_total", precision = 12, scale = 2)
|
||||
private BigDecimal serviciosTotal;
|
||||
|
||||
@Column(name = "base_imponible", precision = 12, scale = 2)
|
||||
private BigDecimal baseImponible;
|
||||
|
||||
@Column(name = "iva_tipo", precision = 5, scale = 2)
|
||||
private BigDecimal ivaTipo;
|
||||
|
||||
@Column(name = "iva_importe", precision = 12, scale = 2)
|
||||
private BigDecimal ivaImporte;
|
||||
|
||||
@Column(name = "total_con_iva", precision = 12, scale = 2)
|
||||
private BigDecimal totalConIva;
|
||||
|
||||
// JSONs de apoyo (todas las tiradas, servicios, bloques de
|
||||
// maquetación/marcapáginas y snapshot)
|
||||
@Lob
|
||||
@Column(name = "precios_por_tirada_json", columnDefinition = "json")
|
||||
private String preciosPorTiradaJson; // [{tirada, precio_unitario, precio_total_tirada}, ...]
|
||||
|
||||
@Lob
|
||||
@Column(name = "servicios_json", columnDefinition = "json")
|
||||
private String serviciosJson;
|
||||
|
||||
@Lob
|
||||
@Column(name = "datos_maquetacion_json", columnDefinition = "json")
|
||||
private String datosMaquetacionJson;
|
||||
|
||||
@Lob
|
||||
@Column(name = "datos_marcapaginas_json", columnDefinition = "json")
|
||||
private String datosMarcapaginasJson;
|
||||
|
||||
@Lob
|
||||
@Column(name = "pricing_snapshot", columnDefinition = "json")
|
||||
private String pricingSnapshotJson;
|
||||
|
||||
// ====== TUS CAMPOS ORIGINALES ======
|
||||
|
||||
@NotNull(message = "{presupuesto.errores.tipo-encuadernacion}", groups = PresupuestoValidationGroups.DatosGenerales.class)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ -192,39 +301,267 @@ public class Presupuesto implements Cloneable{
|
||||
|
||||
@NotNull(message = "{presupuesto.errores.acabado-cubierta}", groups = PresupuestoValidationGroups.Cubierta.class)
|
||||
@Column(name = "acabado")
|
||||
private Integer acabado = 1;
|
||||
private Integer acabado = 1;
|
||||
|
||||
@Column(name = "sobrecubierta")
|
||||
private Boolean sobrecubierta = false;
|
||||
|
||||
@Column(name = "papel_sobrecubierta_id")
|
||||
private Integer papelSobrecubiertaId = 2;
|
||||
|
||||
@Column(name = "gramaje_sobrecubierta")
|
||||
private Integer gramajeSobrecubierta = 170;
|
||||
|
||||
@Column(name = "tamanio_solapas_sobrecubierta")
|
||||
private Integer tamanioSolapasSobrecubierta = 80;
|
||||
|
||||
@Column(name = "acabado_sobrecubierta")
|
||||
private Integer acabadoSobrecubierta = 0; // 0: sin acabado,
|
||||
private Integer acabadoSobrecubierta = 0;
|
||||
|
||||
@Column(name = "faja")
|
||||
private Boolean faja = false;
|
||||
|
||||
@Column(name = "papel_faja_id")
|
||||
private Integer papelFajaId = 2;
|
||||
|
||||
@Column(name = "gramaje_faja")
|
||||
private Integer gramajeFaja = 170;
|
||||
|
||||
@Column(name = "tamanio_solapas_faja")
|
||||
private Integer tamanioSolapasFaja = 80;
|
||||
|
||||
@Column(name = "acabado_faja")
|
||||
private Integer acabadoFaja = 0; // 0: sin acabado
|
||||
private Integer acabadoFaja = 0;
|
||||
|
||||
@Column(name = "alto_faja")
|
||||
private Integer altoFaja = 0;
|
||||
|
||||
@Column(name = "presupuesto_maquetacion")
|
||||
private Boolean presupuestoMaquetacion = false;
|
||||
@Column(name = "presupuesto_maquetacion_data")
|
||||
private String presupuestoMaquetacionData;
|
||||
// ====== AUDIT ======
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@CreatedBy
|
||||
@Column(name = "created_by", updatable = false) // BIGINT o VARCHAR: ajusta el tipo del campo
|
||||
private Long createdBy; // o String si tu columna es texto
|
||||
|
||||
|
||||
// Getters y Setters
|
||||
|
||||
|
||||
// ====== MÉTODOS AUX ======
|
||||
|
||||
public String resumenPresupuesto() {
|
||||
return String.format("%s - %s - %dx%d mm - %d Páginas (N:%d C:%d) - Tira:%d",
|
||||
this.titulo,
|
||||
this.tipoEncuadernacion,
|
||||
this.ancho,
|
||||
this.alto,
|
||||
(this.paginasNegro != null ? this.paginasNegro : 0)
|
||||
+ (this.paginasColorTotal != null ? this.paginasColorTotal : 0),
|
||||
this.paginasNegro != null ? this.paginasNegro : 0,
|
||||
this.paginasColorTotal != null ? this.paginasColorTotal : 0,
|
||||
this.selectedTirada != null ? this.selectedTirada : 0);
|
||||
}
|
||||
|
||||
public Integer[] getTiradas() {
|
||||
return new Integer[] { tirada1, tirada2, tirada3, tirada4 };
|
||||
}
|
||||
|
||||
// ====== GETTERS/SETTERS (incluye nuevos y existentes) ======
|
||||
|
||||
public Origen getOrigen() {
|
||||
return origen;
|
||||
}
|
||||
|
||||
public void setOrigen(Origen origen) {
|
||||
this.origen = origen;
|
||||
}
|
||||
|
||||
public Estado getEstado() {
|
||||
return estado;
|
||||
}
|
||||
|
||||
public void setEstado(Estado estado) {
|
||||
this.estado = estado;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getVisitorId() {
|
||||
return visitorId;
|
||||
}
|
||||
|
||||
public void setVisitorId(String visitorId) {
|
||||
this.visitorId = visitorId;
|
||||
}
|
||||
|
||||
public String getIpHash() {
|
||||
return ipHash;
|
||||
}
|
||||
|
||||
public void setIpHash(String ipHash) {
|
||||
this.ipHash = ipHash;
|
||||
}
|
||||
|
||||
public String getIpTrunc() {
|
||||
return ipTrunc;
|
||||
}
|
||||
|
||||
public void setIpTrunc(String ipTrunc) {
|
||||
this.ipTrunc = ipTrunc;
|
||||
}
|
||||
|
||||
public String getPais() {
|
||||
return pais;
|
||||
}
|
||||
|
||||
public void setPais(String pais) {
|
||||
this.pais = pais;
|
||||
}
|
||||
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public void setRegion(String region) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
public void setCiudad(String ciudad) {
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public BigDecimal getPrecioUnitario() {
|
||||
return precioUnitario;
|
||||
}
|
||||
|
||||
public void setPrecioUnitario(BigDecimal precioUnitario) {
|
||||
this.precioUnitario = precioUnitario;
|
||||
}
|
||||
|
||||
public BigDecimal getPrecioTotalTirada() {
|
||||
return precioTotalTirada;
|
||||
}
|
||||
|
||||
public void setPrecioTotalTirada(BigDecimal precioTotalTirada) {
|
||||
this.precioTotalTirada = precioTotalTirada;
|
||||
}
|
||||
|
||||
public BigDecimal getServiciosTotal() {
|
||||
return serviciosTotal;
|
||||
}
|
||||
|
||||
public void setServiciosTotal(BigDecimal serviciosTotal) {
|
||||
this.serviciosTotal = serviciosTotal;
|
||||
}
|
||||
|
||||
public BigDecimal getBaseImponible() {
|
||||
return baseImponible;
|
||||
}
|
||||
|
||||
public void setBaseImponible(BigDecimal baseImponible) {
|
||||
this.baseImponible = baseImponible;
|
||||
}
|
||||
|
||||
public BigDecimal getIvaTipo() {
|
||||
return ivaTipo;
|
||||
}
|
||||
|
||||
public void setIvaTipo(BigDecimal ivaTipo) {
|
||||
this.ivaTipo = ivaTipo;
|
||||
}
|
||||
|
||||
public BigDecimal getIvaImporte() {
|
||||
return ivaImporte;
|
||||
}
|
||||
|
||||
public void setIvaImporte(BigDecimal ivaImporte) {
|
||||
this.ivaImporte = ivaImporte;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalConIva() {
|
||||
return totalConIva;
|
||||
}
|
||||
|
||||
public void setTotalConIva(BigDecimal totalConIva) {
|
||||
this.totalConIva = totalConIva;
|
||||
}
|
||||
|
||||
public String getPreciosPorTiradaJson() {
|
||||
return preciosPorTiradaJson;
|
||||
}
|
||||
|
||||
public void setPreciosPorTiradaJson(String preciosPorTiradaJson) {
|
||||
this.preciosPorTiradaJson = preciosPorTiradaJson;
|
||||
}
|
||||
|
||||
public String getServiciosJson() {
|
||||
return serviciosJson;
|
||||
}
|
||||
|
||||
public void setServiciosJson(String serviciosJson) {
|
||||
this.serviciosJson = serviciosJson;
|
||||
}
|
||||
|
||||
public String getDatosMaquetacionJson() {
|
||||
return datosMaquetacionJson;
|
||||
}
|
||||
|
||||
public void setDatosMaquetacionJson(String datosMaquetacionJson) {
|
||||
this.datosMaquetacionJson = datosMaquetacionJson;
|
||||
}
|
||||
|
||||
public String getDatosMarcapaginasJson() {
|
||||
return datosMarcapaginasJson;
|
||||
}
|
||||
|
||||
public void setDatosMarcapaginasJson(String datosMarcapaginasJson) {
|
||||
this.datosMarcapaginasJson = datosMarcapaginasJson;
|
||||
}
|
||||
|
||||
public String getPricingSnapshotJson() {
|
||||
return pricingSnapshotJson;
|
||||
}
|
||||
|
||||
public void setPricingSnapshotJson(String pricingSnapshotJson) {
|
||||
this.pricingSnapshotJson = pricingSnapshotJson;
|
||||
}
|
||||
|
||||
public TipoEncuadernacion getTipoEncuadernacion() {
|
||||
return tipoEncuadernacion;
|
||||
}
|
||||
|
||||
public void setTipoEncuadernacion(TipoEncuadernacion tipoEncuadernacion) {
|
||||
this.tipoEncuadernacion = tipoEncuadernacion;
|
||||
}
|
||||
|
||||
public String getTitulo() {
|
||||
return titulo;
|
||||
}
|
||||
|
||||
public void setTitulo(String titulo) {
|
||||
this.titulo = titulo;
|
||||
}
|
||||
|
||||
public String getAutor() {
|
||||
return autor;
|
||||
}
|
||||
@ -269,14 +606,18 @@ public class Presupuesto implements Cloneable{
|
||||
return tirada4;
|
||||
}
|
||||
|
||||
public Integer[] getTiradas() {
|
||||
return new Integer[] { tirada1, tirada2, tirada3, tirada4 };
|
||||
}
|
||||
|
||||
public void setTirada4(Integer tirada4) {
|
||||
this.tirada4 = tirada4;
|
||||
}
|
||||
|
||||
public Integer getSelectedTirada() {
|
||||
return selectedTirada;
|
||||
}
|
||||
|
||||
public void setSelectedTirada(Integer selectedTirada) {
|
||||
this.selectedTirada = selectedTirada;
|
||||
}
|
||||
|
||||
public Integer getAncho() {
|
||||
return ancho;
|
||||
}
|
||||
@ -301,22 +642,6 @@ public class Presupuesto implements Cloneable{
|
||||
this.formatoPersonalizado = formatoPersonalizado;
|
||||
}
|
||||
|
||||
public String getTitulo() {
|
||||
return titulo;
|
||||
}
|
||||
|
||||
public void setTitulo(String titulo) {
|
||||
this.titulo = titulo;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Integer getPaginasNegro() {
|
||||
return paginasNegro;
|
||||
}
|
||||
@ -333,14 +658,6 @@ public class Presupuesto implements Cloneable{
|
||||
this.paginasColor = paginasColor;
|
||||
}
|
||||
|
||||
public TipoEncuadernacion getTipoEncuadernacion() {
|
||||
return tipoEncuadernacion;
|
||||
}
|
||||
|
||||
public void setTipoEncuadernacion(TipoEncuadernacion tipoEncuadernacion) {
|
||||
this.tipoEncuadernacion = tipoEncuadernacion;
|
||||
}
|
||||
|
||||
public String getPosicionPaginasColor() {
|
||||
return posicionPaginasColor;
|
||||
}
|
||||
@ -557,36 +874,5 @@ public class Presupuesto implements Cloneable{
|
||||
this.altoFaja = altoFaja;
|
||||
}
|
||||
|
||||
public Integer getSelectedTirada() {
|
||||
return selectedTirada;
|
||||
}
|
||||
|
||||
public void setSelectedTirada(Integer selectedTirada) {
|
||||
this.selectedTirada = selectedTirada;
|
||||
}
|
||||
|
||||
public Boolean getPresupuestoMaquetacion() {
|
||||
return presupuestoMaquetacion;
|
||||
}
|
||||
public void setPresupuestoMaquetacion(Boolean presupuestoMaquetacion) {
|
||||
this.presupuestoMaquetacion = presupuestoMaquetacion;
|
||||
}
|
||||
public String getPresupuestoMaquetacionData() {
|
||||
return presupuestoMaquetacionData;
|
||||
}
|
||||
public void setPresupuestoMaquetacionData(String presupuestoMaquetacionData) {
|
||||
this.presupuestoMaquetacionData = presupuestoMaquetacionData;
|
||||
}
|
||||
|
||||
public String resumenPresupuesto() {
|
||||
return String.format("%s - %s - %dx%d mm - %d Páginas (N:%d C:%d) - Tira:%d",
|
||||
this.titulo,
|
||||
this.tipoEncuadernacion,
|
||||
this.ancho,
|
||||
this.alto,
|
||||
this.paginasNegro + this.paginasColorTotal,
|
||||
this.paginasNegro,
|
||||
this.paginasColorTotal,
|
||||
this.selectedTirada != null ? this.selectedTirada : 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ import com.imprimelibros.erp.presupuesto.classes.PresupuestoMaquetacion;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoMarcapaginas;
|
||||
import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
@Controller
|
||||
@ -388,13 +389,22 @@ public class PresupuestoController {
|
||||
|
||||
// Se hace un post para no tener problemas con la longitud de la URL
|
||||
@PostMapping("/public/resumen")
|
||||
public ResponseEntity<?> getResumen(@RequestBody Map<String, Object> body, Locale locale) {
|
||||
public ResponseEntity<?> getResumen(
|
||||
@RequestBody Map<String, Object> body,
|
||||
Locale locale,
|
||||
HttpServletRequest request) {
|
||||
|
||||
Presupuesto p = objectMapper.convertValue(body.get("presupuesto"), Presupuesto.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> serviciosList = (List<Map<String, Object>>) body.getOrDefault("servicios", List.of());
|
||||
|
||||
return ResponseEntity.ok(presupuestoService.getResumen(p, serviciosList, locale));
|
||||
String sessionId = request.getSession(true).getId();
|
||||
String ip = request.getRemoteAddr();
|
||||
|
||||
var resumen = presupuestoService.getResumenPublico(p, serviciosList, locale, sessionId, ip);
|
||||
|
||||
return ResponseEntity.ok(resumen);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Repository
|
||||
public interface PresupuestoRepository extends JpaRepository<Presupuesto, Long> {
|
||||
|
||||
Optional<Presupuesto> findFirstBySessionIdAndOrigenAndEstadoInOrderByUpdatedAtDesc(
|
||||
String sessionId,
|
||||
Presupuesto.Origen origen,
|
||||
Collection<Presupuesto.Estado> estados);
|
||||
|
||||
List<Presupuesto> findByOrigenAndEstado(Presupuesto.Origen origen, Presupuesto.Estado estado);
|
||||
|
||||
// Incluye borrados (ignora @Where) usando native
|
||||
@Query(value = "SELECT * FROM presupuesto WHERE id = :id", nativeQuery = true)
|
||||
Optional<Presupuesto> findAnyById(@Param("id") Long id);
|
||||
|
||||
Optional<Presupuesto> findTopBySessionIdAndEstadoOrderByCreatedAtDesc(String sessionId, Presupuesto.Estado estado);
|
||||
|
||||
}
|
||||
@ -13,6 +13,7 @@ import java.text.NumberFormat;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
@ -53,18 +54,23 @@ public class PresupuestoService {
|
||||
@Autowired
|
||||
protected MarcapaginasRepository marcapaginasRepository;
|
||||
|
||||
@Autowired
|
||||
protected PresupuestoRepository presupuestoRepository;
|
||||
|
||||
private final PresupuestadorItems presupuestadorItems;
|
||||
private final PresupuestoFormatter presupuestoFormatter;
|
||||
private final skApiClient apiClient;
|
||||
private final GeoIpService geoIpService;
|
||||
|
||||
public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter, skApiClient apiClient) {
|
||||
public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter,
|
||||
skApiClient apiClient, GeoIpService geoIpService) {
|
||||
this.presupuestadorItems = presupuestadorItems;
|
||||
this.presupuestoFormatter = presupuestoFormatter;
|
||||
this.apiClient = apiClient;
|
||||
this.geoIpService = geoIpService;
|
||||
}
|
||||
|
||||
public boolean validateDatosGenerales(int[] tiradas) {
|
||||
|
||||
for (int tirada : tiradas) {
|
||||
if (tirada <= 0) {
|
||||
return false; // Invalid tirada found
|
||||
@ -74,7 +80,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Boolean isPOD(Presupuesto presupuesto) {
|
||||
|
||||
int pod_value = variableService.getValorEntero("POD");
|
||||
return (presupuesto.getTirada1() != null && presupuesto.getTirada1() <= pod_value) ||
|
||||
(presupuesto.getTirada2() != null && presupuesto.getTirada2() <= pod_value) ||
|
||||
@ -83,7 +88,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerOpcionesColor(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
List<ImagenPresupuesto> opciones = new ArrayList<>();
|
||||
|
||||
if (presupuesto.getPaginasColor() > 0) {
|
||||
@ -121,7 +125,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerOpcionesPapelInterior(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
List<ImagenPresupuesto> opciones = new ArrayList<>();
|
||||
|
||||
opciones.add(this.presupuestadorItems.getPapelOffsetBlanco(locale));
|
||||
@ -144,7 +147,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
boolean yaSeleccionado = opciones.stream().anyMatch(ImagenPresupuesto::isSelected);
|
||||
|
||||
if (!yaSeleccionado && !opciones.isEmpty()) {
|
||||
ImagenPresupuesto primeraOpcion = opciones.get(0);
|
||||
primeraOpcion.setSelected(true);
|
||||
@ -153,12 +155,10 @@ public class PresupuestoService {
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("opciones_papel_interior", opciones);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerOpcionesGramajeInterior(Presupuesto presupuesto) {
|
||||
|
||||
List<String> gramajes = new ArrayList<>();
|
||||
|
||||
final int BLANCO_OFFSET_ID = 3;
|
||||
@ -168,7 +168,6 @@ public class PresupuestoService {
|
||||
final int ESTUCADO_MATE_ID = 2;
|
||||
|
||||
if (presupuesto.getPapelInteriorId() != null && presupuesto.getPapelInteriorId() == BLANCO_OFFSET_ID) {
|
||||
|
||||
gramajes.add("80");
|
||||
gramajes.add("90");
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negrohq ||
|
||||
@ -180,32 +179,25 @@ public class PresupuestoService {
|
||||
}
|
||||
} else if (presupuesto.getPapelInteriorId() != null
|
||||
&& presupuesto.getPapelInteriorId() == BLANCO_OFFSET_VOLUMEN_ID) {
|
||||
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro ||
|
||||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
|
||||
gramajes.add("80");
|
||||
}
|
||||
|
||||
} else if (presupuesto.getPapelInteriorId() != null && presupuesto.getPapelInteriorId() == AHUESADO_OFFSET_ID) {
|
||||
|
||||
gramajes.add("80");
|
||||
gramajes.add("90");
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negrohq ||
|
||||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.colorhq) {
|
||||
gramajes.add("100");
|
||||
}
|
||||
|
||||
} else if (presupuesto.getPapelInteriorId() != null
|
||||
&& presupuesto.getPapelInteriorId() == AHUESADO_OFFSET_VOLUMEN_ID) {
|
||||
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro ||
|
||||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
|
||||
gramajes.add("70");
|
||||
gramajes.add("80");
|
||||
}
|
||||
|
||||
} else if (presupuesto.getPapelInteriorId() != null && presupuesto.getPapelInteriorId() == ESTUCADO_MATE_ID) {
|
||||
|
||||
if (presupuesto.getTipoImpresion() != Presupuesto.TipoImpresion.color) {
|
||||
gramajes.add("90");
|
||||
}
|
||||
@ -234,7 +226,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerOpcionesPapelCubierta(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
List<ImagenPresupuesto> opciones = new ArrayList<>();
|
||||
|
||||
if (presupuesto.getTipoCubierta() == Presupuesto.TipoCubierta.tapaBlanda) {
|
||||
@ -255,7 +246,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerOpcionesGramajeCubierta(Presupuesto presupuesto) {
|
||||
|
||||
List<String> gramajes = new ArrayList<>();
|
||||
|
||||
final int CARTULINA_GRAFICA_ID = 5;
|
||||
@ -268,7 +258,6 @@ public class PresupuestoService {
|
||||
gramajes.add("350");
|
||||
} else if (presupuesto.getPapelCubiertaId() != null && presupuesto.getPapelCubiertaId() == ESTUCADO_MATE_ID) {
|
||||
if (presupuesto.getTipoCubierta() == Presupuesto.TipoCubierta.tapaBlanda) {
|
||||
|
||||
gramajes.add("250");
|
||||
gramajes.add("300");
|
||||
gramajes.add("350");
|
||||
@ -283,7 +272,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> toSkApiRequest(Presupuesto presupuesto) {
|
||||
|
||||
final int SK_CLIENTE_ID = 1284;
|
||||
final int SK_PAGINAS_CUADERNILLO = 32;
|
||||
|
||||
@ -343,7 +331,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Integer getTipoImpresionId(Presupuesto presupuesto) {
|
||||
|
||||
if (presupuesto.getTipoEncuadernacion() == Presupuesto.TipoEncuadernacion.fresado) {
|
||||
if (presupuesto.getTipoCubierta() == Presupuesto.TipoCubierta.tapaDura ||
|
||||
presupuesto.getTipoCubierta() == Presupuesto.TipoCubierta.tapaDuraLomoRedondo) {
|
||||
@ -437,12 +424,6 @@ public class PresupuestoService {
|
||||
return resultado;
|
||||
}
|
||||
|
||||
public Map<String, Object> aplicarMargenTiradas(Map<String, Object> data) {
|
||||
|
||||
// implementar margenes
|
||||
return (Map<String, Object>) data;
|
||||
}
|
||||
|
||||
public String obtenerPrecioRetractilado(Presupuesto presupuesto, Locale locale) {
|
||||
Integer[] tiradas = presupuesto.getTiradas();
|
||||
Integer tirada_min = Arrays.stream(tiradas)
|
||||
@ -460,7 +441,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerServiciosExtras(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
List<Object> opciones = new ArrayList<>();
|
||||
|
||||
Double price_prototipo = this.obtenerPrototipo(presupuesto);
|
||||
@ -532,8 +512,8 @@ public class PresupuestoService {
|
||||
put("price", messageSource.getMessage("presupuesto.consultar-soporte", null, locale));
|
||||
put("priceUnit", "");
|
||||
} else {
|
||||
put("price", NumberFormat.getNumberInstance(locale)
|
||||
.format(Math.round(price_prototipo * 100.0) / 100.0));
|
||||
put("price",
|
||||
NumberFormat.getNumberInstance(locale).format(Math.round(price_prototipo * 100.0) / 100.0));
|
||||
put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale));
|
||||
}
|
||||
}
|
||||
@ -561,10 +541,8 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
private Double obtenerPrototipo(Presupuesto presupuesto) {
|
||||
|
||||
// Obtenemos el precio de 1 unidad para el ejemplar de prueba
|
||||
HashMap<String, Object> price = new HashMap<>();
|
||||
// make a copy of "presupuesto" to avoid modifying the original object
|
||||
Presupuesto presupuestoTemp = presupuesto.clone();
|
||||
presupuestoTemp.setTirada1(1);
|
||||
presupuestoTemp.setTirada2(null);
|
||||
@ -575,7 +553,8 @@ public class PresupuestoService {
|
||||
} else if (presupuestoTemp.getTipoImpresion() == Presupuesto.TipoImpresion.negro) {
|
||||
presupuestoTemp.setTipoImpresion(Presupuesto.TipoImpresion.negrohq);
|
||||
}
|
||||
String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuestoTemp), presupuestoTemp.getTipoEncuadernacion(), presupuestoTemp.getTipoCubierta());
|
||||
String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuestoTemp),
|
||||
presupuestoTemp.getTipoEncuadernacion(), presupuestoTemp.getTipoCubierta());
|
||||
Double price_prototipo = 0.0;
|
||||
try {
|
||||
price = new ObjectMapper().readValue(priceStr, new TypeReference<>() {
|
||||
@ -584,7 +563,6 @@ public class PresupuestoService {
|
||||
if (price_prototipo < 25) {
|
||||
price_prototipo = 25.0;
|
||||
}
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (Exception exception) {
|
||||
}
|
||||
@ -595,7 +573,6 @@ public class PresupuestoService {
|
||||
try {
|
||||
List<MaquetacionPrecios> lista = maquetacionPreciosRepository.findAll();
|
||||
|
||||
// helper para obtener un precio por clave
|
||||
java.util.function.Function<String, Double> price = key -> lista.stream()
|
||||
.filter(p -> key.equals(p.getKey()))
|
||||
.map(MaquetacionPrecios::getValue)
|
||||
@ -604,13 +581,10 @@ public class PresupuestoService {
|
||||
|
||||
BigDecimal precio = BigDecimal.ZERO;
|
||||
|
||||
// millar_maquetacion * (numCaracteres / 1000.0)
|
||||
BigDecimal millares = BigDecimal.valueOf(presupuestoMaquetacion.getNumCaracteres()).divide(
|
||||
BigDecimal.valueOf(1000), 6,
|
||||
RoundingMode.HALF_UP);
|
||||
BigDecimal.valueOf(1000), 6, RoundingMode.HALF_UP);
|
||||
precio = precio.add(millares.multiply(BigDecimal.valueOf(price.apply("millar_maquetacion"))));
|
||||
|
||||
// Numero de paginas estimado
|
||||
int numPaginas = 0;
|
||||
Integer matricesPorPagina = maquetacionMatricesRepository.findMatrices(
|
||||
presupuestoMaquetacion.getFormato(),
|
||||
@ -618,7 +592,7 @@ public class PresupuestoService {
|
||||
if (matricesPorPagina != null && matricesPorPagina > 0) {
|
||||
numPaginas = presupuestoMaquetacion.getNumCaracteres() / matricesPorPagina;
|
||||
}
|
||||
// Precio por pagina estimado
|
||||
|
||||
BigDecimal precioRedondeado = precio.setScale(2, RoundingMode.HALF_UP);
|
||||
double precioPaginaEstimado = 0.0;
|
||||
if (numPaginas > 0) {
|
||||
@ -627,7 +601,6 @@ public class PresupuestoService {
|
||||
.doubleValue();
|
||||
}
|
||||
|
||||
// tabla, columna, foto
|
||||
precio = precio
|
||||
.add(BigDecimal.valueOf(presupuestoMaquetacion.getNumTablas())
|
||||
.multiply(BigDecimal.valueOf(price.apply("tabla"))));
|
||||
@ -652,7 +625,6 @@ public class PresupuestoService {
|
||||
precio = precio.add(BigDecimal.valueOf(price.apply("epub")));
|
||||
}
|
||||
|
||||
// redondeo final
|
||||
precioRedondeado = precio.setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
HashMap<String, Object> out = new HashMap<>();
|
||||
@ -684,9 +656,7 @@ public class PresupuestoService {
|
||||
|
||||
public HashMap<String, Object> getPrecioMarcapaginas(PresupuestoMarcapaginas presupuestoMarcapaginas,
|
||||
Locale locale) {
|
||||
|
||||
try {
|
||||
|
||||
List<Marcapaginas> m = marcapaginasRepository.findPrecios(presupuestoMarcapaginas);
|
||||
if (m.isEmpty() || m.get(0) == null) {
|
||||
HashMap<String, Object> out = new HashMap<>();
|
||||
@ -774,47 +744,52 @@ public class PresupuestoService {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula el resumen (SIN persistir cambios de estado).
|
||||
* Mantiene firma para no romper llamadas existentes.
|
||||
*/
|
||||
public Map<String, Object> getResumen(Presupuesto presupuesto, List<Map<String, Object>> servicios, Locale locale) {
|
||||
|
||||
Map<String, Object> resumen = new HashMap<>();
|
||||
resumen.put("titulo", presupuesto.getTitulo());
|
||||
|
||||
Presupuesto pressupuestoTemp = presupuesto.clone();
|
||||
|
||||
resumen.put("imagen", "/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + ".png");
|
||||
resumen.put("imagen_alt", messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, locale));
|
||||
resumen.put("imagen",
|
||||
"/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + ".png");
|
||||
resumen.put("imagen_alt",
|
||||
messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, locale));
|
||||
|
||||
boolean hayDepositoLegal = servicios != null && servicios.stream()
|
||||
.map(m -> java.util.Objects.toString(m.get("id"), "")) // null-safe -> String
|
||||
.map(m -> java.util.Objects.toString(m.get("id"), ""))
|
||||
.map(String::trim)
|
||||
.anyMatch("deposito-legal"::equals);
|
||||
|
||||
if(hayDepositoLegal){
|
||||
pressupuestoTemp.setSelectedTirada(presupuesto.getSelectedTirada()+4);
|
||||
if (hayDepositoLegal) {
|
||||
pressupuestoTemp.setSelectedTirada(
|
||||
presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() + 4 : 4);
|
||||
}
|
||||
|
||||
|
||||
HashMap<String, Object> precios = this.calcularPresupuesto(pressupuestoTemp, locale);
|
||||
if(precios.containsKey("error")){
|
||||
if (precios.containsKey("error")) {
|
||||
resumen.put("error", precios.get("error"));
|
||||
return resumen;
|
||||
}
|
||||
|
||||
|
||||
HashMap<String, Object> linea = new HashMap<>();
|
||||
Double precio_unitario = 0.0;
|
||||
Double precio_total = 0.0;
|
||||
Integer counter = 0;
|
||||
linea.put("descripcion", presupuestoFormatter.resumen(presupuesto, servicios, locale));
|
||||
linea.put("cantidad", presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0);
|
||||
precio_unitario = ((List<Double>) ((Map<String, Object>) precios.get("data")).get("precios"))
|
||||
.get(0);
|
||||
precio_total = precio_unitario * presupuesto.getSelectedTirada();
|
||||
precio_unitario = ((List<Double>) ((Map<String, Object>) precios.get("data")).get("precios")).get(0);
|
||||
precio_total = precio_unitario
|
||||
* (presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0);
|
||||
linea.put("precio_unitario", precio_unitario);
|
||||
linea.put("precio_total", BigDecimal.valueOf(precio_total).setScale(2, RoundingMode.HALF_UP));
|
||||
resumen.put("linea" + counter, linea);
|
||||
counter++;
|
||||
|
||||
if(hayDepositoLegal) {
|
||||
|
||||
if (hayDepositoLegal) {
|
||||
linea = new HashMap<>();
|
||||
linea.put("descripcion", messageSource.getMessage("presupuesto.resumen-deposito-legal", null, locale));
|
||||
linea.put("cantidad", 4);
|
||||
@ -825,15 +800,15 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
List<Map<String, Object>> serviciosExtras = new ArrayList<>();
|
||||
|
||||
if(servicios != null){
|
||||
if (servicios != null) {
|
||||
for (Map<String, Object> servicio : servicios) {
|
||||
HashMap<String, Object> servicioData = new HashMap<>();
|
||||
servicioData.put("id", servicio.get("id"));
|
||||
servicioData.put("descripcion", servicio.get("label"));
|
||||
servicioData.put("precio", servicio.get("id").equals("marcapaginas") ?
|
||||
Double.parseDouble(servicio.get("price").toString())/Double.parseDouble(servicio.get("units").toString()) :
|
||||
servicio.get("price"));
|
||||
servicioData.put("precio", servicio.get("id").equals("marcapaginas")
|
||||
? Double.parseDouble(servicio.get("price").toString())
|
||||
/ Double.parseDouble(servicio.get("units").toString())
|
||||
: servicio.get("price"));
|
||||
servicioData.put("unidades", servicio.get("units"));
|
||||
serviciosExtras.add(servicioData);
|
||||
}
|
||||
@ -843,10 +818,217 @@ public class PresupuestoService {
|
||||
return resumen;
|
||||
}
|
||||
|
||||
public HashMap<String, Object> calcularPresupuesto(Presupuesto presupuesto, Locale locale) {
|
||||
/**
|
||||
* PÚBLICO: calcula el resumen y GUARDA el presupuesto completo como BORRADOR.
|
||||
* Se invoca al entrar en la pestaña "Resumen" del presupuestador público.
|
||||
*/
|
||||
// PresupuestoService.java
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> getResumenPublico(
|
||||
Presupuesto presupuesto,
|
||||
List<Map<String, Object>> servicios,
|
||||
Locale locale,
|
||||
String sessionId,
|
||||
String ip) {
|
||||
|
||||
// 1) Calcula el resumen (como ya haces)
|
||||
Map<String, Object> resumen = getResumen(presupuesto, servicios, locale);
|
||||
if (resumen.containsKey("error"))
|
||||
return resumen;
|
||||
|
||||
// 2) Totales a partir del resumen
|
||||
// - precio_unitario: primer precio devuelto por la API
|
||||
// - cantidad: selected_tirada
|
||||
// - precio_total_tirada
|
||||
// - servicios_total (si hay)
|
||||
double precioUnit = 0.0;
|
||||
int cantidad = presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0;
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Double> precios = (List<Double>) ((Map<String, Object>) resumen.getOrDefault("precios", Map.of()))
|
||||
.getOrDefault("precios", List.of());
|
||||
if (precios.isEmpty()) {
|
||||
// si no venía en "resumen", recalcúlalo directamente
|
||||
var preciosCalc = this.calcularPresupuesto(presupuesto, locale);
|
||||
precios = (List<Double>) ((Map<String, Object>) preciosCalc.get("data")).get("precios");
|
||||
}
|
||||
precioUnit = precios.get(0);
|
||||
// guarda el snapshot completo de precios para auditoría
|
||||
presupuesto.setPreciosPorTiradaJson(new ObjectMapper().writeValueAsString(precios));
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
BigDecimal precioTotalTirada = BigDecimal.valueOf(precioUnit)
|
||||
.multiply(BigDecimal.valueOf(cantidad))
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
// servicios_total
|
||||
BigDecimal serviciosTotal = BigDecimal.ZERO;
|
||||
if (servicios != null) {
|
||||
for (Map<String, Object> s : servicios) {
|
||||
try {
|
||||
double unidades = Double.parseDouble(String.valueOf(s.getOrDefault("units", 0)));
|
||||
double precio = Double.parseDouble(String.valueOf(
|
||||
s.get("id").equals("marcapaginas")
|
||||
? (Double.parseDouble(String.valueOf(s.get("price"))) / unidades) // unidad
|
||||
: s.getOrDefault("price", 0)));
|
||||
serviciosTotal = serviciosTotal.add(
|
||||
BigDecimal.valueOf(precio).multiply(BigDecimal.valueOf(unidades)));
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
presupuesto.setServiciosJson(new ObjectMapper().writeValueAsString(servicios));
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
// base imponible, IVA y total (si tienes IVA configurable, úsalo; si no, 0)
|
||||
BigDecimal baseImponible = precioTotalTirada.add(serviciosTotal);
|
||||
BigDecimal ivaTipo = BigDecimal.ZERO;
|
||||
try {
|
||||
double iva = 4.0; // 0..100
|
||||
ivaTipo = BigDecimal.valueOf(iva);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
BigDecimal ivaImporte = baseImponible.multiply(ivaTipo).divide(BigDecimal.valueOf(100), 2,
|
||||
RoundingMode.HALF_UP);
|
||||
BigDecimal totalConIva = baseImponible.add(ivaImporte);
|
||||
|
||||
// 3) Enriquecer el Presupuesto a persistir
|
||||
presupuesto.setEstado(Presupuesto.Estado.borrador);
|
||||
presupuesto.setOrigen(Presupuesto.Origen.publico);
|
||||
presupuesto.setSessionId(sessionId);
|
||||
|
||||
// IP: guarda hash y trunc (si tienes campos). Si no, guarda tal cual en
|
||||
// ip_trunc/ip_hash según tu modelo.
|
||||
presupuesto.setIpTrunc(ip);
|
||||
presupuesto.setIpHash(Integer.toHexString(ip.hashCode()));
|
||||
|
||||
// ubicación (si tienes un servicio GeoIP disponible; si no, omite estas tres
|
||||
// líneas)
|
||||
try {
|
||||
GeoIpService.GeoData geo = geoIpService.lookup(ip).orElse(null);
|
||||
presupuesto.setPais(geo.getPais());
|
||||
presupuesto.setRegion(geo.getRegion());
|
||||
presupuesto.setCiudad(geo.getCiudad());
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
// precios y totales
|
||||
presupuesto.setPrecioUnitario(BigDecimal.valueOf(precioUnit).setScale(6, RoundingMode.HALF_UP));
|
||||
presupuesto.setPrecioTotalTirada(precioTotalTirada);
|
||||
presupuesto.setServiciosTotal(serviciosTotal);
|
||||
presupuesto.setBaseImponible(baseImponible);
|
||||
presupuesto.setIvaTipo(ivaTipo);
|
||||
presupuesto.setIvaImporte(ivaImporte);
|
||||
presupuesto.setTotalConIva(totalConIva);
|
||||
|
||||
// 4) UPSERT: si viene id -> actualiza; si no, reusa el último borrador de la
|
||||
// sesión
|
||||
Presupuesto entidad;
|
||||
if (presupuesto.getId() != null) {
|
||||
entidad = presupuestoRepository.findById(presupuesto.getId()).orElse(presupuesto);
|
||||
} else {
|
||||
entidad = presupuestoRepository
|
||||
.findTopBySessionIdAndEstadoOrderByCreatedAtDesc(sessionId, Presupuesto.Estado.borrador)
|
||||
.orElse(presupuesto);
|
||||
// Si se reutiliza un borrador existente, copia el ID a nuestro objeto para
|
||||
// hacer merge
|
||||
presupuesto.setId(entidad.getId());
|
||||
}
|
||||
|
||||
// 5) Guardar/actualizar
|
||||
entidad = mergePresupuesto(entidad, presupuesto);
|
||||
presupuestoRepository.saveAndFlush(entidad);
|
||||
|
||||
// Opcional: devolver el id guardado al frontend para que lo envíe en llamadas
|
||||
// siguientes
|
||||
resumen.put("presupuesto_id", entidad.getId());
|
||||
resumen.put("precio_unitario", presupuesto.getPrecioUnitario());
|
||||
resumen.put("precio_total_tirada", presupuesto.getPrecioTotalTirada());
|
||||
resumen.put("servicios_total", presupuesto.getServiciosTotal());
|
||||
resumen.put("base_imponible", presupuesto.getBaseImponible());
|
||||
resumen.put("iva_tipo", presupuesto.getIvaTipo());
|
||||
resumen.put("iva_importe", presupuesto.getIvaImporte());
|
||||
resumen.put("total_con_iva", presupuesto.getTotalConIva());
|
||||
|
||||
return resumen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copia de campos "actualizables" para no machacar otros (created_at, etc.)
|
||||
*/
|
||||
private Presupuesto mergePresupuesto(Presupuesto target, Presupuesto src) {
|
||||
// Campos funcionales
|
||||
target.setTitulo(src.getTitulo());
|
||||
target.setTipoEncuadernacion(src.getTipoEncuadernacion());
|
||||
target.setTipoCubierta(src.getTipoCubierta());
|
||||
target.setTipoImpresion(src.getTipoImpresion());
|
||||
target.setPaginasNegro(src.getPaginasNegro());
|
||||
target.setPaginasColor(src.getPaginasColor());
|
||||
target.setPaginasColorTotal(src.getPaginasColorTotal());
|
||||
target.setPosicionPaginasColor(src.getPosicionPaginasColor());
|
||||
target.setAncho(src.getAncho());
|
||||
target.setAlto(src.getAlto());
|
||||
target.setPapelInteriorId(src.getPapelInteriorId());
|
||||
target.setGramajeInterior(src.getGramajeInterior());
|
||||
target.setPapelCubiertaId(src.getPapelCubiertaId());
|
||||
target.setGramajeCubierta(src.getGramajeCubierta());
|
||||
target.setCubiertaCaras(src.getCubiertaCaras());
|
||||
target.setSolapasCubierta(src.getSolapasCubierta());
|
||||
target.setTamanioSolapasCubierta(src.getTamanioSolapasCubierta());
|
||||
target.setSobrecubierta(src.getSobrecubierta());
|
||||
target.setPapelSobrecubiertaId(src.getPapelSobrecubiertaId());
|
||||
target.setGramajeSobrecubierta(src.getGramajeSobrecubierta());
|
||||
target.setTamanioSolapasSobrecubierta(src.getTamanioSolapasSobrecubierta());
|
||||
target.setAcabado(src.getAcabado());
|
||||
target.setCabezada(src.getCabezada());
|
||||
target.setTipoCubierta(src.getTipoCubierta());
|
||||
target.setSelectedTirada(src.getSelectedTirada());
|
||||
target.setTirada1(src.getTirada1());
|
||||
target.setTirada2(src.getTirada2());
|
||||
target.setTirada3(src.getTirada3());
|
||||
target.setTirada4(src.getTirada4());
|
||||
|
||||
// Metadatos y totales
|
||||
target.setEstado(Presupuesto.Estado.borrador);
|
||||
target.setOrigen(src.getOrigen());
|
||||
target.setSessionId(src.getSessionId());
|
||||
target.setIpHash(src.getIpHash());
|
||||
target.setIpTrunc(src.getIpTrunc());
|
||||
target.setPais(src.getPais());
|
||||
target.setRegion(src.getRegion());
|
||||
target.setCiudad(src.getCiudad());
|
||||
target.setServiciosJson(src.getServiciosJson());
|
||||
target.setPreciosPorTiradaJson(src.getPreciosPorTiradaJson());
|
||||
target.setPrecioUnitario(src.getPrecioUnitario());
|
||||
target.setPrecioTotalTirada(src.getPrecioTotalTirada());
|
||||
target.setServiciosTotal(src.getServiciosTotal());
|
||||
target.setBaseImponible(src.getBaseImponible());
|
||||
target.setIvaTipo(src.getIvaTipo());
|
||||
target.setIvaImporte(src.getIvaImporte());
|
||||
target.setTotalConIva(src.getTotalConIva());
|
||||
target.setCreatedBy(target.getCreatedBy() == null ? src.getCreatedBy() : target.getCreatedBy()); // no pisar si
|
||||
// ya existe
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* PRIVADO (futuro botón "Guardar"): persiste el presupuesto como borrador.
|
||||
*/
|
||||
@Transactional
|
||||
public Presupuesto guardarPrivado(Presupuesto presupuesto) {
|
||||
presupuesto.setEstado(Presupuesto.Estado.borrador);
|
||||
return presupuestoRepository.saveAndFlush(presupuesto);
|
||||
}
|
||||
|
||||
public HashMap<String, Object> calcularPresupuesto(Presupuesto presupuesto, Locale locale) {
|
||||
HashMap<String, Object> price = new HashMap<>();
|
||||
String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuesto), presupuesto.getTipoEncuadernacion(), presupuesto.getTipoCubierta());
|
||||
String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuesto), presupuesto.getTipoEncuadernacion(),
|
||||
presupuesto.getTipoCubierta());
|
||||
|
||||
try {
|
||||
price = new ObjectMapper().readValue(priceStr, new TypeReference<>() {
|
||||
@ -855,7 +1037,6 @@ public class PresupuestoService {
|
||||
price = new HashMap<>();
|
||||
price.put("error", messageSource.getMessage("presupuesto.error-obtener-precio", null, locale));
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package com.imprimelibros.erp.presupuesto.geo;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.GeoIpService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CompositeGeoIpService implements GeoIpService {
|
||||
|
||||
private final List<GeoIpService> delegates;
|
||||
|
||||
public CompositeGeoIpService(List<GeoIpService> delegates) {
|
||||
this.delegates = delegates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<GeoData> lookup(String ip) {
|
||||
for (GeoIpService d : delegates) {
|
||||
try {
|
||||
Optional<GeoData> r = d.lookup(ip);
|
||||
if (r.isPresent()) return r;
|
||||
} catch (Exception ignore) { /* tolerante */ }
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.imprimelibros.erp.presupuesto.geo;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.GeoIpService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class GeoIpConfig {
|
||||
|
||||
// MaxMind (local). Activar con: geoip.maxmind.enabled=true
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "geoip.maxmind", name = "enabled", havingValue = "true")
|
||||
public GeoIpService maxMindGeoIpService(ResourceLoader loader,
|
||||
GeoIpProperties props) {
|
||||
String path = props.getMaxmind().dbClasspathLocation;
|
||||
return new MaxMindGeoIpService(loader, path);
|
||||
}
|
||||
|
||||
// HTTP. Activar con: geoip.http.enabled=true
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "geoip.http", name = "enabled", havingValue = "true")
|
||||
public GeoIpService httpGeoIpService(GeoIpProperties props) {
|
||||
RestTemplate rt = new RestTemplate();
|
||||
return new HttpGeoIpService(rt, props.getHttp().endpointTemplate);
|
||||
}
|
||||
|
||||
// Composite. Activo si cualquiera de los anteriores lo está: geoip.enabled=true
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "geoip", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@Primary
|
||||
public GeoIpService compositeGeoIpService(List<GeoIpService> delegates) {
|
||||
// Si no hay ninguno, lista vacía → Composite devuelve empty y no rompe nada
|
||||
return new CompositeGeoIpService(new ArrayList<>(delegates));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GeoIpProperties geoIpProperties() {
|
||||
return new GeoIpProperties();
|
||||
}
|
||||
|
||||
// --- Props holder simple ---
|
||||
public static class GeoIpProperties {
|
||||
private MaxMindProps maxmind = new MaxMindProps();
|
||||
private HttpProps http = new HttpProps();
|
||||
|
||||
public MaxMindProps getMaxmind() { return maxmind; }
|
||||
public HttpProps getHttp() { return http; }
|
||||
|
||||
public static class MaxMindProps {
|
||||
public boolean enabled = false;
|
||||
public String dbClasspathLocation = "classpath:geoip/GeoLite2-City.mmdb";
|
||||
}
|
||||
public static class HttpProps {
|
||||
public boolean enabled = false;
|
||||
// {ip} será reemplazado por la IP
|
||||
public String endpointTemplate = "https://ipapi.co/{ip}/json/";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package com.imprimelibros.erp.presupuesto.geo;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.GeoIpService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class HttpGeoIpService implements GeoIpService {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final String endpointTemplate; // p.ej. "https://ipapi.co/{ip}/json/"
|
||||
|
||||
public HttpGeoIpService(RestTemplate restTemplate, String endpointTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.endpointTemplate = endpointTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<GeoData> lookup(String ip) {
|
||||
try {
|
||||
if (ip == null || ip.isBlank()) return Optional.empty();
|
||||
if (isPrivateIp(ip)) return Optional.empty();
|
||||
|
||||
String url = endpointTemplate.replace("{ip}", ip);
|
||||
ResponseEntity<Map> resp = restTemplate.getForEntity(url, Map.class);
|
||||
if (!resp.getStatusCode().is2xxSuccessful() || resp.getBody() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Map body = resp.getBody();
|
||||
|
||||
// Campos más comunes según proveedor
|
||||
String pais = firstNonBlank(
|
||||
(String) body.get("country_name"),
|
||||
(String) body.get("country")
|
||||
);
|
||||
String region = firstNonBlank(
|
||||
(String) body.get("region"),
|
||||
(String) body.get("regionName"),
|
||||
(String) body.get("state")
|
||||
);
|
||||
String ciudad = firstNonBlank(
|
||||
(String) body.get("city")
|
||||
);
|
||||
|
||||
if (isBlank(pais) && isBlank(region) && isBlank(ciudad)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new GeoData(pais, region, ciudad));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isBlank(String s) { return s == null || s.isBlank(); }
|
||||
private static String firstNonBlank(String... vals) {
|
||||
for (String v : vals) if (!isBlank(v)) return v;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isPrivateIp(String ip) {
|
||||
return ip.startsWith("10.") ||
|
||||
ip.startsWith("192.168.") ||
|
||||
ip.matches("^172\\.(1[6-9]|2\\d|3[0-1])\\..*") ||
|
||||
ip.equals("127.0.0.1") ||
|
||||
ip.startsWith("169.254.") ||
|
||||
ip.equals("::1") ||
|
||||
ip.startsWith("fe80:") ||
|
||||
ip.startsWith("fc00:") || ip.startsWith("fd00:");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package com.imprimelibros.erp.presupuesto.geo;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.GeoIpService;
|
||||
import com.maxmind.geoip2.DatabaseReader;
|
||||
import com.maxmind.geoip2.exception.GeoIp2Exception;
|
||||
import com.maxmind.geoip2.model.CityResponse;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MaxMindGeoIpService implements GeoIpService, AutoCloseable {
|
||||
|
||||
private final DatabaseReader dbReader;
|
||||
|
||||
public MaxMindGeoIpService(ResourceLoader resourceLoader, String dbClasspathLocation) {
|
||||
try {
|
||||
Resource resource = resourceLoader.getResource(dbClasspathLocation);
|
||||
this.dbReader = new DatabaseReader.Builder(resource.getInputStream()).build();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("No se pudo cargar la base de MaxMind desde: " + dbClasspathLocation, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<GeoData> lookup(String ip) {
|
||||
try {
|
||||
if (ip == null || ip.isBlank()) return Optional.empty();
|
||||
// Ignora IPs privadas habituales
|
||||
if (isPrivateIp(ip)) return Optional.empty();
|
||||
|
||||
InetAddress addr = InetAddress.getByName(ip);
|
||||
CityResponse resp = dbReader.city(addr);
|
||||
|
||||
String pais = safe(resp.getCountry() != null ? resp.getCountry().getNames().get("es") : null,
|
||||
resp.getCountry() != null ? resp.getCountry().getName() : null);
|
||||
String region = safe(resp.getMostSpecificSubdivision() != null ? resp.getMostSpecificSubdivision().getNames().get("es") : null,
|
||||
resp.getMostSpecificSubdivision() != null ? resp.getMostSpecificSubdivision().getName() : null);
|
||||
String ciudad = safe(resp.getCity() != null ? resp.getCity().getNames().get("es") : null,
|
||||
resp.getCity() != null ? resp.getCity().getName() : null);
|
||||
|
||||
if (isAllBlank(pais, region, ciudad)) return Optional.empty();
|
||||
return Optional.of(new GeoData(pais, region, ciudad));
|
||||
} catch (IOException | GeoIp2Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static String safe(String... candidates) {
|
||||
for (String c : candidates) if (c != null && !c.isBlank()) return c;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isAllBlank(String... vals) {
|
||||
for (String v : vals) if (v != null && !v.isBlank()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isPrivateIp(String ip) {
|
||||
// Simplificado: rangos privados IPv4, y link-local/loopback. IPv6 simplificado.
|
||||
return ip.startsWith("10.") ||
|
||||
ip.startsWith("192.168.") ||
|
||||
ip.matches("^172\\.(1[6-9]|2\\d|3[0-1])\\..*") ||
|
||||
ip.equals("127.0.0.1") ||
|
||||
ip.startsWith("169.254.") ||
|
||||
ip.equals("::1") ||
|
||||
ip.startsWith("fe80:") ||
|
||||
ip.startsWith("fc00:") || ip.startsWith("fd00:");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (dbReader != null) dbReader.close();
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,6 @@ import java.lang.reflect.Method;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.FlushModeType;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.persistence.PersistenceUnit;
|
||||
import jakarta.persistence.criteria.*;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
|
||||
@ -73,4 +73,11 @@ spring.mail.properties.mail.smtp.starttls.enable=true
|
||||
#
|
||||
# Remove JSESSIONID from URL
|
||||
#
|
||||
server.servlet.session.persistent=false
|
||||
server.servlet.session.persistent=false
|
||||
|
||||
#
|
||||
# GeoIP
|
||||
#
|
||||
geoip.enabled=true
|
||||
geoip.maxmind.enabled=true
|
||||
geoip.http.enabled=true
|
||||
|
||||
BIN
src/main/resources/geoip/GeoLite2-City.mmdb
Normal file
BIN
src/main/resources/geoip/GeoLite2-City.mmdb
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 MiB |
@ -1418,6 +1418,11 @@ class PresupuestoCliente {
|
||||
data: JSON.stringify(body)
|
||||
}).then((data) => {
|
||||
$('#resumen-titulo').text(data.titulo);
|
||||
if (resumen.presupuesto_id) {
|
||||
window.PRESUPUESTO_ID = resumen.presupuesto_id;
|
||||
}
|
||||
body.presupuesto.id = window.PRESUPUESTO_ID || body.presupuesto.id || null;
|
||||
|
||||
this.#updateResumenTable(data);
|
||||
}).catch((error) => {
|
||||
console.error("Error obtener resumen: ", error);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.imprimelibros.erp;
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ -13,7 +13,6 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.PresupuestoService;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoMaquetacion;
|
||||
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices.FontSize;
|
||||
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices.Formato;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.imprimelibros.erp;
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ -14,7 +14,6 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.PresupuestoService;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoMarcapaginas;
|
||||
import com.imprimelibros.erp.presupuesto.marcapaginas.Marcapaginas;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.imprimelibros.erp;
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
22
src/test/resources/application-test.properties
Normal file
22
src/test/resources/application-test.properties
Normal file
@ -0,0 +1,22 @@
|
||||
# BBDD de test (H2 en memoria)
|
||||
spring.datasource.url=jdbc:h2:mem:imprimelibros;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
|
||||
# JPA/Hibernate
|
||||
spring.jpa.hibernate.ddl-auto=create-drop
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
spring.jpa.show-sql=false
|
||||
|
||||
# Evitar interferencias de seguridad/auditoría en tests
|
||||
spring.main.allow-bean-definition-overriding=true
|
||||
|
||||
# Desactiva MaxMind para no requerir el .mmdb en test
|
||||
geoip.maxmind.enabled=false
|
||||
|
||||
# Activa el servicio HTTP
|
||||
geoip.http.enabled=true
|
||||
|
||||
# Usa una URL plantilla controlable (la “levanta” MockRestServiceServer)
|
||||
geoip.http.endpoint-template=http://geoip.test/{ip}
|
||||
Reference in New Issue
Block a user