añadidos entidades, repos y facturacionservice

This commit is contained in:
2025-12-30 19:51:04 +01:00
parent 98a5fcaa0b
commit 089641b601
18 changed files with 1200 additions and 1 deletions

View File

@ -0,0 +1,67 @@
package com.imprimelibros.erp.common.jpa;
import com.imprimelibros.erp.users.User;
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;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractAuditedEntitySoftTs {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedDate
@Column(name = "created_at", updatable = false)
private Instant createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private Instant updatedAt;
@CreatedBy
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "created_by")
private User createdBy;
@LastModifiedBy
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "updated_by")
private User updatedBy;
@Column(name = "deleted_at")
private Instant deletedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "deleted_by")
private User deletedBy;
// 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 Instant getDeletedAt() { return deletedAt; }
public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; }
public User getDeletedBy() { return deletedBy; }
public void setDeletedBy(User deletedBy) { this.deletedBy = deletedBy; }
}

View File

@ -0,0 +1,6 @@
package com.imprimelibros.erp.facturacion;
public enum EstadoFactura {
borrador,
validada
}

View File

@ -0,0 +1,7 @@
package com.imprimelibros.erp.facturacion;
public enum EstadoPagoFactura {
pendiente,
pagada,
cancelada
}

View File

@ -0,0 +1,156 @@
package com.imprimelibros.erp.facturacion;
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
import com.imprimelibros.erp.users.User;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(
name = "facturas",
uniqueConstraints = {
@UniqueConstraint(name = "uq_facturas_numero_factura", columnNames = "numero_factura")
}
)
public class Factura extends AbstractAuditedEntitySoftTs {
@Column(name = "pedido_id")
private Long pedidoId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "factura_rectificada_id")
private Factura facturaRectificada;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "factura_rectificativa_id")
private Factura facturaRectificativa;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cliente_id")
private User cliente;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "serie_id")
private SerieFactura serie;
@Column(name = "numero_factura", length = 50)
private String numeroFactura;
@Enumerated(EnumType.STRING)
@Column(name = "estado", nullable = false, length = 20)
private EstadoFactura estado = EstadoFactura.borrador;
@Enumerated(EnumType.STRING)
@Column(name = "estado_pago", nullable = false, length = 20)
private EstadoPagoFactura estadoPago = EstadoPagoFactura.pendiente;
@Enumerated(EnumType.STRING)
@Column(name = "tipo_pago", nullable = false, length = 30)
private TipoPago tipoPago = TipoPago.otros;
@Column(name = "fecha_emision")
private LocalDateTime fechaEmision;
@Column(name = "base_imponible", precision = 10, scale = 2)
private BigDecimal baseImponible;
@Column(name = "iva_4", precision = 10, scale = 2)
private BigDecimal iva4;
@Column(name = "iva_21", precision = 10, scale = 2)
private BigDecimal iva21;
@Column(name = "total_factura", precision = 10, scale = 2)
private BigDecimal totalFactura;
@Column(name = "total_pagado", precision = 10, scale = 2)
private BigDecimal totalPagado = new BigDecimal("0.00");
@Lob
@Column(name = "notas")
private String notas;
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
private List<FacturaLinea> lineas = new ArrayList<>();
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
private List<FacturaPago> pagos = new ArrayList<>();
// Helpers
public void addLinea(FacturaLinea linea) {
linea.setFactura(this);
this.lineas.add(linea);
}
public void removeLinea(FacturaLinea linea) {
this.lineas.remove(linea);
linea.setFactura(null);
}
public void addPago(FacturaPago pago) {
pago.setFactura(this);
this.pagos.add(pago);
}
public void removePago(FacturaPago pago) {
this.pagos.remove(pago);
pago.setFactura(null);
}
// Getters/Setters
public Long getPedidoId() { return pedidoId; }
public void setPedidoId(Long pedidoId) { this.pedidoId = pedidoId; }
public Factura getFacturaRectificada() { return facturaRectificada; }
public void setFacturaRectificada(Factura facturaRectificada) { this.facturaRectificada = facturaRectificada; }
public Factura getFacturaRectificativa() { return facturaRectificativa; }
public void setFacturaRectificativa(Factura facturaRectificativa) { this.facturaRectificativa = facturaRectificativa; }
public User getCliente() { return cliente; }
public void setCliente(User cliente) { this.cliente = cliente; }
public SerieFactura getSerie() { return serie; }
public void setSerie(SerieFactura serie) { this.serie = serie; }
public String getNumeroFactura() { return numeroFactura; }
public void setNumeroFactura(String numeroFactura) { this.numeroFactura = numeroFactura; }
public EstadoFactura getEstado() { return estado; }
public void setEstado(EstadoFactura estado) { this.estado = estado; }
public EstadoPagoFactura getEstadoPago() { return estadoPago; }
public void setEstadoPago(EstadoPagoFactura estadoPago) { this.estadoPago = estadoPago; }
public TipoPago getTipoPago() { return tipoPago; }
public void setTipoPago(TipoPago tipoPago) { this.tipoPago = tipoPago; }
public LocalDateTime getFechaEmision() { return fechaEmision; }
public void setFechaEmision(LocalDateTime fechaEmision) { this.fechaEmision = fechaEmision; }
public BigDecimal getBaseImponible() { return baseImponible; }
public void setBaseImponible(BigDecimal baseImponible) { this.baseImponible = baseImponible; }
public BigDecimal getIva4() { return iva4; }
public void setIva4(BigDecimal iva4) { this.iva4 = iva4; }
public BigDecimal getIva21() { return iva21; }
public void setIva21(BigDecimal iva21) { this.iva21 = iva21; }
public BigDecimal getTotalFactura() { return totalFactura; }
public void setTotalFactura(BigDecimal totalFactura) { this.totalFactura = totalFactura; }
public BigDecimal getTotalPagado() { return totalPagado; }
public void setTotalPagado(BigDecimal totalPagado) { this.totalPagado = totalPagado; }
public String getNotas() { return notas; }
public void setNotas(String notas) { this.notas = notas; }
public List<FacturaLinea> getLineas() { return lineas; }
public void setLineas(List<FacturaLinea> lineas) { this.lineas = lineas; }
public List<FacturaPago> getPagos() { return pagos; }
public void setPagos(List<FacturaPago> pagos) { this.pagos = pagos; }
}

