UDT-011: Localización Temporal Argentina (infra transversal) #25

Merged
dmolinari merged 24 commits from feature/UDT-011 into main 2026-04-18 13:57:49 +00:00
6 changed files with 61 additions and 50 deletions
Showing only changes of commit 4e1d8f69ab - Show all commits

View File

@@ -95,9 +95,13 @@ public sealed class IngresosBrutos
/// </summary> /// </summary>
/// <exception cref="InvalidOperationException">Si la predecesora ya está cerrada (VigenciaHasta != null).</exception> /// <exception cref="InvalidOperationException">Si la predecesora ya está cerrada (VigenciaHasta != null).</exception>
/// <exception cref="ArgumentException">Si vigenciaDesde no es posterior a la predecesora, o nuevaAlicuota fuera de rango.</exception> /// <exception cref="ArgumentException">Si vigenciaDesde no es posterior a la predecesora, o nuevaAlicuota fuera de rango.</exception>
/// <summary>
/// <param name="now">Timestamp UTC provisto por el caller (Application layer via TimeProvider).</param>
/// </summary>
public (IngresosBrutos predecesoraCerrada, IngresosBrutos nuevaVersion) NuevaVersion( public (IngresosBrutos predecesoraCerrada, IngresosBrutos nuevaVersion) NuevaVersion(
decimal nuevaAlicuota, decimal nuevaAlicuota,
DateOnly vigenciaDesde) DateOnly vigenciaDesde,
DateTime now)
{ {
if (VigenciaHasta is not null) if (VigenciaHasta is not null)
throw new InvalidOperationException( throw new InvalidOperationException(
@@ -120,7 +124,7 @@ public sealed class IngresosBrutos
vigenciaHasta: vigenciaDesde.AddDays(-1), vigenciaHasta: vigenciaDesde.AddDays(-1),
predecesorId: PredecesorId, predecesorId: PredecesorId,
fechaCreacion: FechaCreacion, fechaCreacion: FechaCreacion,
fechaModificacion: DateTime.UtcNow); fechaModificacion: now);
var nueva = ForCreation( var nueva = ForCreation(
provincia: Provincia, provincia: Provincia,
@@ -136,26 +140,26 @@ public sealed class IngresosBrutos
// ── Cosmetic mutators (NO WithAlicuota, NO WithProvincia) ───────────────── // ── Cosmetic mutators (NO WithAlicuota, NO WithProvincia) ─────────────────
/// <summary>Actualiza la descripción. Alicuota y Provincia permanecen inmutables.</summary> /// <summary>Actualiza la descripción. Alicuota y Provincia permanecen inmutables.</summary>
public IngresosBrutos WithDescripcion(string descripcion) public IngresosBrutos WithDescripcion(string descripcion, DateTime now)
=> new(Id, Provincia, descripcion, Alicuota, Activo, => new(Id, Provincia, descripcion, Alicuota, Activo,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow); VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, now);
/// <summary>Retorna instancia con Activo=false.</summary> /// <summary>Retorna instancia con Activo=false.</summary>
public IngresosBrutos Deactivate() public IngresosBrutos Deactivate(DateTime now)
=> new(Id, Provincia, Descripcion, Alicuota, false, => new(Id, Provincia, Descripcion, Alicuota, false,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow); VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, now);
/// <summary>Retorna instancia con Activo=true.</summary> /// <summary>Retorna instancia con Activo=true.</summary>
public IngresosBrutos Reactivate() public IngresosBrutos Reactivate(DateTime now)
=> new(Id, Provincia, Descripcion, Alicuota, true, => new(Id, Provincia, Descripcion, Alicuota, true,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow); VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, now);
/// <summary> /// <summary>
/// Cierra la vigencia seteando VigenciaHasta. Usado por el handler de NuevaVersion. /// Cierra la vigencia seteando VigenciaHasta. Usado por el handler de NuevaVersion.
/// </summary> /// </summary>
public IngresosBrutos CerrarVigencia(DateOnly vigenciaHasta) public IngresosBrutos CerrarVigencia(DateOnly vigenciaHasta, DateTime now)
=> new(Id, Provincia, Descripcion, Alicuota, Activo, => new(Id, Provincia, Descripcion, Alicuota, Activo,
VigenciaDesde, vigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow); VigenciaDesde, vigenciaHasta, PredecesorId, FechaCreacion, now);
// ── Private helpers ─────────────────────────────────────────────────────── // ── Private helpers ───────────────────────────────────────────────────────

View File

@@ -49,9 +49,9 @@ public sealed class Medio
/// <summary> /// <summary>
/// Returns a new instance with updated fields. Codigo is immutable (use BD UQ to enforce). /// Returns a new instance with updated fields. Codigo is immutable (use BD UQ to enforce).
/// Sets FechaModificacion = UtcNow. /// Caller is responsible for passing the current UTC timestamp via TimeProvider.GetUtcNow().UtcDateTime.
/// </summary> /// </summary>
public Medio WithUpdatedProfile(string nombre, TipoMedio tipo, int? plataformaEmpresaId) public Medio WithUpdatedProfile(string nombre, TipoMedio tipo, int? plataformaEmpresaId, DateTime now)
=> new( => new(
id: Id, id: Id,
codigo: Codigo, codigo: Codigo,
@@ -60,9 +60,9 @@ public sealed class Medio
plataformaEmpresaId: plataformaEmpresaId, plataformaEmpresaId: plataformaEmpresaId,
activo: Activo, activo: Activo,
fechaCreacion: FechaCreacion, fechaCreacion: FechaCreacion,
fechaModificacion: DateTime.UtcNow); fechaModificacion: now);
public Medio WithActivo(bool activo) public Medio WithActivo(bool activo, DateTime now)
=> new( => new(
id: Id, id: Id,
codigo: Codigo, codigo: Codigo,
@@ -71,5 +71,5 @@ public sealed class Medio
plataformaEmpresaId: PlataformaEmpresaId, plataformaEmpresaId: PlataformaEmpresaId,
activo: activo, activo: activo,
fechaCreacion: FechaCreacion, fechaCreacion: FechaCreacion,
fechaModificacion: DateTime.UtcNow); fechaModificacion: now);
} }

View File

@@ -52,8 +52,9 @@ public sealed class PuntoDeVenta
/// <summary> /// <summary>
/// Retorna una nueva instancia con nombre, numeroAFIP y descripcion actualizados. /// Retorna una nueva instancia con nombre, numeroAFIP y descripcion actualizados.
/// MedioId es inmutable (enforce en BD). /// MedioId es inmutable (enforce en BD).
/// Caller is responsible for passing the current UTC timestamp via TimeProvider.GetUtcNow().UtcDateTime.
/// </summary> /// </summary>
public PuntoDeVenta WithUpdatedProfile(string nombre, short numeroAFIP, string? descripcion) public PuntoDeVenta WithUpdatedProfile(string nombre, short numeroAFIP, string? descripcion, DateTime now)
=> new( => new(
id: Id, id: Id,
medioId: MedioId, medioId: MedioId,
@@ -62,9 +63,9 @@ public sealed class PuntoDeVenta
descripcion: descripcion, descripcion: descripcion,
activo: Activo, activo: Activo,
fechaCreacion: FechaCreacion, fechaCreacion: FechaCreacion,
fechaModificacion: DateTime.UtcNow); fechaModificacion: now);
public PuntoDeVenta WithActivo(bool activo) public PuntoDeVenta WithActivo(bool activo, DateTime now)
=> new( => new(
id: Id, id: Id,
medioId: MedioId, medioId: MedioId,
@@ -73,5 +74,5 @@ public sealed class PuntoDeVenta
descripcion: Descripcion, descripcion: Descripcion,
activo: activo, activo: activo,
fechaCreacion: FechaCreacion, fechaCreacion: FechaCreacion,
fechaModificacion: DateTime.UtcNow); fechaModificacion: now);
} }

