feat(domain): entidad PuntoDeVenta + SecuenciaComprobante + TipoComprobante + excepciones

This commit is contained in:
2026-04-17 12:21:45 -03:00
parent bef8977c5c
commit 43877bd4a1
8 changed files with 371 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
namespace SIGCM2.Domain.Entities;
/// <summary>
/// Punto de Venta AFIP vinculado a un Medio. Gestiona comprobantes fiscales.
/// Identidad por Id (IDENTITY). NumeroAFIP único por Medio (enforced por UNIQUE(MedioId,NumeroAFIP) en BD).
/// </summary>
public sealed class PuntoDeVenta
{
public int Id { get; }
public int MedioId { get; }
public short NumeroAFIP { get; }
public string Nombre { get; }
public string? Descripcion { get; }
public bool Activo { get; }
public DateTime FechaCreacion { get; }
public DateTime? FechaModificacion { get; }
public PuntoDeVenta(
int id,
int medioId,
short numeroAFIP,
string nombre,
string? descripcion,
bool activo,
DateTime fechaCreacion,
DateTime? fechaModificacion)
{
Id = id;
MedioId = medioId;
NumeroAFIP = numeroAFIP;
Nombre = nombre;
Descripcion = descripcion;
Activo = activo;
FechaCreacion = fechaCreacion;
FechaModificacion = fechaModificacion;
}
/// <summary>
/// Factory para crear un nuevo PdV (Id=0 — BD asigna via IDENTITY; Activo=true; FechaCreacion por DF de BD).
/// </summary>
public static PuntoDeVenta ForCreation(int medioId, short numeroAFIP, string nombre, string? descripcion)
=> new(
id: 0,
medioId: medioId,
numeroAFIP: numeroAFIP,
nombre: nombre,
descripcion: descripcion,
activo: true,
fechaCreacion: default,
fechaModificacion: null);
/// <summary>
/// Retorna una nueva instancia con nombre, numeroAFIP y descripcion actualizados.
/// MedioId es inmutable (enforce en BD).
/// </summary>
public PuntoDeVenta WithUpdatedProfile(string nombre, short numeroAFIP, string? descripcion)
=> new(
id: Id,
medioId: MedioId,
numeroAFIP: numeroAFIP,
nombre: nombre,
descripcion: descripcion,
activo: Activo,
fechaCreacion: FechaCreacion,
fechaModificacion: DateTime.UtcNow);
public PuntoDeVenta WithActivo(bool activo)
=> new(
id: Id,
medioId: MedioId,
numeroAFIP: NumeroAFIP,
nombre: Nombre,
descripcion: Descripcion,
activo: activo,
fechaCreacion: FechaCreacion,
fechaModificacion: DateTime.UtcNow);
}

View File

@@ -0,0 +1,34 @@
using SIGCM2.Domain.Enums;
namespace SIGCM2.Domain.Entities;
/// <summary>
/// Lleva el correlativo de números de comprobante por (PuntoDeVentaId × TipoComprobante).
/// La reserva atómica la ejecuta usp_ReservarNumeroComprobante directamente en BD.
/// Este objeto es un helper de lectura/proyección.
/// </summary>
public sealed class SecuenciaComprobante
{
public int PuntoDeVentaId { get; }
public TipoComprobante TipoComprobante { get; }
public int UltimoNumero { get; }
public DateTime FechaCreacion { get; }
public DateTime? FechaModificacion { get; }
/// <summary>El próximo número disponible (read-only, sin modificar el estado).</summary>
public int ProximoNumero => UltimoNumero + 1;
public SecuenciaComprobante(
int puntoDeVentaId,
TipoComprobante tipoComprobante,
int ultimoNumero,
DateTime fechaCreacion,
DateTime? fechaModificacion)
{
PuntoDeVentaId = puntoDeVentaId;
TipoComprobante = tipoComprobante;
UltimoNumero = ultimoNumero;
FechaCreacion = fechaCreacion;
FechaModificacion = fechaModificacion;
}
}

View File

@@ -0,0 +1,16 @@
namespace SIGCM2.Domain.Enums;
/// <summary>
/// Tipos de comprobante AFIP soportados por ADM-008.
/// Valor TINYINT persistido en BD (CHECK TipoComprobante BETWEEN 1 AND 6).
/// Migración a tabla maestra diferida a FAC-001.
/// </summary>
public enum TipoComprobante : byte
{
FacturaA = 1,
FacturaB = 2,
FacturaC = 3,
NotaCreditoA = 4,
NotaCreditoB = 5,
NotaCreditoC = 6,
}

View File

@@ -0,0 +1,18 @@
namespace SIGCM2.Domain.Exceptions;
/// <summary>
/// Thrown when a (MedioId, NumeroAFIP) combination already exists in the system.
/// Enforced by UNIQUE(MedioId, NumeroAFIP) in DB as safety net (REQ-PDV-003).
/// </summary>
public sealed class NumeroAFIPDuplicadoException : DomainException
{
public int MedioId { get; }
public short NumeroAFIP { get; }
public NumeroAFIPDuplicadoException(int medioId, short numeroAFIP)
: base($"El número AFIP '{numeroAFIP}' ya existe para el medio {medioId}.")
{
MedioId = medioId;
NumeroAFIP = numeroAFIP;
}
}

View File

@@ -0,0 +1,15 @@
namespace SIGCM2.Domain.Exceptions;
/// <summary>
/// Thrown when a mutation (reserva) is attempted on an inactive PuntoDeVenta.
/// </summary>
public sealed class PuntoDeVentaInactivoException : DomainException
{
public int PuntoDeVentaId { get; }
public PuntoDeVentaInactivoException(int puntoDeVentaId)
: base($"El punto de venta {puntoDeVentaId} está inactivo. No se pueden realizar operaciones hasta reactivarlo.")
{
PuntoDeVentaId = puntoDeVentaId;
}
}

View File

@@ -0,0 +1,15 @@
namespace SIGCM2.Domain.Exceptions;
/// <summary>
/// Thrown when a requested PuntoDeVenta does not exist in the system.
/// </summary>
public sealed class PuntoDeVentaNotFoundException : DomainException
{
public int Id { get; }
public PuntoDeVentaNotFoundException(int id)
: base($"El punto de venta con id '{id}' no existe.")
{
Id = id;
}
}