View File

@ -0,0 +1,56 @@
package com.imprimelibros.erp.facturacion;
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "facturas_lineas")
public class FacturaLinea extends AbstractAuditedEntitySoftTs {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "factura_id")
private Factura factura;
@Lob
@Column(name = "descripcion")
private String descripcion;
@Column(name = "cantidad")
private Integer cantidad;
@Column(name = "base_linea", precision = 10, scale = 2)
private BigDecimal baseLinea;
@Column(name = "iva_4_linea", precision = 10, scale = 2)
private BigDecimal iva4Linea;
@Column(name = "iva_21_linea", precision = 10, scale = 2)
private BigDecimal iva21Linea;
@Column(name = "total_linea", precision = 10, scale = 2)
private BigDecimal totalLinea;
// Getters/Setters
public Factura getFactura() { return factura; }
public void setFactura(Factura factura) { this.factura = factura; }
public String getDescripcion() { return descripcion; }
public void setDescripcion(String descripcion) { this.descripcion = descripcion; }
public Integer getCantidad() { return cantidad; }
public void setCantidad(Integer cantidad) { this.cantidad = cantidad; }
public BigDecimal getBaseLinea() { return baseLinea; }
public void setBaseLinea(BigDecimal baseLinea) { this.baseLinea = baseLinea; }
public BigDecimal getIva4Linea() { return iva4Linea; }
public void setIva4Linea(BigDecimal iva4Linea) { this.iva4Linea = iva4Linea; }
public BigDecimal getIva21Linea() { return iva21Linea; }
public void setIva21Linea(BigDecimal iva21Linea) { this.iva21Linea = iva21Linea; }
public BigDecimal getTotalLinea() { return totalLinea; }
public void setTotalLinea(BigDecimal totalLinea) { this.totalLinea = totalLinea; }
}