View File

@@ -46,9 +46,9 @@ public sealed class Seccion
/// <summary> /// <summary>
/// Returns a new instance with updated fields. MedioId and Codigo are immutable. /// Returns a new instance with updated fields. MedioId and Codigo are immutable.
/// Sets FechaModificacion = UtcNow. /// Caller is responsible for passing the current UTC timestamp via TimeProvider.GetUtcNow().UtcDateTime.
/// </summary> /// </summary>
public Seccion WithUpdatedProfile(string nombre, string tipo) public Seccion WithUpdatedProfile(string nombre, string tipo, DateTime now)
=> new( => new(
id: Id, id: Id,
medioId: MedioId, medioId: MedioId,
@@ -57,9 +57,9 @@ public sealed class Seccion
tipo: tipo, tipo: tipo,
activo: Activo, activo: Activo,
fechaCreacion: FechaCreacion, fechaCreacion: FechaCreacion,
fechaModificacion: DateTime.UtcNow); fechaModificacion: now);
public Seccion WithActivo(bool activo) public Seccion WithActivo(bool activo, DateTime now)
=> new( => new(
id: Id, id: Id,
medioId: MedioId, medioId: MedioId,
@@ -68,5 +68,5 @@ public sealed class Seccion
tipo: Tipo, tipo: Tipo,
activo: activo, activo: activo,
fechaCreacion: FechaCreacion, fechaCreacion: FechaCreacion,
fechaModificacion: DateTime.UtcNow); fechaModificacion: now);
} }

