feat(domain): entidad PuntoDeVenta + SecuenciaComprobante + TipoComprobante + excepciones
This commit is contained in:
77
src/api/SIGCM2.Domain/Entities/PuntoDeVenta.cs
Normal file
77
src/api/SIGCM2.Domain/Entities/PuntoDeVenta.cs
Normal 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);
|
||||
}
|
||||
34
src/api/SIGCM2.Domain/Entities/SecuenciaComprobante.cs
Normal file
34
src/api/SIGCM2.Domain/Entities/SecuenciaComprobante.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
16
src/api/SIGCM2.Domain/Enums/TipoComprobante.cs
Normal file
16
src/api/SIGCM2.Domain/Enums/TipoComprobante.cs
Normal 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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
139
tests/SIGCM2.Application.Tests/Domain/PuntoDeVentaTests.cs
Normal file
139
tests/SIGCM2.Application.Tests/Domain/PuntoDeVentaTests.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Domain.Enums;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Domain;
|
||||
|
||||
public class PuntoDeVentaTests
|
||||
{
|
||||
private static PuntoDeVenta MakePdv(
|
||||
int id = 1,
|
||||
int medioId = 5,
|
||||
short numeroAFIP = 1,
|
||||
string nombre = "PdV Central",
|
||||
string? descripcion = null,
|
||||
bool activo = true)
|
||||
=> new(id, medioId, numeroAFIP, nombre, descripcion, activo, DateTime.UtcNow, null);
|
||||
|
||||
// ── ForCreation ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_SetsCorrectValues()
|
||||
{
|
||||
var pdv = PuntoDeVenta.ForCreation(medioId: 5, numeroAFIP: 3, nombre: "PdV Sur", descripcion: null);
|
||||
|
||||
Assert.Equal(0, pdv.Id);
|
||||
Assert.Equal(5, pdv.MedioId);
|
||||
Assert.Equal(3, pdv.NumeroAFIP);
|
||||
Assert.Equal("PdV Sur", pdv.Nombre);
|
||||
Assert.Null(pdv.Descripcion);
|
||||
Assert.True(pdv.Activo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForCreation_WithDescripcion_SetsDescripcion()
|
||||
{
|
||||
var pdv = PuntoDeVenta.ForCreation(5, 1, "PdV Norte", "Descripcion larga");
|
||||
|
||||
Assert.Equal("Descripcion larga", pdv.Descripcion);
|
||||
}
|
||||
|
||||
// ── WithUpdatedProfile ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WithUpdatedProfile_ReturnsNewInstanceWithUpdatedFields()
|
||||
{
|
||||
var original = MakePdv(id: 10, nombre: "Original");
|
||||
|
||||
var updated = original.WithUpdatedProfile(nombre: "Actualizado", numeroAFIP: 7, descripcion: "Desc");
|
||||
|
||||
Assert.NotSame(original, updated);
|
||||
Assert.Equal("Actualizado", updated.Nombre);
|
||||
Assert.Equal(7, updated.NumeroAFIP);
|
||||
Assert.Equal("Desc", updated.Descripcion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithUpdatedProfile_ImmutableFields_Preserved()
|
||||
{
|
||||
var original = MakePdv(id: 10, medioId: 5);
|
||||
|
||||
var updated = original.WithUpdatedProfile("Nuevo", 2, null);
|
||||
|
||||
Assert.Equal(10, updated.Id);
|
||||
Assert.Equal(5, updated.MedioId);
|
||||
Assert.Equal(original.Activo, updated.Activo);
|
||||
Assert.Equal(original.FechaCreacion, updated.FechaCreacion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithUpdatedProfile_SetsFechaModificacion()
|
||||
{
|
||||
var original = MakePdv();
|
||||
|
||||
var updated = original.WithUpdatedProfile("Nuevo", 2, null);
|
||||
|
||||
Assert.NotNull(updated.FechaModificacion);
|
||||
}
|
||||
|
||||
// ── WithActivo ────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WithActivo_False_ReturnsDomainObjectWithActivoFalse()
|
||||
{
|
||||
var pdv = MakePdv(activo: true);
|
||||
|
||||
var deactivated = pdv.WithActivo(false);
|
||||
|
||||
Assert.False(deactivated.Activo);
|
||||
Assert.NotSame(pdv, deactivated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithActivo_True_ReturnsDomainObjectWithActivoTrue()
|
||||
{
|
||||
var pdv = MakePdv(activo: false);
|
||||
|
||||
var reactivated = pdv.WithActivo(true);
|
||||
|
||||
Assert.True(reactivated.Activo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithActivo_ImmutableFields_Preserved()
|
||||
{
|
||||
var pdv = MakePdv(id: 99, medioId: 3);
|
||||
|
||||
var toggled = pdv.WithActivo(false);
|
||||
|
||||
Assert.Equal(99, toggled.Id);
|
||||
Assert.Equal(3, toggled.MedioId);
|
||||
Assert.Equal(pdv.NumeroAFIP, toggled.NumeroAFIP);
|
||||
Assert.Equal(pdv.Nombre, toggled.Nombre);
|
||||
}
|
||||
|
||||
// ── Constructor sets all properties ───────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsAllProperties()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var pdv = new PuntoDeVenta(
|
||||
id: 7,
|
||||
medioId: 3,
|
||||
numeroAFIP: 4,
|
||||
nombre: "PdV Test",
|
||||
descripcion: "Desc Test",
|
||||
activo: true,
|
||||
fechaCreacion: now,
|
||||
fechaModificacion: null);
|
||||
|
||||
Assert.Equal(7, pdv.Id);
|
||||
Assert.Equal(3, pdv.MedioId);
|
||||
Assert.Equal(4, pdv.NumeroAFIP);
|
||||
Assert.Equal("PdV Test", pdv.Nombre);
|
||||
Assert.Equal("Desc Test", pdv.Descripcion);
|
||||
Assert.True(pdv.Activo);
|
||||
Assert.Equal(now, pdv.FechaCreacion);
|
||||
Assert.Null(pdv.FechaModificacion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Domain.Enums;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Domain;
|
||||
|
||||
public class SecuenciaComprobanteTests
|
||||
{
|
||||
private static SecuenciaComprobante Make(
|
||||
int puntoDeVentaId = 1,
|
||||
TipoComprobante tipo = TipoComprobante.FacturaA,
|
||||
int ultimoNumero = 0)
|
||||
=> new(puntoDeVentaId, tipo, ultimoNumero, DateTime.UtcNow, null);
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsAllProperties()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var seq = new SecuenciaComprobante(
|
||||
puntoDeVentaId: 3,
|
||||
tipoComprobante: TipoComprobante.FacturaB,
|
||||
ultimoNumero: 42,
|
||||
fechaCreacion: now,
|
||||
fechaModificacion: null);
|
||||
|
||||
Assert.Equal(3, seq.PuntoDeVentaId);
|
||||
Assert.Equal(TipoComprobante.FacturaB, seq.TipoComprobante);
|
||||
Assert.Equal(42, seq.UltimoNumero);
|
||||
Assert.Equal(now, seq.FechaCreacion);
|
||||
Assert.Null(seq.FechaModificacion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProximoNumero_WhenUltimoNumeroZero_ReturnsOne()
|
||||
{
|
||||
var seq = Make(ultimoNumero: 0);
|
||||
|
||||
Assert.Equal(1, seq.ProximoNumero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProximoNumero_WhenUltimoNumeroN_ReturnsNPlusOne()
|
||||
{
|
||||
var seq = Make(ultimoNumero: 7);
|
||||
|
||||
Assert.Equal(8, seq.ProximoNumero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllTipoComprobanteValues_CanBeUsedInConstructor()
|
||||
{
|
||||
foreach (TipoComprobante tipo in Enum.GetValues<TipoComprobante>())
|
||||
{
|
||||
var seq = Make(tipo: tipo);
|
||||
Assert.Equal(tipo, seq.TipoComprobante);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user