View File

@ -0,0 +1,46 @@
package com.imprimelibros.erp.facturacion;
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "facturas_pagos")
public class FacturaPago extends AbstractAuditedEntitySoftTs {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "factura_id")
private Factura factura;
@Enumerated(EnumType.STRING)
@Column(name = "metodo_pago", nullable = false, length = 30)
private TipoPago metodoPago = TipoPago.otros;
@Column(name = "cantidad_pagada", precision = 10, scale = 2)
private BigDecimal cantidadPagada;
@Column(name = "fecha_pago")
private LocalDateTime fechaPago;
@Lob
@Column(name = "notas")
private String notas;
// Getters/Setters
public Factura getFactura() { return factura; }
public void setFactura(Factura factura) { this.factura = factura; }
public TipoPago getMetodoPago() { return metodoPago; }
public void setMetodoPago(TipoPago metodoPago) { this.metodoPago = metodoPago; }
public BigDecimal getCantidadPagada() { return cantidadPagada; }
public void setCantidadPagada(BigDecimal cantidadPagada) { this.cantidadPagada = cantidadPagada; }
public LocalDateTime getFechaPago() { return fechaPago; }
public void setFechaPago(LocalDateTime fechaPago) { this.fechaPago = fechaPago; }
public String getNotas() { return notas; }
public void setNotas(String notas) { this.notas = notas; }
}

View File

@ -0,0 +1,34 @@
package com.imprimelibros.erp.facturacion;
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
import jakarta.persistence.*;
@Entity
@Table(name = "series_facturas")
public class SerieFactura extends AbstractAuditedEntitySoftTs {
@Column(name = "nombre_serie", nullable = false, length = 100)
private String nombreSerie;
@Column(name = "prefijo", nullable = false, length = 10)
private String prefijo;
@Enumerated(EnumType.STRING)
@Column(name = "tipo", nullable = false, length = 50)
private TipoSerieFactura tipo = TipoSerieFactura.facturacion;
@Column(name = "numero_actual", nullable = false)
private Integer numeroActual = 1;
public String getNombreSerie() { return nombreSerie; }
public void setNombreSerie(String nombreSerie) { this.nombreSerie = nombreSerie; }
public String getPrefijo() { return prefijo; }
public void setPrefijo(String prefijo) { this.prefijo = prefijo; }
public TipoSerieFactura getTipo() { return tipo; }
public void setTipo(TipoSerieFactura tipo) { this.tipo = tipo; }
public Integer getNumeroActual() { return numeroActual; }
public void setNumeroActual(Integer numeroActual) { this.numeroActual = numeroActual; }
}

View File

@ -0,0 +1,8 @@
package com.imprimelibros.erp.facturacion;
public enum TipoPago {
tpv_tarjeta,
tpv_bizum,
transferencia,
otros
}

View File

@ -0,0 +1,5 @@
package com.imprimelibros.erp.facturacion;
public enum TipoSerieFactura {
facturacion
}

View File

@ -0,0 +1,41 @@
package com.imprimelibros.erp.facturacion.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
public class FacturaLineaUpsertDto {
private Long id; // null => nueva línea
@NotBlank
private String descripcion;
@NotNull
private Integer cantidad;
@NotNull
private BigDecimal baseLinea; // base imponible de la línea (sin IVA)
private boolean aplicaIva4;
private boolean aplicaIva21;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getDescripcion() { return descripcion; }
public void setDescripcion(String descripcion) { this.descripcion = descripcion; }
public Integer getCantidad() { return cantidad; }
public void setCantidad(Integer cantidad) { this.cantidad = cantidad; }
public BigDecimal getBaseLinea() { return baseLinea; }
public void setBaseLinea(BigDecimal baseLinea) { this.baseLinea = baseLinea; }
public boolean isAplicaIva4() { return aplicaIva4; }
public void setAplicaIva4(boolean aplicaIva4) { this.aplicaIva4 = aplicaIva4; }
public boolean isAplicaIva21() { return aplicaIva21; }
public void setAplicaIva21(boolean aplicaIva21) { this.aplicaIva21 = aplicaIva21; }
}