View File

@@ -106,9 +106,14 @@ public sealed class TipoDeIva
/// </summary> /// </summary>
/// <exception cref="InvalidOperationException">Si la predecesora ya está cerrada (VigenciaHasta != null).</exception> /// <exception cref="InvalidOperationException">Si la predecesora ya está cerrada (VigenciaHasta != null).</exception>
/// <exception cref="ArgumentException">Si vigenciaDesde no es posterior a la predecesora, o nuevoPorcentaje fuera de rango.</exception> /// <exception cref="ArgumentException">Si vigenciaDesde no es posterior a la predecesora, o nuevoPorcentaje fuera de rango.</exception>
/// <summary>
/// Crea una nueva versión con el porcentaje actualizado.
/// <param name="now">Timestamp UTC provisto por el caller (Application layer via TimeProvider).</param>
/// </summary>
public (TipoDeIva predecesoraCerrada, TipoDeIva nuevaVersion) NuevaVersion( public (TipoDeIva predecesoraCerrada, TipoDeIva nuevaVersion) NuevaVersion(
decimal nuevoPorcentaje, decimal nuevoPorcentaje,
DateOnly vigenciaDesde) DateOnly vigenciaDesde,
DateTime now)
{ {
if (VigenciaHasta is not null) if (VigenciaHasta is not null)
throw new InvalidOperationException( throw new InvalidOperationException(
@@ -132,7 +137,7 @@ public sealed class TipoDeIva
vigenciaHasta: vigenciaDesde.AddDays(-1), vigenciaHasta: vigenciaDesde.AddDays(-1),
predecesorId: PredecesorId, predecesorId: PredecesorId,
fechaCreacion: FechaCreacion, fechaCreacion: FechaCreacion,
fechaModificacion: DateTime.UtcNow); fechaModificacion: now);
var nueva = ForCreation( var nueva = ForCreation(
codigo: Codigo, codigo: Codigo,
@@ -149,36 +154,36 @@ public sealed class TipoDeIva
// ── Cosmetic mutators (sealed With* — NOT WithPorcentaje) ───────────────── // ── Cosmetic mutators (sealed With* — NOT WithPorcentaje) ─────────────────
/// <summary>Actualiza la descripción. Porcentaje y vigencias permanecen inmutables.</summary> /// <summary>Actualiza la descripción. Porcentaje y vigencias permanecen inmutables.</summary>
public TipoDeIva WithDescripcion(string descripcion) public TipoDeIva WithDescripcion(string descripcion, DateTime now)
=> new(Id, Codigo, descripcion, Porcentaje, AplicaIVA, Activo, => new(Id, Codigo, descripcion, Porcentaje, AplicaIVA, Activo,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow); VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, now);
/// <summary>Actualiza el código. Porcentaje y vigencias permanecen inmutables.</summary> /// <summary>Actualiza el código. Porcentaje y vigencias permanecen inmutables.</summary>
public TipoDeIva WithCodigo(string codigo) public TipoDeIva WithCodigo(string codigo, DateTime now)
=> new(Id, codigo, Descripcion, Porcentaje, AplicaIVA, Activo, => new(Id, codigo, Descripcion, Porcentaje, AplicaIVA, Activo,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow); VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, now);
/// <summary>Actualiza la bandera AplicaIVA. Porcentaje permanece inmutable.</summary> /// <summary>Actualiza la bandera AplicaIVA. Porcentaje permanece inmutable.</summary>
public TipoDeIva WithAplicaIVA(bool aplicaIVA) public TipoDeIva WithAplicaIVA(bool aplicaIVA, DateTime now)
=> new(Id, Codigo, Descripcion, Porcentaje, aplicaIVA, Activo, => new(Id, Codigo, Descripcion, Porcentaje, aplicaIVA, Activo,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow); VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, now);
/// <summary>Retorna instancia con Activo=false.</summary> /// <summary>Retorna instancia con Activo=false.</summary>
public TipoDeIva Deactivate() public TipoDeIva Deactivate(DateTime now)
=> new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, false, => new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, false,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow); VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, now);
/// <summary>Retorna instancia con Activo=true.</summary> /// <summary>Retorna instancia con Activo=true.</summary>
public TipoDeIva Reactivate() public TipoDeIva Reactivate(DateTime now)
=> new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, true, => new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, true,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow); VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, now);
/// <summary> /// <summary>
/// Cierra la vigencia seteando VigenciaHasta. Usado por el handler de NuevaVersion. /// Cierra la vigencia seteando VigenciaHasta. Usado por el handler de NuevaVersion.
/// </summary> /// </summary>
public TipoDeIva CerrarVigencia(DateOnly vigenciaHasta) public TipoDeIva CerrarVigencia(DateOnly vigenciaHasta, DateTime now)
=> new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, Activo, => new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, Activo,
VigenciaDesde, vigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow); VigenciaDesde, vigenciaHasta, PredecesorId, FechaCreacion, now);
// ── Private helpers ─────────────────────────────────────────────────────── // ── Private helpers ───────────────────────────────────────────────────────