View File

@ -0,0 +1,36 @@
package com.imprimelibros.erp.facturacion.dto;
import com.imprimelibros.erp.facturacion.TipoPago;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class FacturaPagoUpsertDto {
private Long id; // null => nuevo pago
@NotNull
private TipoPago metodoPago;
@NotNull
private BigDecimal cantidadPagada;
private LocalDateTime fechaPago;
private String notas;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public TipoPago getMetodoPago() { return metodoPago; }
public void setMetodoPago(TipoPago metodoPago) { this.metodoPago = metodoPago; }
public BigDecimal getCantidadPagada() { return cantidadPagada; }
public void setCantidadPagada(BigDecimal cantidadPagada) { this.cantidadPagada = cantidadPagada; }
public LocalDateTime getFechaPago() { return fechaPago; }
public void setFechaPago(LocalDateTime fechaPago) { this.fechaPago = fechaPago; }
public String getNotas() { return notas; }
public void setNotas(String notas) { this.notas = notas; }
}

View File

@ -0,0 +1,10 @@
package com.imprimelibros.erp.facturacion.repo;
import com.imprimelibros.erp.facturacion.FacturaLinea;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface FacturaLineaRepository extends JpaRepository<FacturaLinea, Long> {
List<FacturaLinea> findByFacturaId(Long facturaId);
}

View File

@ -0,0 +1,10 @@
package com.imprimelibros.erp.facturacion.repo;
import com.imprimelibros.erp.facturacion.FacturaPago;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface FacturaPagoRepository extends JpaRepository<FacturaPago, Long> {
List<FacturaPago> findByFacturaId(Long facturaId);
}

View File

@ -0,0 +1,10 @@
package com.imprimelibros.erp.facturacion.repo;
import com.imprimelibros.erp.facturacion.Factura;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface FacturaRepository extends JpaRepository<Factura, Long> {
Optional<Factura> findByNumeroFactura(String numeroFactura);
}

View File

@ -0,0 +1,18 @@
package com.imprimelibros.erp.facturacion.repo;
import com.imprimelibros.erp.facturacion.SerieFactura;
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import jakarta.persistence.LockModeType;
import java.util.Optional;
public interface SerieFacturaRepository extends JpaRepository<SerieFactura, Long> {
Optional<SerieFactura> findByTipo(TipoSerieFactura tipo);
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from SerieFactura s where s.id = :id")
Optional<SerieFactura> findByIdForUpdate(@Param("id") Long id);
}

View File