View File

@@ -76,9 +76,10 @@ public sealed class Usuario
/// <summary> /// <summary>
/// Returns a new instance with updated profile fields. /// Returns a new instance with updated profile fields.
/// Sets FechaModificacion = UtcNow. Username and PasswordHash are immutable. /// Caller is responsible for passing the current UTC timestamp via TimeProvider.GetUtcNow().UtcDateTime.
/// Username and PasswordHash are immutable.
/// </summary> /// </summary>
public Usuario WithUpdatedProfile(string nombre, string apellido, string? email, string rol, bool activo) public Usuario WithUpdatedProfile(string nombre, string apellido, string? email, string rol, bool activo, DateTime now)
=> new( => new(
id: Id, id: Id,
username: Username, username: Username,
@@ -89,15 +90,15 @@ public sealed class Usuario
rol: rol, rol: rol,
permisosJson: PermisosJson, permisosJson: PermisosJson,
activo: activo, activo: activo,
fechaModificacion: DateTime.UtcNow, fechaModificacion: now,
ultimoLogin: UltimoLogin, ultimoLogin: UltimoLogin,
mustChangePassword: MustChangePassword); mustChangePassword: MustChangePassword);
/// <summary> /// <summary>
/// Returns a new instance with a new password hash and mustChangePassword flag. /// Returns a new instance with a new password hash and mustChangePassword flag.
/// Sets FechaModificacion = UtcNow. /// Caller is responsible for passing the current UTC timestamp via TimeProvider.GetUtcNow().UtcDateTime.
/// </summary> /// </summary>
public Usuario WithNewPasswordHash(string hash, bool mustChangePassword) public Usuario WithNewPasswordHash(string hash, bool mustChangePassword, DateTime now)
=> new( => new(
id: Id, id: Id,
username: Username, username: Username,
@@ -108,15 +109,15 @@ public sealed class Usuario
rol: Rol, rol: Rol,
permisosJson: PermisosJson, permisosJson: PermisosJson,
activo: Activo, activo: Activo,
fechaModificacion: DateTime.UtcNow, fechaModificacion: now,
ultimoLogin: UltimoLogin, ultimoLogin: UltimoLogin,
mustChangePassword: mustChangePassword); mustChangePassword: mustChangePassword);
/// <summary> /// <summary>
/// Returns a new instance with only the MustChangePassword flag changed. /// Returns a new instance with only the MustChangePassword flag changed.
/// Sets FechaModificacion = UtcNow. /// Caller is responsible for passing the current UTC timestamp via TimeProvider.GetUtcNow().UtcDateTime.
/// </summary> /// </summary>
public Usuario WithMustChangePassword(bool value) public Usuario WithMustChangePassword(bool value, DateTime now)
=> new( => new(
id: Id, id: Id,
username: Username, username: Username,
@@ -127,16 +128,16 @@ public sealed class Usuario
rol: Rol, rol: Rol,
permisosJson: PermisosJson, permisosJson: PermisosJson,
activo: Activo, activo: Activo,
fechaModificacion: DateTime.UtcNow, fechaModificacion: now,
ultimoLogin: UltimoLogin, ultimoLogin: UltimoLogin,
mustChangePassword: value); mustChangePassword: value);
/// <summary> /// <summary>
/// UDT-009: Returns a new instance with PermisosJson replaced. /// UDT-009: Returns a new instance with PermisosJson replaced.
/// Sets FechaModificacion = UtcNow. /// Caller is responsible for passing the current UTC timestamp via TimeProvider.GetUtcNow().UtcDateTime.
/// Accepts raw JSON string so Domain stays free of Application dependencies. /// Accepts raw JSON string so Domain stays free of Application dependencies.
/// </summary> /// </summary>
public Usuario WithPermisosJson(string permisosJson) public Usuario WithPermisosJson(string permisosJson, DateTime now)
=> new( => new(
id: Id, id: Id,
username: Username, username: Username,
@@ -147,7 +148,7 @@ public sealed class Usuario
rol: Rol, rol: Rol,
permisosJson: permisosJson, permisosJson: permisosJson,
activo: Activo, activo: Activo,
fechaModificacion: DateTime.UtcNow, fechaModificacion: now,
ultimoLogin: UltimoLogin, ultimoLogin: UltimoLogin,
mustChangePassword: MustChangePassword); mustChangePassword: MustChangePassword);