@ -0,0 +1,280 @@
package com.imprimelibros.erp.facturacion.service;
import com.imprimelibros.erp.facturacion.*;
import com.imprimelibros.erp.facturacion.dto.FacturaLineaUpsertDto;
import com.imprimelibros.erp.facturacion.dto.FacturaPagoUpsertDto;
import com.imprimelibros.erp.facturacion.repo.FacturaPagoRepository;
import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
@Service
public class FacturacionService {
private final FacturaRepository facturaRepo;
private final SerieFacturaRepository serieRepo;
private final FacturaPagoRepository pagoRepo;
public FacturacionService(
FacturaRepository facturaRepo,
SerieFacturaRepository serieRepo,
FacturaPagoRepository pagoRepo
) {
this.facturaRepo = facturaRepo;
this.serieRepo = serieRepo;
this.pagoRepo = pagoRepo;
}
// -----------------------
// Estado / Numeración
// -----------------------
@Transactional
public Factura validarFactura(Long facturaId) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
// Puedes permitir validar desde borrador solamente (lo normal)
if (factura.getEstado() == EstadoFactura.validada) {
return factura;
}
if (factura.getFechaEmision() == null) {
factura.setFechaEmision(LocalDateTime.now());
}
if (factura.getSerie() == null) {
throw new IllegalStateException("La factura no tiene serie asignada.");
}
// Si ya tiene numero_factura, no reservamos otro
if (factura.getNumeroFactura() == null || factura.getNumeroFactura().isBlank()) {
SerieFactura serieLocked = serieRepo.findByIdForUpdate(factura.getSerie().getId())
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + factura.getSerie().getId()));
long next = (serieLocked.getNumeroActual() == null) ? 1L : serieLocked.getNumeroActual();
String numeroFactura = buildNumeroFactura(serieLocked.getPrefijo(), next);
factura.setNumeroFactura(numeroFactura);
// Incrementar contador para la siguiente
serieLocked.setNumeroActual((int) (next + 1)); // si cambias numero_actual a BIGINT en entidad, quita el cast
serieRepo.save(serieLocked);
}
recalcularTotales(factura);
factura.setEstado(EstadoFactura.validada);
return facturaRepo.save(factura);
}
@Transactional
public Factura volverABorrador(Long facturaId) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
factura.setEstado(EstadoFactura.borrador);
// No tocamos numero_factura (se conserva) -> evita duplicados y auditoría rara
recalcularTotales(factura);
return facturaRepo.save(factura);
}
private String buildNumeroFactura(String prefijo, long numero) {
String pref = (prefijo == null) ? "" : prefijo.trim();
String num = String.format("%07d", numero);
return pref.isBlank() ? num : (pref + "-" + num);
}
// -----------------------
// Líneas
// -----------------------
@Transactional
public Factura upsertLinea(Long facturaId, FacturaLineaUpsertDto dto) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
if (factura.getEstado() != EstadoFactura.borrador) {
throw new IllegalStateException("Solo se pueden editar líneas en facturas en borrador.");
}
FacturaLinea linea;
if (dto.getId() == null) {
linea = new FacturaLinea();
linea.setFactura(factura);
factura.getLineas().add(linea);
} else {
linea = factura.getLineas().stream()
.filter(l -> dto.getId().equals(l.getId()))
.findFirst()
.orElseThrow(() -> new EntityNotFoundException("Línea no encontrada: " + dto.getId()));
}
linea.setDescripcion(dto.getDescripcion());
linea.setCantidad(dto.getCantidad());
// Base por unidad o base total? Tu migración no define precio unitario.
// Asumimos que baseLinea es TOTAL de línea (sin IVA) y cantidad informativa.
linea.setBaseLinea(scale2(dto.getBaseLinea()));
// Iva por checks: calculamos importes, no porcentajes
BigDecimal iva4 = BigDecimal.ZERO;
BigDecimal iva21 = BigDecimal.ZERO;
if (dto.isAplicaIva4() && dto.isAplicaIva21()) {
throw new IllegalArgumentException("Una línea no puede tener IVA 4% y 21% a la vez.");
}
if (dto.isAplicaIva4()) {
iva4 = scale2(linea.getBaseLinea().multiply(new BigDecimal("0.04")));
}
if (dto.isAplicaIva21()) {
iva21 = scale2(linea.getBaseLinea().multiply(new BigDecimal("0.21")));
}
linea.setIva4Linea(iva4);
linea.setIva21Linea(iva21);
linea.setTotalLinea(scale2(linea.getBaseLinea().add(iva4).add(iva21)));
recalcularTotales(factura);
return facturaRepo.save(factura);
}
@Transactional
public Factura borrarLinea(Long facturaId, Long lineaId) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
if (factura.getEstado() != EstadoFactura.borrador) {
throw new IllegalStateException("Solo se pueden borrar líneas en facturas en borrador.");
}
boolean removed = factura.getLineas().removeIf(l -> lineaId.equals(l.getId()));
if (!removed) {
throw new EntityNotFoundException("Línea no encontrada: " + lineaId);
}
recalcularTotales(factura);
return facturaRepo.save(factura);
}
// -----------------------
// Pagos
// -----------------------
@Transactional
public Factura upsertPago(Long facturaId, FacturaPagoUpsertDto dto) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
// Permitir añadir pagos tanto en borrador como validada (según tu regla)
FacturaPago pago;
if (dto.getId() == null) {
pago = new FacturaPago();
pago.setFactura(factura);
factura.getPagos().add(pago);
} else {
pago = factura.getPagos().stream()
.filter(p -> dto.getId().equals(p.getId()))
.findFirst()
.orElseThrow(() -> new EntityNotFoundException("Pago no encontrado: " + dto.getId()));
}
pago.setMetodoPago(dto.getMetodoPago());
pago.setCantidadPagada(scale2(dto.getCantidadPagada()));
pago.setFechaPago(dto.getFechaPago() != null ? dto.getFechaPago() : LocalDateTime.now());
pago.setNotas(dto.getNotas());
// El tipo_pago de la factura: si tiene un pago, lo reflejamos (último pago manda)
factura.setTipoPago(dto.getMetodoPago());
recalcularTotales(factura);
return facturaRepo.save(factura);
}
@Transactional
public Factura borrarPago(Long facturaId, Long pagoId) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
boolean removed = factura.getPagos().removeIf(p -> pagoId.equals(p.getId()));
if (!removed) {
throw new EntityNotFoundException("Pago no encontrado: " + pagoId);
}
recalcularTotales(factura);
return facturaRepo.save(factura);
}
// -----------------------
// Recalcular totales
// -----------------------
@Transactional
public void recalcularTotales(Long facturaId) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
recalcularTotales(factura);
facturaRepo.save(factura);
}
private void recalcularTotales(Factura factura) {
BigDecimal base = BigDecimal.ZERO;
BigDecimal iva4 = BigDecimal.ZERO;
BigDecimal iva21 = BigDecimal.ZERO;
BigDecimal total = BigDecimal.ZERO;
if (factura.getLineas() != null) {
for (FacturaLinea l : factura.getLineas()) {
base = base.add(nvl(l.getBaseLinea()));
iva4 = iva4.add(nvl(l.getIva4Linea()));
iva21 = iva21.add(nvl(l.getIva21Linea()));
total = total.add(nvl(l.getTotalLinea()));
}
}
factura.setBaseImponible(scale2(base));
factura.setIva4(scale2(iva4));
factura.setIva21(scale2(iva21));
factura.setTotalFactura(scale2(total));
// total_pagado
BigDecimal pagado = BigDecimal.ZERO;
if (factura.getPagos() != null) {
for (FacturaPago p : factura.getPagos()) {
pagado = pagado.add(nvl(p.getCantidadPagada()));
}
}
factura.setTotalPagado(scale2(pagado));
// estado_pago
// - cancelada: si la factura está marcada como cancelada manualmente (aquí NO lo hacemos automático)
// - pagada: si total_pagado >= total_factura y total_factura > 0
// - pendiente: resto
if (factura.getEstadoPago() == EstadoPagoFactura.cancelada) {
return;
}
BigDecimal totalFactura = nvl(factura.getTotalFactura());
if (totalFactura.compareTo(BigDecimal.ZERO) > 0 &&
factura.getTotalPagado().compareTo(totalFactura) >= 0) {
factura.setEstadoPago(EstadoPagoFactura.pagada);
} else {
factura.setEstadoPago(EstadoPagoFactura.pendiente);
}
}
private static BigDecimal nvl(BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
private static BigDecimal scale2(BigDecimal v) {
return (v == null ? BigDecimal.ZERO : v).setScale(2, RoundingMode.HALF_UP);
}
}