Merge pull request 'UDT-011: Localización Temporal Argentina (infra transversal)' (#25) from feature/UDT-011 into main
This commit was merged in pull request #25.
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- Test dependencies -->
|
<!-- Test dependencies -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.1.0" />
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
|||||||
37
database/migrations/V015_ROLLBACK.sql
Normal file
37
database/migrations/V015_ROLLBACK.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
-- V015_ROLLBACK.sql
|
||||||
|
-- Reversa de V015__create_local_timezone_views.sql.
|
||||||
|
--
|
||||||
|
-- Elimina: dbo.v_AuditEvent_Local + dbo.v_SecurityEvent_Local
|
||||||
|
-- No toca datos: las tablas base AuditEvent y SecurityEvent no se modifican.
|
||||||
|
--
|
||||||
|
-- Idempotente: seguro para re-ejecutar.
|
||||||
|
-- Prerequisito: ningún objeto dependa de estas vistas (funciones, SPs, otras vistas).
|
||||||
|
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
SET ANSI_NULLS ON;
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.v_AuditEvent_Local', 'V') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP VIEW dbo.v_AuditEvent_Local;
|
||||||
|
PRINT 'View dbo.v_AuditEvent_Local dropped.';
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
PRINT 'View dbo.v_AuditEvent_Local does not exist — skip.';
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.v_SecurityEvent_Local', 'V') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP VIEW dbo.v_SecurityEvent_Local;
|
||||||
|
PRINT 'View dbo.v_SecurityEvent_Local dropped.';
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
PRINT 'View dbo.v_SecurityEvent_Local does not exist — skip.';
|
||||||
|
GO
|
||||||
|
|
||||||
|
PRINT '';
|
||||||
|
PRINT 'V015 rolled back.';
|
||||||
|
PRINT ' - dbo.v_AuditEvent_Local removed.';
|
||||||
|
PRINT ' - dbo.v_SecurityEvent_Local removed.';
|
||||||
|
GO
|
||||||
88
database/migrations/V015__create_local_timezone_views.sql
Normal file
88
database/migrations/V015__create_local_timezone_views.sql
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
-- V015__create_local_timezone_views.sql
|
||||||
|
-- UDT-011: Vistas admin con OccurredAt convertido a hora Argentina.
|
||||||
|
--
|
||||||
|
-- Crea:
|
||||||
|
-- dbo.v_AuditEvent_Local — AuditEvent con OccurredAtLocal (offset -03:00)
|
||||||
|
-- dbo.v_SecurityEvent_Local — SecurityEvent con OccurredAtLocal (offset -03:00)
|
||||||
|
--
|
||||||
|
-- Conversión: OccurredAt AT TIME ZONE 'UTC' AT TIME ZONE 'Argentina Standard Time'
|
||||||
|
-- → offset fijo -03:00, sin DST (Argentina dejó el horario de verano en 2009).
|
||||||
|
-- → Nombre 'Argentina Standard Time' es portable: Windows + SQL Server Linux 2022+ (via ICU).
|
||||||
|
--
|
||||||
|
-- Idempotente: re-ejecutable. Guard IF OBJECT_ID IS NULL en cada vista.
|
||||||
|
-- No altera tablas base — rollback seguro sin pérdida de datos.
|
||||||
|
-- Reversa: V015_ROLLBACK.sql.
|
||||||
|
-- Run on: SIGCM2 (dev) y SIGCM2_Test (integration tests).
|
||||||
|
--
|
||||||
|
-- Covers: REQ-DB-VIEWS-001, REQ-DB-VIEWS-002, REQ-DB-VIEWS-003, REQ-DB-VIEWS-004
|
||||||
|
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
SET ANSI_NULLS ON;
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
-- 1. dbo.v_AuditEvent_Local
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
-- Nota: CREATE VIEW no permite IF...BEGIN...END directo — se usa EXEC('CREATE VIEW ...').
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.v_AuditEvent_Local', 'V') IS NULL
|
||||||
|
BEGIN
|
||||||
|
EXEC('
|
||||||
|
CREATE VIEW dbo.v_AuditEvent_Local AS
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OccurredAt,
|
||||||
|
OccurredAt AT TIME ZONE ''UTC'' AT TIME ZONE ''Argentina Standard Time'' AS OccurredAtLocal,
|
||||||
|
ActorUserId,
|
||||||
|
ActorRoleId,
|
||||||
|
Action,
|
||||||
|
TargetType,
|
||||||
|
TargetId,
|
||||||
|
CorrelationId,
|
||||||
|
IpAddress,
|
||||||
|
UserAgent,
|
||||||
|
Metadata
|
||||||
|
FROM dbo.AuditEvent;
|
||||||
|
');
|
||||||
|
PRINT 'View dbo.v_AuditEvent_Local created.';
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
PRINT 'View dbo.v_AuditEvent_Local already exists — skip.';
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
-- 2. dbo.v_SecurityEvent_Local
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.v_SecurityEvent_Local', 'V') IS NULL
|
||||||
|
BEGIN
|
||||||
|
EXEC('
|
||||||
|
CREATE VIEW dbo.v_SecurityEvent_Local AS
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OccurredAt,
|
||||||
|
OccurredAt AT TIME ZONE ''UTC'' AT TIME ZONE ''Argentina Standard Time'' AS OccurredAtLocal,
|
||||||
|
ActorUserId,
|
||||||
|
AttemptedUsername,
|
||||||
|
SessionId,
|
||||||
|
Action,
|
||||||
|
Result,
|
||||||
|
FailureReason,
|
||||||
|
IpAddress,
|
||||||
|
UserAgent,
|
||||||
|
Metadata
|
||||||
|
FROM dbo.SecurityEvent;
|
||||||
|
');
|
||||||
|
PRINT 'View dbo.v_SecurityEvent_Local created.';
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
PRINT 'View dbo.v_SecurityEvent_Local already exists — skip.';
|
||||||
|
GO
|
||||||
|
|
||||||
|
PRINT '';
|
||||||
|
PRINT 'V015 applied successfully.';
|
||||||
|
PRINT ' - dbo.v_AuditEvent_Local (AuditEvent + OccurredAtLocal offset -03:00)';
|
||||||
|
PRINT ' - dbo.v_SecurityEvent_Local (SecurityEvent + OccurredAtLocal offset -03:00)';
|
||||||
|
PRINT ' - Argentina Standard Time = UTC-3 (fixed offset, no DST since 2009)';
|
||||||
|
GO
|
||||||
31
src/api/SIGCM2.Api/Json/DateOnlyJsonConverter.cs
Normal file
31
src/api/SIGCM2.Api/Json/DateOnlyJsonConverter.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SIGCM2.Api.Json;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON converter for <see cref="DateOnly"/> that uses the "yyyy-MM-dd" ISO format.
|
||||||
|
///
|
||||||
|
/// UDT-011: Ensures Cat2 date fields (VigenciaDesde, etc.) never serialize as
|
||||||
|
/// "2026-05-01T00:00:00" or with a UTC suffix "Z", which would mislead consumers
|
||||||
|
/// into treating civil Argentine dates as absolute UTC instants.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DateOnlyJsonConverter : JsonConverter<DateOnly>
|
||||||
|
{
|
||||||
|
private const string DateFormat = "yyyy-MM-dd";
|
||||||
|
|
||||||
|
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var str = reader.GetString();
|
||||||
|
if (str is null)
|
||||||
|
throw new JsonException("DateOnly value cannot be null.");
|
||||||
|
|
||||||
|
return DateOnly.ParseExact(str, DateFormat, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.ToString(DateFormat, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,13 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using Scalar.AspNetCore;
|
using Scalar.AspNetCore;
|
||||||
using SIGCM2.Api.Authorization;
|
using SIGCM2.Api.Authorization;
|
||||||
|
using SIGCM2.Api.Filters;
|
||||||
using SIGCM2.Api.HealthChecks;
|
using SIGCM2.Api.HealthChecks;
|
||||||
|
using SIGCM2.Api.Json;
|
||||||
using SIGCM2.Api.Middleware;
|
using SIGCM2.Api.Middleware;
|
||||||
using SIGCM2.Application;
|
using SIGCM2.Application;
|
||||||
using SIGCM2.Infrastructure;
|
using SIGCM2.Infrastructure;
|
||||||
using SIGCM2.Infrastructure.Audit.Jobs;
|
using SIGCM2.Infrastructure.Audit.Jobs;
|
||||||
using SIGCM2.Api.Filters;
|
|
||||||
|
|
||||||
// Bootstrap logger — before DI is built
|
// Bootstrap logger — before DI is built
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
@@ -36,10 +37,15 @@ builder.Services.AddAuthorization();
|
|||||||
builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||||
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, ForbiddenProblemDetailsHandler>();
|
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, ForbiddenProblemDetailsHandler>();
|
||||||
|
|
||||||
// Controllers with exception filter
|
// Controllers with exception filter + JSON options
|
||||||
|
// UDT-011: DateOnlyJsonConverter ensures Cat2 date fields serialize as "yyyy-MM-dd"
|
||||||
|
// and never as "2026-05-01T00:00:00" or with a UTC "Z" suffix.
|
||||||
builder.Services.AddControllers(opts =>
|
builder.Services.AddControllers(opts =>
|
||||||
{
|
{
|
||||||
opts.Filters.Add<ExceptionFilter>();
|
opts.Filters.Add<ExceptionFilter>();
|
||||||
|
}).AddJsonOptions(jsonOpts =>
|
||||||
|
{
|
||||||
|
jsonOpts.JsonSerializerOptions.Converters.Add(new DateOnlyJsonConverter());
|
||||||
});
|
});
|
||||||
|
|
||||||
// OpenAPI / Scalar
|
// OpenAPI / Scalar
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public sealed class LoginCommandHandler : ICommandHandler<LoginCommand, LoginRes
|
|||||||
private readonly IRolPermisoRepository _rolPermisoRepository;
|
private readonly IRolPermisoRepository _rolPermisoRepository;
|
||||||
private readonly ISecurityEventLogger _security;
|
private readonly ISecurityEventLogger _security;
|
||||||
private readonly ILogger<LoginCommandHandler> _logger;
|
private readonly ILogger<LoginCommandHandler> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public LoginCommandHandler(
|
public LoginCommandHandler(
|
||||||
IUsuarioRepository repository,
|
IUsuarioRepository repository,
|
||||||
@@ -33,7 +34,8 @@ public sealed class LoginCommandHandler : ICommandHandler<LoginCommand, LoginRes
|
|||||||
AuthOptions authOptions,
|
AuthOptions authOptions,
|
||||||
IRolPermisoRepository rolPermisoRepository,
|
IRolPermisoRepository rolPermisoRepository,
|
||||||
ISecurityEventLogger security,
|
ISecurityEventLogger security,
|
||||||
ILogger<LoginCommandHandler> logger)
|
ILogger<LoginCommandHandler> logger,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_hasher = hasher;
|
_hasher = hasher;
|
||||||
@@ -45,6 +47,7 @@ public sealed class LoginCommandHandler : ICommandHandler<LoginCommand, LoginRes
|
|||||||
_rolPermisoRepository = rolPermisoRepository;
|
_rolPermisoRepository = rolPermisoRepository;
|
||||||
_security = security;
|
_security = security;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LoginResponseDto> Handle(LoginCommand command)
|
public async Task<LoginResponseDto> Handle(LoginCommand command)
|
||||||
@@ -81,7 +84,7 @@ public sealed class LoginCommandHandler : ICommandHandler<LoginCommand, LoginRes
|
|||||||
// Generate and persist refresh token — only the hash hits the DB
|
// Generate and persist refresh token — only the hash hits the DB
|
||||||
var rawRefresh = _refreshGenerator.Generate();
|
var rawRefresh = _refreshGenerator.Generate();
|
||||||
var hash = TokenHasher.Sha256Base64Url(rawRefresh);
|
var hash = TokenHasher.Sha256Base64Url(rawRefresh);
|
||||||
var now = DateTime.UtcNow;
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
var ttl = TimeSpan.FromDays(_authOptions.RefreshTokenDays);
|
var ttl = TimeSpan.FromDays(_authOptions.RefreshTokenDays);
|
||||||
var entity = RefreshToken.IssueForNewFamily(
|
var entity = RefreshToken.IssueForNewFamily(
|
||||||
usuario.Id, hash, now, ttl,
|
usuario.Id, hash, now, ttl,
|
||||||
|
|||||||
@@ -8,18 +8,24 @@ public sealed class LogoutCommandHandler : ICommandHandler<LogoutCommand, Logout
|
|||||||
{
|
{
|
||||||
private readonly IRefreshTokenRepository _refreshRepo;
|
private readonly IRefreshTokenRepository _refreshRepo;
|
||||||
private readonly ISecurityEventLogger _security;
|
private readonly ISecurityEventLogger _security;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public LogoutCommandHandler(IRefreshTokenRepository refreshRepo, ISecurityEventLogger security)
|
public LogoutCommandHandler(
|
||||||
|
IRefreshTokenRepository refreshRepo,
|
||||||
|
ISecurityEventLogger security,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_refreshRepo = refreshRepo;
|
_refreshRepo = refreshRepo;
|
||||||
_security = security;
|
_security = security;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogoutResponseDto> Handle(LogoutCommand command)
|
public async Task<LogoutResponseDto> Handle(LogoutCommand command)
|
||||||
{
|
{
|
||||||
// Revoke all active tokens for the user across all families.
|
// Revoke all active tokens for the user across all families.
|
||||||
// Idempotent: 0 rows affected is not an error.
|
// Idempotent: 0 rows affected is not an error.
|
||||||
await _refreshRepo.RevokeAllActiveForUserAsync(command.UsuarioId, DateTime.UtcNow);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
await _refreshRepo.RevokeAllActiveForUserAsync(command.UsuarioId, now);
|
||||||
await _security.LogAsync("logout", "success", actorUserId: command.UsuarioId);
|
await _security.LogAsync("logout", "success", actorUserId: command.UsuarioId);
|
||||||
return new LogoutResponseDto(true, "Sesión cerrada correctamente");
|
return new LogoutResponseDto(true, "Sesión cerrada correctamente");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public sealed class RefreshCommandHandler : ICommandHandler<RefreshCommand, Refr
|
|||||||
private readonly IClientContext _clientCtx;
|
private readonly IClientContext _clientCtx;
|
||||||
private readonly AuthOptions _authOptions;
|
private readonly AuthOptions _authOptions;
|
||||||
private readonly ISecurityEventLogger _security;
|
private readonly ISecurityEventLogger _security;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public RefreshCommandHandler(
|
public RefreshCommandHandler(
|
||||||
IRefreshTokenRepository refreshRepo,
|
IRefreshTokenRepository refreshRepo,
|
||||||
@@ -25,7 +26,8 @@ public sealed class RefreshCommandHandler : ICommandHandler<RefreshCommand, Refr
|
|||||||
IRefreshTokenGenerator refreshGenerator,
|
IRefreshTokenGenerator refreshGenerator,
|
||||||
IClientContext clientCtx,
|
IClientContext clientCtx,
|
||||||
AuthOptions authOptions,
|
AuthOptions authOptions,
|
||||||
ISecurityEventLogger security)
|
ISecurityEventLogger security,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_refreshRepo = refreshRepo;
|
_refreshRepo = refreshRepo;
|
||||||
_usuarioRepo = usuarioRepo;
|
_usuarioRepo = usuarioRepo;
|
||||||
@@ -34,6 +36,7 @@ public sealed class RefreshCommandHandler : ICommandHandler<RefreshCommand, Refr
|
|||||||
_clientCtx = clientCtx;
|
_clientCtx = clientCtx;
|
||||||
_authOptions = authOptions;
|
_authOptions = authOptions;
|
||||||
_security = security;
|
_security = security;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RefreshResponseDto> Handle(RefreshCommand command)
|
public async Task<RefreshResponseDto> Handle(RefreshCommand command)
|
||||||
@@ -60,7 +63,7 @@ public sealed class RefreshCommandHandler : ICommandHandler<RefreshCommand, Refr
|
|||||||
if (stored is null)
|
if (stored is null)
|
||||||
throw new InvalidRefreshTokenException();
|
throw new InvalidRefreshTokenException();
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
|
||||||
// 4. Reuse detection: already revoked → chain revocation and throw
|
// 4. Reuse detection: already revoked → chain revocation and throw
|
||||||
if (stored.IsRevoked)
|
if (stored.IsRevoked)
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
namespace SIGCM2.Application.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for <see cref="TimeProvider"/> that expose Argentina-localized
|
||||||
|
/// date helpers. Handles the UTC-3 offset cross-platform (IANA / Windows TZ IDs).
|
||||||
|
///
|
||||||
|
/// UDT-011: Cat2 fields (VigenciaDesde, etc.) must use civil Argentine date, never
|
||||||
|
/// raw UTC, to avoid date-creep during the 22:00–23:59 window.
|
||||||
|
/// </summary>
|
||||||
|
public static class TimeProviderArgentinaExtensions
|
||||||
|
{
|
||||||
|
// IANA TZ id — Linux / macOS / .NET 8+ on Windows with ICU
|
||||||
|
public const string ArgentinaTimeZoneId = "America/Argentina/Buenos_Aires";
|
||||||
|
|
||||||
|
// Windows built-in TZ id — fallback for environments without ICU
|
||||||
|
public const string ArgentinaTimeZoneIdWindows = "Argentina Standard Time";
|
||||||
|
|
||||||
|
private static readonly TimeZoneInfo ArgentinaTz = LoadArgentinaTz();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns today's civil date in Argentina timezone, computed from the
|
||||||
|
/// <see cref="TimeProvider"/> UTC clock.
|
||||||
|
/// Safe in tests via <c>FakeTimeProvider</c>; safe in production via
|
||||||
|
/// <c>TimeProvider.System</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static DateOnly GetArgentinaToday(this TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
var utcNow = timeProvider.GetUtcNow();
|
||||||
|
var argentinaNow = TimeZoneInfo.ConvertTime(utcNow, ArgentinaTz);
|
||||||
|
return DateOnly.FromDateTime(argentinaNow.DateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeZoneInfo LoadArgentinaTz()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TimeZoneInfo.FindSystemTimeZoneById(ArgentinaTimeZoneId);
|
||||||
|
}
|
||||||
|
catch (TimeZoneNotFoundException)
|
||||||
|
{
|
||||||
|
// Windows without ICU: fall back to built-in Windows TZ name
|
||||||
|
return TimeZoneInfo.FindSystemTimeZoneById(ArgentinaTimeZoneIdWindows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,6 +67,9 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
public static IServiceCollection AddApplication(this IServiceCollection services)
|
public static IServiceCollection AddApplication(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
// UDT-011: TimeProvider singleton — available to all handlers for Cat2 date computation
|
||||||
|
services.AddSingleton(TimeProvider.System);
|
||||||
|
|
||||||
// Command handlers
|
// Command handlers
|
||||||
services.AddScoped<ICommandHandler<LoginCommand, LoginResponseDto>, LoginCommandHandler>();
|
services.AddScoped<ICommandHandler<LoginCommand, LoginResponseDto>, LoginCommandHandler>();
|
||||||
services.AddScoped<ICommandHandler<RefreshCommand, RefreshResponseDto>, RefreshCommandHandler>();
|
services.AddScoped<ICommandHandler<RefreshCommand, RefreshResponseDto>, RefreshCommandHandler>();
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ public sealed class CreateIngresosBrutosCommandHandler
|
|||||||
{
|
{
|
||||||
private readonly IIngresosBrutosRepository _repo;
|
private readonly IIngresosBrutosRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public CreateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit)
|
public CreateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IngresosBrutosDto> Handle(CreateIngresosBrutosCommand command)
|
public async Task<IngresosBrutosDto> Handle(CreateIngresosBrutosCommand command)
|
||||||
@@ -51,7 +53,7 @@ public sealed class CreateIngresosBrutosCommandHandler
|
|||||||
vigenciaDesde: entity.VigenciaDesde,
|
vigenciaDesde: entity.VigenciaDesde,
|
||||||
vigenciaHasta: entity.VigenciaHasta,
|
vigenciaHasta: entity.VigenciaHasta,
|
||||||
predecesorId: entity.PredecesorId,
|
predecesorId: entity.PredecesorId,
|
||||||
fechaCreacion: DateTime.UtcNow,
|
fechaCreacion: _timeProvider.GetUtcNow().UtcDateTime,
|
||||||
fechaModificacion: null));
|
fechaModificacion: null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ public sealed class DeactivateIngresosBrutosCommandHandler
|
|||||||
{
|
{
|
||||||
private readonly IIngresosBrutosRepository _repo;
|
private readonly IIngresosBrutosRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public DeactivateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit)
|
public DeactivateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IngresosBrutosDto> Handle(DeactivateIngresosBrutosCommand command)
|
public async Task<IngresosBrutosDto> Handle(DeactivateIngresosBrutosCommand command)
|
||||||
@@ -41,6 +43,7 @@ public sealed class DeactivateIngresosBrutosCommandHandler
|
|||||||
|
|
||||||
tx.Complete();
|
tx.Complete();
|
||||||
|
|
||||||
return IngresosBrutosMapper.ToDto(entity.Deactivate());
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
return IngresosBrutosMapper.ToDto(entity.Deactivate(now));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ public sealed class NuevaVersionIngresosBrutosCommandHandler
|
|||||||
{
|
{
|
||||||
private readonly IIngresosBrutosRepository _repo;
|
private readonly IIngresosBrutosRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public NuevaVersionIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit)
|
public NuevaVersionIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<NuevaVersionIibbResultDto> Handle(NuevaVersionIngresosBrutosCommand command)
|
public async Task<NuevaVersionIibbResultDto> Handle(NuevaVersionIngresosBrutosCommand command)
|
||||||
@@ -29,10 +31,13 @@ public sealed class NuevaVersionIngresosBrutosCommandHandler
|
|||||||
if (!predecesora.Activo || predecesora.VigenciaHasta is not null)
|
if (!predecesora.Activo || predecesora.VigenciaHasta is not null)
|
||||||
throw new PredecesorYaCerradoException(command.PredecesoraId);
|
throw new PredecesorYaCerradoException(command.PredecesoraId);
|
||||||
|
|
||||||
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
|
||||||
// Steps 3–4: domain validation + tuple creation (throws ArgumentException if vigencia invalid)
|
// Steps 3–4: domain validation + tuple creation (throws ArgumentException if vigencia invalid)
|
||||||
var (predecesoraCerrada, nuevaVersion) = predecesora.NuevaVersion(
|
var (predecesoraCerrada, nuevaVersion) = predecesora.NuevaVersion(
|
||||||
command.NuevaAlicuota,
|
command.NuevaAlicuota,
|
||||||
command.VigenciaDesde);
|
command.VigenciaDesde,
|
||||||
|
now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ public sealed class ReactivateIngresosBrutosCommandHandler
|
|||||||
{
|
{
|
||||||
private readonly IIngresosBrutosRepository _repo;
|
private readonly IIngresosBrutosRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ReactivateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit)
|
public ReactivateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IngresosBrutosDto> Handle(ReactivateIngresosBrutosCommand command)
|
public async Task<IngresosBrutosDto> Handle(ReactivateIngresosBrutosCommand command)
|
||||||
@@ -41,6 +43,7 @@ public sealed class ReactivateIngresosBrutosCommandHandler
|
|||||||
|
|
||||||
tx.Complete();
|
tx.Complete();
|
||||||
|
|
||||||
return IngresosBrutosMapper.ToDto(entity.Reactivate());
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
return IngresosBrutosMapper.ToDto(entity.Reactivate(now));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ public sealed class UpdateIngresosBrutosCommandHandler
|
|||||||
{
|
{
|
||||||
private readonly IIngresosBrutosRepository _repo;
|
private readonly IIngresosBrutosRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public UpdateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit)
|
public UpdateIngresosBrutosCommandHandler(IIngresosBrutosRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IngresosBrutosDto> Handle(UpdateIngresosBrutosCommand command)
|
public async Task<IngresosBrutosDto> Handle(UpdateIngresosBrutosCommand command)
|
||||||
@@ -24,8 +26,9 @@ public sealed class UpdateIngresosBrutosCommandHandler
|
|||||||
var entity = await _repo.GetByIdAsync(command.Id)
|
var entity = await _repo.GetByIdAsync(command.Id)
|
||||||
?? throw new IngresosBrutosNotFoundException(command.Id);
|
?? throw new IngresosBrutosNotFoundException(command.Id);
|
||||||
|
|
||||||
var updated = entity.WithDescripcion(command.Descripcion);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
updated = command.Activo ? updated.Reactivate() : updated.Deactivate();
|
var updated = entity.WithDescripcion(command.Descripcion, now);
|
||||||
|
updated = command.Activo ? updated.Reactivate(now) : updated.Deactivate(now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ public sealed class DeactivateMedioCommandHandler : ICommandHandler<DeactivateMe
|
|||||||
{
|
{
|
||||||
private readonly IMedioRepository _repo;
|
private readonly IMedioRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public DeactivateMedioCommandHandler(IMedioRepository repo, IAuditLogger audit)
|
public DeactivateMedioCommandHandler(IMedioRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MedioStatusDto> Handle(DeactivateMedioCommand command)
|
public async Task<MedioStatusDto> Handle(DeactivateMedioCommand command)
|
||||||
@@ -27,7 +29,8 @@ public sealed class DeactivateMedioCommandHandler : ICommandHandler<DeactivateMe
|
|||||||
if (!target.Activo)
|
if (!target.Activo)
|
||||||
return new MedioStatusDto(target.Id, target.Codigo, target.Activo);
|
return new MedioStatusDto(target.Id, target.Codigo, target.Activo);
|
||||||
|
|
||||||
var updated = target.WithActivo(false);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
var updated = target.WithActivo(false, now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ public sealed class ReactivateMedioCommandHandler : ICommandHandler<ReactivateMe
|
|||||||
{
|
{
|
||||||
private readonly IMedioRepository _repo;
|
private readonly IMedioRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ReactivateMedioCommandHandler(IMedioRepository repo, IAuditLogger audit)
|
public ReactivateMedioCommandHandler(IMedioRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MedioStatusDto> Handle(ReactivateMedioCommand command)
|
public async Task<MedioStatusDto> Handle(ReactivateMedioCommand command)
|
||||||
@@ -28,7 +30,8 @@ public sealed class ReactivateMedioCommandHandler : ICommandHandler<ReactivateMe
|
|||||||
if (target.Activo)
|
if (target.Activo)
|
||||||
return new MedioStatusDto(target.Id, target.Codigo, target.Activo);
|
return new MedioStatusDto(target.Id, target.Codigo, target.Activo);
|
||||||
|
|
||||||
var updated = target.WithActivo(true);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
var updated = target.WithActivo(true, now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ public sealed class UpdateMedioCommandHandler : ICommandHandler<UpdateMedioComma
|
|||||||
{
|
{
|
||||||
private readonly IMedioRepository _repo;
|
private readonly IMedioRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public UpdateMedioCommandHandler(IMedioRepository repo, IAuditLogger audit)
|
public UpdateMedioCommandHandler(IMedioRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MedioUpdatedDto> Handle(UpdateMedioCommand command)
|
public async Task<MedioUpdatedDto> Handle(UpdateMedioCommand command)
|
||||||
@@ -23,7 +25,8 @@ public sealed class UpdateMedioCommandHandler : ICommandHandler<UpdateMedioComma
|
|||||||
var target = await _repo.GetByIdAsync(command.Id)
|
var target = await _repo.GetByIdAsync(command.Id)
|
||||||
?? throw new MedioNotFoundException(command.Id);
|
?? throw new MedioNotFoundException(command.Id);
|
||||||
|
|
||||||
var updated = target.WithUpdatedProfile(command.Nombre, command.Tipo, command.PlataformaEmpresaId);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
var updated = target.WithUpdatedProfile(command.Nombre, command.Tipo, command.PlataformaEmpresaId, now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -12,15 +12,18 @@ public sealed class DeactivatePuntoDeVentaCommandHandler : ICommandHandler<Deact
|
|||||||
private readonly IPuntoDeVentaRepository _repo;
|
private readonly IPuntoDeVentaRepository _repo;
|
||||||
private readonly IMedioRepository _medioRepo;
|
private readonly IMedioRepository _medioRepo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public DeactivatePuntoDeVentaCommandHandler(
|
public DeactivatePuntoDeVentaCommandHandler(
|
||||||
IPuntoDeVentaRepository repo,
|
IPuntoDeVentaRepository repo,
|
||||||
IMedioRepository medioRepo,
|
IMedioRepository medioRepo,
|
||||||
IAuditLogger audit)
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_medioRepo = medioRepo;
|
_medioRepo = medioRepo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PuntoDeVentaStatusDto> Handle(DeactivatePuntoDeVentaCommand command)
|
public async Task<PuntoDeVentaStatusDto> Handle(DeactivatePuntoDeVentaCommand command)
|
||||||
@@ -32,7 +35,8 @@ public sealed class DeactivatePuntoDeVentaCommandHandler : ICommandHandler<Deact
|
|||||||
if (!target.Activo)
|
if (!target.Activo)
|
||||||
return new PuntoDeVentaStatusDto(target.Id, target.NumeroAFIP, target.Activo);
|
return new PuntoDeVentaStatusDto(target.Id, target.NumeroAFIP, target.Activo);
|
||||||
|
|
||||||
var updated = target.WithActivo(false);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
var updated = target.WithActivo(false, now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -13,15 +13,18 @@ public sealed class ReactivatePuntoDeVentaCommandHandler : ICommandHandler<React
|
|||||||
private readonly IPuntoDeVentaRepository _repo;
|
private readonly IPuntoDeVentaRepository _repo;
|
||||||
private readonly IMedioRepository _medioRepo;
|
private readonly IMedioRepository _medioRepo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ReactivatePuntoDeVentaCommandHandler(
|
public ReactivatePuntoDeVentaCommandHandler(
|
||||||
IPuntoDeVentaRepository repo,
|
IPuntoDeVentaRepository repo,
|
||||||
IMedioRepository medioRepo,
|
IMedioRepository medioRepo,
|
||||||
IAuditLogger audit)
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_medioRepo = medioRepo;
|
_medioRepo = medioRepo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PuntoDeVentaStatusDto> Handle(ReactivatePuntoDeVentaCommand command)
|
public async Task<PuntoDeVentaStatusDto> Handle(ReactivatePuntoDeVentaCommand command)
|
||||||
@@ -39,7 +42,8 @@ public sealed class ReactivatePuntoDeVentaCommandHandler : ICommandHandler<React
|
|||||||
if (target.Activo)
|
if (target.Activo)
|
||||||
return new PuntoDeVentaStatusDto(target.Id, target.NumeroAFIP, target.Activo);
|
return new PuntoDeVentaStatusDto(target.Id, target.NumeroAFIP, target.Activo);
|
||||||
|
|
||||||
var updated = target.WithActivo(true);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
var updated = target.WithActivo(true, now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -12,15 +12,18 @@ public sealed class UpdatePuntoDeVentaCommandHandler : ICommandHandler<UpdatePun
|
|||||||
private readonly IPuntoDeVentaRepository _repo;
|
private readonly IPuntoDeVentaRepository _repo;
|
||||||
private readonly IMedioRepository _medioRepo;
|
private readonly IMedioRepository _medioRepo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public UpdatePuntoDeVentaCommandHandler(
|
public UpdatePuntoDeVentaCommandHandler(
|
||||||
IPuntoDeVentaRepository repo,
|
IPuntoDeVentaRepository repo,
|
||||||
IMedioRepository medioRepo,
|
IMedioRepository medioRepo,
|
||||||
IAuditLogger audit)
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_medioRepo = medioRepo;
|
_medioRepo = medioRepo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PuntoDeVentaUpdatedDto> Handle(UpdatePuntoDeVentaCommand command)
|
public async Task<PuntoDeVentaUpdatedDto> Handle(UpdatePuntoDeVentaCommand command)
|
||||||
@@ -39,7 +42,8 @@ public sealed class UpdatePuntoDeVentaCommandHandler : ICommandHandler<UpdatePun
|
|||||||
if (exists)
|
if (exists)
|
||||||
throw new NumeroAFIPDuplicadoException(target.MedioId, command.NumeroAFIP);
|
throw new NumeroAFIPDuplicadoException(target.MedioId, command.NumeroAFIP);
|
||||||
|
|
||||||
var updated = target.WithUpdatedProfile(command.Nombre, command.NumeroAFIP, command.Descripcion);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
var updated = target.WithUpdatedProfile(command.Nombre, command.NumeroAFIP, command.Descripcion, now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ public sealed class DeactivateSeccionCommandHandler : ICommandHandler<Deactivate
|
|||||||
private readonly ISeccionRepository _repo;
|
private readonly ISeccionRepository _repo;
|
||||||
private readonly IMedioRepository _medioRepo;
|
private readonly IMedioRepository _medioRepo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public DeactivateSeccionCommandHandler(ISeccionRepository repo, IMedioRepository medioRepo, IAuditLogger audit)
|
public DeactivateSeccionCommandHandler(
|
||||||
|
ISeccionRepository repo,
|
||||||
|
IMedioRepository medioRepo,
|
||||||
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_medioRepo = medioRepo;
|
_medioRepo = medioRepo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SeccionStatusDto> Handle(DeactivateSeccionCommand command)
|
public async Task<SeccionStatusDto> Handle(DeactivateSeccionCommand command)
|
||||||
@@ -35,7 +41,8 @@ public sealed class DeactivateSeccionCommandHandler : ICommandHandler<Deactivate
|
|||||||
if (!target.Activo)
|
if (!target.Activo)
|
||||||
return new SeccionStatusDto(target.Id, target.Codigo, target.Activo);
|
return new SeccionStatusDto(target.Id, target.Codigo, target.Activo);
|
||||||
|
|
||||||
var updated = target.WithActivo(false);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
var updated = target.WithActivo(false, now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -13,12 +13,18 @@ public sealed class ReactivateSeccionCommandHandler : ICommandHandler<Reactivate
|
|||||||
private readonly ISeccionRepository _repo;
|
private readonly ISeccionRepository _repo;
|
||||||
private readonly IMedioRepository _medioRepo;
|
private readonly IMedioRepository _medioRepo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ReactivateSeccionCommandHandler(ISeccionRepository repo, IMedioRepository medioRepo, IAuditLogger audit)
|
public ReactivateSeccionCommandHandler(
|
||||||
|
ISeccionRepository repo,
|
||||||
|
IMedioRepository medioRepo,
|
||||||
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_medioRepo = medioRepo;
|
_medioRepo = medioRepo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SeccionStatusDto> Handle(ReactivateSeccionCommand command)
|
public async Task<SeccionStatusDto> Handle(ReactivateSeccionCommand command)
|
||||||
@@ -36,7 +42,8 @@ public sealed class ReactivateSeccionCommandHandler : ICommandHandler<Reactivate
|
|||||||
if (target.Activo)
|
if (target.Activo)
|
||||||
return new SeccionStatusDto(target.Id, target.Codigo, target.Activo);
|
return new SeccionStatusDto(target.Id, target.Codigo, target.Activo);
|
||||||
|
|
||||||
var updated = target.WithActivo(true);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
var updated = target.WithActivo(true, now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ public sealed class UpdateSeccionCommandHandler : ICommandHandler<UpdateSeccionC
|
|||||||
private readonly ISeccionRepository _repo;
|
private readonly ISeccionRepository _repo;
|
||||||
private readonly IMedioRepository _medioRepo;
|
private readonly IMedioRepository _medioRepo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public UpdateSeccionCommandHandler(ISeccionRepository repo, IMedioRepository medioRepo, IAuditLogger audit)
|
public UpdateSeccionCommandHandler(
|
||||||
|
ISeccionRepository repo,
|
||||||
|
IMedioRepository medioRepo,
|
||||||
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_medioRepo = medioRepo;
|
_medioRepo = medioRepo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SeccionUpdatedDto> Handle(UpdateSeccionCommand command)
|
public async Task<SeccionUpdatedDto> Handle(UpdateSeccionCommand command)
|
||||||
@@ -31,7 +37,8 @@ public sealed class UpdateSeccionCommandHandler : ICommandHandler<UpdateSeccionC
|
|||||||
if (!medio.Activo)
|
if (!medio.Activo)
|
||||||
throw new MedioInactivoException(medio.Id);
|
throw new MedioInactivoException(medio.Id);
|
||||||
|
|
||||||
var updated = target.WithUpdatedProfile(command.Nombre, command.Tipo);
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
var updated = target.WithUpdatedProfile(command.Nombre, command.Tipo, now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ public sealed class CreateTipoDeIvaCommandHandler : ICommandHandler<CreateTipoDe
|
|||||||
{
|
{
|
||||||
private readonly ITipoDeIvaRepository _repo;
|
private readonly ITipoDeIvaRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public CreateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit)
|
public CreateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TipoDeIvaDto> Handle(CreateTipoDeIvaCommand command)
|
public async Task<TipoDeIvaDto> Handle(CreateTipoDeIvaCommand command)
|
||||||
@@ -55,7 +57,7 @@ public sealed class CreateTipoDeIvaCommandHandler : ICommandHandler<CreateTipoDe
|
|||||||
vigenciaDesde: entity.VigenciaDesde,
|
vigenciaDesde: entity.VigenciaDesde,
|
||||||
vigenciaHasta: entity.VigenciaHasta,
|
vigenciaHasta: entity.VigenciaHasta,
|
||||||
predecesorId: entity.PredecesorId,
|
predecesorId: entity.PredecesorId,
|
||||||
fechaCreacion: DateTime.UtcNow,
|
fechaCreacion: _timeProvider.GetUtcNow().UtcDateTime,
|
||||||
fechaModificacion: null));
|
fechaModificacion: null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ public sealed class DeactivateTipoDeIvaCommandHandler : ICommandHandler<Deactiva
|
|||||||
{
|
{
|
||||||
private readonly ITipoDeIvaRepository _repo;
|
private readonly ITipoDeIvaRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public DeactivateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit)
|
public DeactivateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TipoDeIvaDto> Handle(DeactivateTipoDeIvaCommand command)
|
public async Task<TipoDeIvaDto> Handle(DeactivateTipoDeIvaCommand command)
|
||||||
@@ -41,6 +43,7 @@ public sealed class DeactivateTipoDeIvaCommandHandler : ICommandHandler<Deactiva
|
|||||||
|
|
||||||
tx.Complete();
|
tx.Complete();
|
||||||
|
|
||||||
return TipoDeIvaMapper.ToDto(entity.Deactivate());
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
return TipoDeIvaMapper.ToDto(entity.Deactivate(now));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ public sealed class NuevaVersionTipoDeIvaCommandHandler
|
|||||||
{
|
{
|
||||||
private readonly ITipoDeIvaRepository _repo;
|
private readonly ITipoDeIvaRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public NuevaVersionTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit)
|
public NuevaVersionTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<NuevaVersionResultDto> Handle(NuevaVersionTipoDeIvaCommand command)
|
public async Task<NuevaVersionResultDto> Handle(NuevaVersionTipoDeIvaCommand command)
|
||||||
@@ -29,10 +31,13 @@ public sealed class NuevaVersionTipoDeIvaCommandHandler
|
|||||||
if (!predecesora.Activo || predecesora.VigenciaHasta is not null)
|
if (!predecesora.Activo || predecesora.VigenciaHasta is not null)
|
||||||
throw new PredecesorYaCerradoException(command.PredecesoraId);
|
throw new PredecesorYaCerradoException(command.PredecesoraId);
|
||||||
|
|
||||||
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
|
||||||
// Steps 3–4: delegate validation + tuple creation to domain (throws ArgumentException on invalid vigencia)
|
// Steps 3–4: delegate validation + tuple creation to domain (throws ArgumentException on invalid vigencia)
|
||||||
var (predecesoraCerrada, nuevaVersion) = predecesora.NuevaVersion(
|
var (predecesoraCerrada, nuevaVersion) = predecesora.NuevaVersion(
|
||||||
command.NuevoPorcentaje,
|
command.NuevoPorcentaje,
|
||||||
command.VigenciaDesde);
|
command.VigenciaDesde,
|
||||||
|
now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ public sealed class ReactivateTipoDeIvaCommandHandler : ICommandHandler<Reactiva
|
|||||||
{
|
{
|
||||||
private readonly ITipoDeIvaRepository _repo;
|
private readonly ITipoDeIvaRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ReactivateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit)
|
public ReactivateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TipoDeIvaDto> Handle(ReactivateTipoDeIvaCommand command)
|
public async Task<TipoDeIvaDto> Handle(ReactivateTipoDeIvaCommand command)
|
||||||
@@ -41,6 +43,7 @@ public sealed class ReactivateTipoDeIvaCommandHandler : ICommandHandler<Reactiva
|
|||||||
|
|
||||||
tx.Complete();
|
tx.Complete();
|
||||||
|
|
||||||
return TipoDeIvaMapper.ToDto(entity.Reactivate());
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
return TipoDeIvaMapper.ToDto(entity.Reactivate(now));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ public sealed class UpdateTipoDeIvaCommandHandler : ICommandHandler<UpdateTipoDe
|
|||||||
{
|
{
|
||||||
private readonly ITipoDeIvaRepository _repo;
|
private readonly ITipoDeIvaRepository _repo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public UpdateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit)
|
public UpdateTipoDeIvaCommandHandler(ITipoDeIvaRepository repo, IAuditLogger audit, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TipoDeIvaDto> Handle(UpdateTipoDeIvaCommand command)
|
public async Task<TipoDeIvaDto> Handle(UpdateTipoDeIvaCommand command)
|
||||||
@@ -23,15 +25,16 @@ public sealed class UpdateTipoDeIvaCommandHandler : ICommandHandler<UpdateTipoDe
|
|||||||
var entity = await _repo.GetByIdAsync(command.Id)
|
var entity = await _repo.GetByIdAsync(command.Id)
|
||||||
?? throw new TipoDeIvaNotFoundException(command.Id);
|
?? throw new TipoDeIvaNotFoundException(command.Id);
|
||||||
|
|
||||||
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
var updated = entity
|
var updated = entity
|
||||||
.WithCodigo(command.Codigo)
|
.WithCodigo(command.Codigo, now)
|
||||||
.WithDescripcion(command.Descripcion)
|
.WithDescripcion(command.Descripcion, now)
|
||||||
.WithAplicaIVA(command.AplicaIVA);
|
.WithAplicaIVA(command.AplicaIVA, now);
|
||||||
|
|
||||||
// Apply Activo change if needed
|
// Apply Activo change if needed
|
||||||
updated = command.Activo
|
updated = command.Activo
|
||||||
? updated.Reactivate()
|
? updated.Reactivate(now)
|
||||||
: updated.Deactivate();
|
: updated.Deactivate(now);
|
||||||
|
|
||||||
using var tx = new TransactionScope(
|
using var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -13,15 +13,18 @@ public sealed class DeactivateUsuarioCommandHandler : ICommandHandler<Deactivate
|
|||||||
private readonly IUsuarioRepository _repository;
|
private readonly IUsuarioRepository _repository;
|
||||||
private readonly IRefreshTokenRepository _refreshTokenRepository;
|
private readonly IRefreshTokenRepository _refreshTokenRepository;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public DeactivateUsuarioCommandHandler(
|
public DeactivateUsuarioCommandHandler(
|
||||||
IUsuarioRepository repository,
|
IUsuarioRepository repository,
|
||||||
IRefreshTokenRepository refreshTokenRepository,
|
IRefreshTokenRepository refreshTokenRepository,
|
||||||
IAuditLogger audit)
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_refreshTokenRepository = refreshTokenRepository;
|
_refreshTokenRepository = refreshTokenRepository;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UsuarioDetailDto> Handle(DeactivateUsuarioCommand cmd)
|
public async Task<UsuarioDetailDto> Handle(DeactivateUsuarioCommand cmd)
|
||||||
@@ -43,7 +46,7 @@ public sealed class DeactivateUsuarioCommandHandler : ICommandHandler<Deactivate
|
|||||||
throw new LastAdminLockoutException();
|
throw new LastAdminLockoutException();
|
||||||
|
|
||||||
var fields = new UpdateUsuarioFields(target.Nombre, target.Apellido, target.Email, target.Rol, false);
|
var fields = new UpdateUsuarioFields(target.Nombre, target.Apellido, target.Email, target.Rol, false);
|
||||||
var now = DateTime.UtcNow;
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
|
||||||
using (var tx = new TransactionScope(
|
using (var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -18,17 +18,20 @@ public sealed class UpdateUsuarioPermisosOverridesCommandHandler
|
|||||||
private readonly IRolPermisoRepository _rolPermisoRepo;
|
private readonly IRolPermisoRepository _rolPermisoRepo;
|
||||||
private readonly IPermisoRepository _permisoRepo;
|
private readonly IPermisoRepository _permisoRepo;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public UpdateUsuarioPermisosOverridesCommandHandler(
|
public UpdateUsuarioPermisosOverridesCommandHandler(
|
||||||
IUsuarioRepository usuarioRepo,
|
IUsuarioRepository usuarioRepo,
|
||||||
IRolPermisoRepository rolPermisoRepo,
|
IRolPermisoRepository rolPermisoRepo,
|
||||||
IPermisoRepository permisoRepo,
|
IPermisoRepository permisoRepo,
|
||||||
IAuditLogger audit)
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_usuarioRepo = usuarioRepo;
|
_usuarioRepo = usuarioRepo;
|
||||||
_rolPermisoRepo = rolPermisoRepo;
|
_rolPermisoRepo = rolPermisoRepo;
|
||||||
_permisoRepo = permisoRepo;
|
_permisoRepo = permisoRepo;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UsuarioPermisosDto> Handle(UpdateUsuarioPermisosOverridesCommand command)
|
public async Task<UsuarioPermisosDto> Handle(UpdateUsuarioPermisosOverridesCommand command)
|
||||||
@@ -59,7 +62,8 @@ public sealed class UpdateUsuarioPermisosOverridesCommandHandler
|
|||||||
// 4. Persist — use WithPermisosJson to get updated FechaModificacion
|
// 4. Persist — use WithPermisosJson to get updated FechaModificacion
|
||||||
var newOverrides = new PermisosOverride(grant, deny);
|
var newOverrides = new PermisosOverride(grant, deny);
|
||||||
var previousOverrides = PermisosOverride.FromJson(usuario.PermisosJson);
|
var previousOverrides = PermisosOverride.FromJson(usuario.PermisosJson);
|
||||||
var updated = usuario.WithPermisosJson(newOverrides.ToJson());
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
var updated = usuario.WithPermisosJson(newOverrides.ToJson(), now);
|
||||||
|
|
||||||
using (var tx = new TransactionScope(
|
using (var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ public sealed class ReactivateUsuarioCommandHandler : ICommandHandler<Reactivate
|
|||||||
{
|
{
|
||||||
private readonly IUsuarioRepository _repository;
|
private readonly IUsuarioRepository _repository;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ReactivateUsuarioCommandHandler(IUsuarioRepository repository, IAuditLogger audit)
|
public ReactivateUsuarioCommandHandler(
|
||||||
|
IUsuarioRepository repository,
|
||||||
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UsuarioDetailDto> Handle(ReactivateUsuarioCommand cmd)
|
public async Task<UsuarioDetailDto> Handle(ReactivateUsuarioCommand cmd)
|
||||||
@@ -34,7 +39,7 @@ public sealed class ReactivateUsuarioCommandHandler : ICommandHandler<Reactivate
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fields = new UpdateUsuarioFields(target.Nombre, target.Apellido, target.Email, target.Rol, true);
|
var fields = new UpdateUsuarioFields(target.Nombre, target.Apellido, target.Email, target.Rol, true);
|
||||||
var now = DateTime.UtcNow;
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
|
||||||
using (var tx = new TransactionScope(
|
using (var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -14,17 +14,20 @@ public sealed class ResetUsuarioPasswordCommandHandler : ICommandHandler<ResetUs
|
|||||||
private readonly IPasswordHasher _hasher;
|
private readonly IPasswordHasher _hasher;
|
||||||
private readonly IRefreshTokenRepository _refreshTokenRepository;
|
private readonly IRefreshTokenRepository _refreshTokenRepository;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public ResetUsuarioPasswordCommandHandler(
|
public ResetUsuarioPasswordCommandHandler(
|
||||||
IUsuarioRepository repository,
|
IUsuarioRepository repository,
|
||||||
IPasswordHasher hasher,
|
IPasswordHasher hasher,
|
||||||
IRefreshTokenRepository refreshTokenRepository,
|
IRefreshTokenRepository refreshTokenRepository,
|
||||||
IAuditLogger audit)
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_hasher = hasher;
|
_hasher = hasher;
|
||||||
_refreshTokenRepository = refreshTokenRepository;
|
_refreshTokenRepository = refreshTokenRepository;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ResetUsuarioPasswordResponse> Handle(ResetUsuarioPasswordCommand cmd)
|
public async Task<ResetUsuarioPasswordResponse> Handle(ResetUsuarioPasswordCommand cmd)
|
||||||
@@ -45,8 +48,9 @@ public sealed class ResetUsuarioPasswordCommandHandler : ICommandHandler<ResetUs
|
|||||||
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
|
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
|
||||||
TransactionScopeAsyncFlowOption.Enabled);
|
TransactionScopeAsyncFlowOption.Enabled);
|
||||||
|
|
||||||
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
await _repository.UpdatePasswordAsync(cmd.TargetId, hash, mustChangePassword: true);
|
await _repository.UpdatePasswordAsync(cmd.TargetId, hash, mustChangePassword: true);
|
||||||
await _refreshTokenRepository.RevokeAllActiveForUserAsync(cmd.TargetId, DateTime.UtcNow);
|
await _refreshTokenRepository.RevokeAllActiveForUserAsync(cmd.TargetId, now);
|
||||||
|
|
||||||
await _audit.LogAsync(
|
await _audit.LogAsync(
|
||||||
action: "usuario.password_reset",
|
action: "usuario.password_reset",
|
||||||
|
|||||||
@@ -14,17 +14,20 @@ public sealed class UpdateUsuarioCommandHandler : ICommandHandler<UpdateUsuarioC
|
|||||||
private readonly IRolRepository _rolRepository;
|
private readonly IRolRepository _rolRepository;
|
||||||
private readonly IRefreshTokenRepository _refreshTokenRepository;
|
private readonly IRefreshTokenRepository _refreshTokenRepository;
|
||||||
private readonly IAuditLogger _audit;
|
private readonly IAuditLogger _audit;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public UpdateUsuarioCommandHandler(
|
public UpdateUsuarioCommandHandler(
|
||||||
IUsuarioRepository repository,
|
IUsuarioRepository repository,
|
||||||
IRolRepository rolRepository,
|
IRolRepository rolRepository,
|
||||||
IRefreshTokenRepository refreshTokenRepository,
|
IRefreshTokenRepository refreshTokenRepository,
|
||||||
IAuditLogger audit)
|
IAuditLogger audit,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_rolRepository = rolRepository;
|
_rolRepository = rolRepository;
|
||||||
_refreshTokenRepository = refreshTokenRepository;
|
_refreshTokenRepository = refreshTokenRepository;
|
||||||
_audit = audit;
|
_audit = audit;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UsuarioDetailDto> Handle(UpdateUsuarioCommand cmd)
|
public async Task<UsuarioDetailDto> Handle(UpdateUsuarioCommand cmd)
|
||||||
@@ -52,7 +55,7 @@ public sealed class UpdateUsuarioCommandHandler : ICommandHandler<UpdateUsuarioC
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fields = new UpdateUsuarioFields(cmd.Nombre, cmd.Apellido, cmd.Email, cmd.Rol, cmd.Activo);
|
var fields = new UpdateUsuarioFields(cmd.Nombre, cmd.Apellido, cmd.Email, cmd.Rol, cmd.Activo);
|
||||||
var now = DateTime.UtcNow;
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
|
||||||
using (var tx = new TransactionScope(
|
using (var tx = new TransactionScope(
|
||||||
TransactionScopeOption.Required,
|
TransactionScopeOption.Required,
|
||||||
|
|||||||
@@ -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 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -12,15 +12,18 @@ public sealed class AuditLogger : IAuditLogger
|
|||||||
private readonly IAuditContext _context;
|
private readonly IAuditContext _context;
|
||||||
private readonly IAuditEventRepository _repo;
|
private readonly IAuditEventRepository _repo;
|
||||||
private readonly IOptions<AuditOptions> _options;
|
private readonly IOptions<AuditOptions> _options;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public AuditLogger(
|
public AuditLogger(
|
||||||
IAuditContext context,
|
IAuditContext context,
|
||||||
IAuditEventRepository repo,
|
IAuditEventRepository repo,
|
||||||
IOptions<AuditOptions> options)
|
IOptions<AuditOptions> options,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_options = options;
|
_options = options;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogAsync(
|
public async Task LogAsync(
|
||||||
@@ -42,7 +45,7 @@ public sealed class AuditLogger : IAuditLogger
|
|||||||
: _context.CorrelationId;
|
: _context.CorrelationId;
|
||||||
|
|
||||||
await _repo.InsertAsync(
|
await _repo.InsertAsync(
|
||||||
occurredAt: DateTime.UtcNow,
|
occurredAt: _timeProvider.GetUtcNow().UtcDateTime,
|
||||||
actorUserId: _context.ActorUserId,
|
actorUserId: _context.ActorUserId,
|
||||||
actorRoleId: _context.ActorRoleId,
|
actorRoleId: _context.ActorRoleId,
|
||||||
action: action,
|
action: action,
|
||||||
|
|||||||
@@ -11,15 +11,18 @@ public sealed class SecurityEventLogger : ISecurityEventLogger
|
|||||||
private readonly ISecurityEventRepository _repo;
|
private readonly ISecurityEventRepository _repo;
|
||||||
private readonly IAuditContext _context;
|
private readonly IAuditContext _context;
|
||||||
private readonly IOptions<AuditOptions> _options;
|
private readonly IOptions<AuditOptions> _options;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public SecurityEventLogger(
|
public SecurityEventLogger(
|
||||||
ISecurityEventRepository repo,
|
ISecurityEventRepository repo,
|
||||||
IAuditContext context,
|
IAuditContext context,
|
||||||
IOptions<AuditOptions> options)
|
IOptions<AuditOptions> options,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_context = context;
|
_context = context;
|
||||||
_options = options;
|
_options = options;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogAsync(
|
public async Task LogAsync(
|
||||||
@@ -37,7 +40,7 @@ public sealed class SecurityEventLogger : ISecurityEventLogger
|
|||||||
: JsonSanitizer.Sanitize(metadata, _options.Value.SanitizedKeys);
|
: JsonSanitizer.Sanitize(metadata, _options.Value.SanitizedKeys);
|
||||||
|
|
||||||
await _repo.InsertAsync(
|
await _repo.InsertAsync(
|
||||||
occurredAt: DateTime.UtcNow,
|
occurredAt: _timeProvider.GetUtcNow().UtcDateTime,
|
||||||
actorUserId: actorUserId,
|
actorUserId: actorUserId,
|
||||||
attemptedUsername: attemptedUsername,
|
attemptedUsername: attemptedUsername,
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
|
|||||||
@@ -68,7 +68,10 @@ public static class DependencyInjection
|
|||||||
});
|
});
|
||||||
|
|
||||||
services.AddScoped<IJwtService>(sp =>
|
services.AddScoped<IJwtService>(sp =>
|
||||||
new JwtService(sp.GetRequiredService<RSA>(), sp.GetRequiredService<JwtOptions>()));
|
new JwtService(
|
||||||
|
sp.GetRequiredService<RSA>(),
|
||||||
|
sp.GetRequiredService<JwtOptions>(),
|
||||||
|
sp.GetRequiredService<TimeProvider>()));
|
||||||
services.AddScoped<IPasswordHasher, BcryptPasswordHasher>();
|
services.AddScoped<IPasswordHasher, BcryptPasswordHasher>();
|
||||||
services.AddSingleton<IRefreshTokenGenerator, RefreshTokenGenerator>();
|
services.AddSingleton<IRefreshTokenGenerator, RefreshTokenGenerator>();
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public sealed class MedioRepository : IMedioRepository
|
|||||||
Tipo = (int)m.Tipo,
|
Tipo = (int)m.Tipo,
|
||||||
m.PlataformaEmpresaId,
|
m.PlataformaEmpresaId,
|
||||||
m.Activo,
|
m.Activo,
|
||||||
FechaModificacion = m.FechaModificacion ?? DateTime.UtcNow,
|
FechaModificacion = m.FechaModificacion,
|
||||||
m.Id,
|
m.Id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ public sealed class PuntoDeVentaRepository : IPuntoDeVentaRepository
|
|||||||
pdv.Nombre,
|
pdv.Nombre,
|
||||||
pdv.Descripcion,
|
pdv.Descripcion,
|
||||||
pdv.Activo,
|
pdv.Activo,
|
||||||
FechaModificacion = pdv.FechaModificacion ?? DateTime.UtcNow,
|
FechaModificacion = pdv.FechaModificacion,
|
||||||
pdv.Id,
|
pdv.Id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public sealed class SeccionRepository : ISeccionRepository
|
|||||||
s.Nombre,
|
s.Nombre,
|
||||||
s.Tipo,
|
s.Tipo,
|
||||||
s.Activo,
|
s.Activo,
|
||||||
FechaModificacion = s.FechaModificacion ?? DateTime.UtcNow,
|
FechaModificacion = s.FechaModificacion,
|
||||||
s.Id,
|
s.Id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ public sealed class JwtService : IJwtService
|
|||||||
{
|
{
|
||||||
private readonly RSA _rsa;
|
private readonly RSA _rsa;
|
||||||
private readonly JwtOptions _options;
|
private readonly JwtOptions _options;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public JwtService(RSA rsa, JwtOptions options)
|
public JwtService(RSA rsa, JwtOptions options, TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_rsa = rsa;
|
_rsa = rsa;
|
||||||
_options = options;
|
_options = options;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -62,7 +64,7 @@ public sealed class JwtService : IJwtService
|
|||||||
new("rol", usuario.Rol),
|
new("rol", usuario.Rol),
|
||||||
};
|
};
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
var descriptor = new SecurityTokenDescriptor
|
var descriptor = new SecurityTokenDescriptor
|
||||||
{
|
{
|
||||||
Subject = new ClaimsIdentity(claims),
|
Subject = new ClaimsIdentity(claims),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Modal de edición / creación de IngresosBrutos
|
// Modal de edición / creación de IngresosBrutos
|
||||||
// CRÍTICO: NO incluye campo Alícuota en modo edit (inmutable, cambiar via NuevaVersion)
|
// CRÍTICO: NO incluye campo Alícuota en modo edit (inmutable, cambiar via NuevaVersion)
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { todayArgentina } from '@/lib/dateFormat'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
@@ -96,7 +97,7 @@ export function IngresosBrutosFormModal({
|
|||||||
descripcion: '',
|
descripcion: '',
|
||||||
activo: true,
|
activo: true,
|
||||||
alicuotaCreate: undefined,
|
alicuotaCreate: undefined,
|
||||||
vigenciaDesde: '',
|
vigenciaDesde: todayArgentina(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ export function IngresosBrutosFormModal({
|
|||||||
descripcion: '',
|
descripcion: '',
|
||||||
activo: true,
|
activo: true,
|
||||||
alicuotaCreate: undefined,
|
alicuotaCreate: undefined,
|
||||||
vigenciaDesde: '',
|
vigenciaDesde: todayArgentina(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
createMutation.reset()
|
createMutation.reset()
|
||||||
@@ -152,7 +153,7 @@ export function IngresosBrutosFormModal({
|
|||||||
provincia: values.provincia as ProvinciaArgentina,
|
provincia: values.provincia as ProvinciaArgentina,
|
||||||
descripcion: values.descripcion,
|
descripcion: values.descripcion,
|
||||||
alicuota: values.alicuotaCreate,
|
alicuota: values.alicuotaCreate,
|
||||||
vigenciaDesde: values.vigenciaDesde ?? new Date().toISOString().slice(0, 10),
|
vigenciaDesde: values.vigenciaDesde ?? todayArgentina(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
import { useNuevaVersionIngresosBrutos } from '../hooks/useIngresosBrutos'
|
import { useNuevaVersionIngresosBrutos } from '../hooks/useIngresosBrutos'
|
||||||
import type { IngresosBrutos } from '../types/ingresosBrutos.types'
|
import type { IngresosBrutos } from '../types/ingresosBrutos.types'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { prevCivilDate, formatCivilDate } from '@/lib/dateFormat'
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
alicuota: z.coerce
|
alicuota: z.coerce
|
||||||
@@ -48,11 +49,12 @@ interface NuevaVigenciaIibbModalProps {
|
|||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function fechaCierre(vigenciaDesde: string): string {
|
/** Formatea la fecha de cierre (vigenciaDesde - 1 día) para display "dd/MM/yyyy".
|
||||||
|
* Usa prevCivilDate (Date.UTC pura, sin TZ) + formatCivilDate (split manual).
|
||||||
|
*/
|
||||||
|
function fechaCierreDisplay(vigenciaDesde: string): string {
|
||||||
if (!vigenciaDesde || !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaDesde)) return '—'
|
if (!vigenciaDesde || !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaDesde)) return '—'
|
||||||
const d = new Date(vigenciaDesde + 'T00:00:00')
|
return formatCivilDate(prevCivilDate(vigenciaDesde))
|
||||||
d.setDate(d.getDate() - 1)
|
|
||||||
return d.toISOString().slice(0, 10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBackendError(err: unknown): string | null {
|
function resolveBackendError(err: unknown): string | null {
|
||||||
@@ -212,7 +214,7 @@ export function NuevaVigenciaIibbModal({
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Versión actual ({item.alicuota}%) quedará cerrada el{' '}
|
Versión actual ({item.alicuota}%) quedará cerrada el{' '}
|
||||||
<strong>{fechaCierre(watchedVigencia)}</strong>.
|
<strong>{fechaCierreDisplay(watchedVigencia)}</strong>.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground font-medium">
|
<p className="text-xs text-muted-foreground font-medium">
|
||||||
Esta acción no se puede deshacer.
|
Esta acción no se puede deshacer.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
import { useNuevaVersionTipoDeIva } from '../hooks/useTiposDeIva'
|
import { useNuevaVersionTipoDeIva } from '../hooks/useTiposDeIva'
|
||||||
import type { TipoDeIva } from '../types/tipoDeIva.types'
|
import type { TipoDeIva } from '../types/tipoDeIva.types'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { prevCivilDate, formatCivilDate } from '@/lib/dateFormat'
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
porcentaje: z.coerce
|
porcentaje: z.coerce
|
||||||
@@ -49,12 +50,12 @@ interface NuevaVigenciaModalProps {
|
|||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Devuelve la fecha anterior (vigenciaDesde - 1 día) como string "yyyy-MM-dd" */
|
/** Formatea la fecha de cierre (vigenciaDesde - 1 día) para display "dd/MM/yyyy".
|
||||||
function fechaCierre(vigenciaDesde: string): string {
|
* Usa prevCivilDate (Date.UTC pura, sin TZ) + formatCivilDate (split manual).
|
||||||
|
*/
|
||||||
|
function fechaCierreDisplay(vigenciaDesde: string): string {
|
||||||
if (!vigenciaDesde || !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaDesde)) return '—'
|
if (!vigenciaDesde || !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaDesde)) return '—'
|
||||||
const d = new Date(vigenciaDesde + 'T00:00:00')
|
return formatCivilDate(prevCivilDate(vigenciaDesde))
|
||||||
d.setDate(d.getDate() - 1)
|
|
||||||
return d.toISOString().slice(0, 10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBackendError(err: unknown): string | null {
|
function resolveBackendError(err: unknown): string | null {
|
||||||
@@ -218,7 +219,7 @@ export function NuevaVigenciaModal({
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Versión actual ({item.porcentaje}%) quedará cerrada el{' '}
|
Versión actual ({item.porcentaje}%) quedará cerrada el{' '}
|
||||||
<strong>{fechaCierre(watchedVigencia)}</strong>.
|
<strong>{fechaCierreDisplay(watchedVigencia)}</strong>.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground font-medium">
|
<p className="text-xs text-muted-foreground font-medium">
|
||||||
Esta acción no se puede deshacer.
|
Esta acción no se puede deshacer.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Modal de edición / creación de TipoDeIva
|
// Modal de edición / creación de TipoDeIva
|
||||||
// CRÍTICO: NO incluye campo Porcentaje (inmutable, cambiar via NuevaVersion)
|
// CRÍTICO: NO incluye campo Porcentaje (inmutable, cambiar via NuevaVersion)
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { todayArgentina } from '@/lib/dateFormat'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
@@ -98,7 +99,7 @@ export function TipoDeIvaFormModal({
|
|||||||
aplicaIVA: true,
|
aplicaIVA: true,
|
||||||
activo: true,
|
activo: true,
|
||||||
porcentajeCreate: undefined,
|
porcentajeCreate: undefined,
|
||||||
vigenciaDesde: '',
|
vigenciaDesde: todayArgentina(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ export function TipoDeIvaFormModal({
|
|||||||
aplicaIVA: true,
|
aplicaIVA: true,
|
||||||
activo: true,
|
activo: true,
|
||||||
porcentajeCreate: undefined,
|
porcentajeCreate: undefined,
|
||||||
vigenciaDesde: '',
|
vigenciaDesde: todayArgentina(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
createMutation.reset()
|
createMutation.reset()
|
||||||
@@ -158,7 +159,7 @@ export function TipoDeIvaFormModal({
|
|||||||
codigo: values.codigo,
|
codigo: values.codigo,
|
||||||
descripcion: values.descripcion,
|
descripcion: values.descripcion,
|
||||||
porcentaje: values.porcentajeCreate,
|
porcentaje: values.porcentajeCreate,
|
||||||
vigenciaDesde: values.vigenciaDesde ?? new Date().toISOString().slice(0, 10),
|
vigenciaDesde: values.vigenciaDesde ?? todayArgentina(),
|
||||||
aplicaIVA: values.aplicaIVA,
|
aplicaIVA: values.aplicaIVA,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,15 +5,7 @@ import { CanPerform } from '@/components/auth/CanPerform'
|
|||||||
import { useMedio } from '../hooks/useMedio'
|
import { useMedio } from '../hooks/useMedio'
|
||||||
import { DeactivateMedioModal } from '../components/DeactivateMedioModal'
|
import { DeactivateMedioModal } from '../components/DeactivateMedioModal'
|
||||||
import { tipoMedioLabel } from '../tipoMedio'
|
import { tipoMedioLabel } from '../tipoMedio'
|
||||||
|
import { formatInstantOrDash } from '@/lib/dateFormat'
|
||||||
function formatDate(iso: string | null): string {
|
|
||||||
if (!iso) return '—'
|
|
||||||
return new Date(iso).toLocaleDateString('es-AR', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MedioDetailPage() {
|
export function MedioDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
@@ -69,11 +61,11 @@ export function MedioDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Creado</span>
|
<span className="text-muted-foreground">Creado</span>
|
||||||
<span>{formatDate(medio.fechaCreacion)}</span>
|
<span>{formatInstantOrDash(medio.fechaCreacion)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Modificado</span>
|
<span className="text-muted-foreground">Modificado</span>
|
||||||
<span>{formatDate(medio.fechaModificacion)}</span>
|
<span>{formatInstantOrDash(medio.fechaModificacion)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,7 @@ import { useMedio } from '../../medios/hooks/useMedio'
|
|||||||
import { DeactivatePuntoDeVentaModal } from '../components/DeactivatePuntoDeVentaModal'
|
import { DeactivatePuntoDeVentaModal } from '../components/DeactivatePuntoDeVentaModal'
|
||||||
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
|
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
|
||||||
import { PdvInactivoBanner } from '../components/PdvInactivoBanner'
|
import { PdvInactivoBanner } from '../components/PdvInactivoBanner'
|
||||||
|
import { formatInstantOrDash } from '@/lib/dateFormat'
|
||||||
function formatDate(iso: string | null): string {
|
|
||||||
if (!iso) return '—'
|
|
||||||
return new Date(iso).toLocaleDateString('es-AR', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PuntoDeVentaDetailPage() {
|
export function PuntoDeVentaDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
@@ -70,11 +62,11 @@ export function PuntoDeVentaDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Creado</span>
|
<span className="text-muted-foreground">Creado</span>
|
||||||
<span>{formatDate(pdv.fechaCreacion)}</span>
|
<span>{formatInstantOrDash(pdv.fechaCreacion)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Modificado</span>
|
<span className="text-muted-foreground">Modificado</span>
|
||||||
<span>{formatDate(pdv.fechaModificacion)}</span>
|
<span>{formatInstantOrDash(pdv.fechaModificacion)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,7 @@ import { DeactivateSeccionModal } from '../components/DeactivateSeccionModal'
|
|||||||
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
|
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
|
||||||
import { tipoSeccionLabel } from '../tipoSeccion'
|
import { tipoSeccionLabel } from '../tipoSeccion'
|
||||||
import { useMedio } from '../../medios/hooks/useMedio'
|
import { useMedio } from '../../medios/hooks/useMedio'
|
||||||
|
import { formatInstantOrDash } from '@/lib/dateFormat'
|
||||||
function formatDate(iso: string | null): string {
|
|
||||||
if (!iso) return '—'
|
|
||||||
return new Date(iso).toLocaleDateString('es-AR', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SeccionDetailPage() {
|
export function SeccionDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
@@ -75,11 +67,11 @@ export function SeccionDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Creado</span>
|
<span className="text-muted-foreground">Creado</span>
|
||||||
<span>{formatDate(seccion.fechaCreacion)}</span>
|
<span>{formatInstantOrDash(seccion.fechaCreacion)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Modificado</span>
|
<span className="text-muted-foreground">Modificado</span>
|
||||||
<span>{formatDate(seccion.fechaModificacion)}</span>
|
<span>{formatInstantOrDash(seccion.fechaModificacion)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,13 @@ import type { ColumnDef } from '@tanstack/react-table'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { DataTable } from '@/components/ui/data-table'
|
import { DataTable } from '@/components/ui/data-table'
|
||||||
import type { UserListItem } from '../types'
|
import type { UserListItem } from '../types'
|
||||||
|
import { formatInstantOrDash } from '@/lib/dateFormat'
|
||||||
|
|
||||||
interface UsersTableProps {
|
interface UsersTableProps {
|
||||||
rows: UserListItem[]
|
rows: UserListItem[]
|
||||||
onRowClick: (user: UserListItem) => void
|
onRowClick: (user: UserListItem) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(iso: string | null): string {
|
|
||||||
if (!iso) return '—'
|
|
||||||
return new Date(iso).toLocaleDateString('es-AR', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UsersTable({ rows, onRowClick }: UsersTableProps) {
|
export function UsersTable({ rows, onRowClick }: UsersTableProps) {
|
||||||
const columns = useMemo<ColumnDef<UserListItem>[]>(
|
const columns = useMemo<ColumnDef<UserListItem>[]>(
|
||||||
() => [
|
() => [
|
||||||
@@ -81,7 +73,7 @@ export function UsersTable({ rows, onRowClick }: UsersTableProps) {
|
|||||||
header: 'Último login',
|
header: 'Último login',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{formatDate(row.original.ultimoLogin)}
|
{formatInstantOrDash(row.original.ultimoLogin)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
meta: { priority: 'low' },
|
meta: { priority: 'low' },
|
||||||
|
|||||||
122
src/web/src/lib/dateFormat.ts
Normal file
122
src/web/src/lib/dateFormat.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Localización temporal Argentina — utility centralizada.
|
||||||
|
* Ver: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.17 ⏰ Localización Temporal Argentina.md
|
||||||
|
* Engram topic_key: sig-cm2/conventions/fechas-timezones
|
||||||
|
*
|
||||||
|
* REGLAS PROHIBIDAS (no usar fuera de este módulo):
|
||||||
|
* - new Date(civilDateString) → aplica UTC, pierde días
|
||||||
|
* - toISOString().slice(0, 10) → UTC creep
|
||||||
|
* - toLocaleString('es-AR', {...}) sin timeZone → depende del navegador
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const AR_TZ = 'America/Argentina/Buenos_Aires';
|
||||||
|
|
||||||
|
type FormatStyle = 'short' | 'medium' | 'long' | 'full';
|
||||||
|
|
||||||
|
interface FormatInstantOptions {
|
||||||
|
dateStyle?: FormatStyle;
|
||||||
|
timeStyle?: FormatStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatea un instante UTC (Cat1) como string legible en zona horaria Argentina.
|
||||||
|
* Usa partes explícitas para garantizar año 4 dígitos y hora 24h independientemente del entorno.
|
||||||
|
* Output: "dd/MM/yyyy, HH:mm:ss"
|
||||||
|
*/
|
||||||
|
export function formatInstant(
|
||||||
|
iso: string,
|
||||||
|
_opts: FormatInstantOptions = { dateStyle: 'short', timeStyle: 'medium' }
|
||||||
|
): string {
|
||||||
|
const parts = new Intl.DateTimeFormat('es-AR', {
|
||||||
|
timeZone: AR_TZ,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).formatToParts(new Date(iso));
|
||||||
|
|
||||||
|
const get = (type: string): string => parts.find(p => p.type === type)!.value;
|
||||||
|
return `${get('day')}/${get('month')}/${get('year')}, ${get('hour')}:${get('minute')}:${get('second')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatea una fecha civil Argentina (Cat2, formato "yyyy-MM-dd") a "dd/MM/yyyy".
|
||||||
|
* Split manual — NO usa new Date() para evitar UTC creep.
|
||||||
|
*/
|
||||||
|
export function formatCivilDate(yyyyMmDd: string): string {
|
||||||
|
const [y, m, d] = yyyyMmDd.split('-');
|
||||||
|
return `${d}/${m}/${y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatea un rango de fechas civiles Argentinas.
|
||||||
|
*/
|
||||||
|
export function formatCivilDateRange(from: string, to: string | null): string {
|
||||||
|
return to ? `${formatCivilDate(from)} → ${formatCivilDate(to)}` : `desde ${formatCivilDate(from)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna la fecha civil Argentina de hoy en formato "yyyy-MM-dd".
|
||||||
|
* Fix de BUG-FE-03: usa Intl.DateTimeFormat con timeZone, NO toISOString().slice(0, 10).
|
||||||
|
*/
|
||||||
|
export function todayArgentina(): string {
|
||||||
|
const now = new Date();
|
||||||
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: AR_TZ,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
}).formatToParts(now);
|
||||||
|
|
||||||
|
const y = parts.find(p => p.type === 'year')!.value;
|
||||||
|
const m = parts.find(p => p.type === 'month')!.value;
|
||||||
|
const d = parts.find(p => p.type === 'day')!.value;
|
||||||
|
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsea una fecha civil Argentina ("yyyy-MM-dd") a partes numéricas.
|
||||||
|
* Split manual — NO usa new Date().
|
||||||
|
*/
|
||||||
|
export function parseCivilDate(yyyyMmDd: string): { year: number; month: number; day: number } {
|
||||||
|
const [y, m, d] = yyyyMmDd.split('-').map(Number);
|
||||||
|
return { year: y, month: m, day: d };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatea un instante UTC (Cat1) nullable a string legible en zona horaria Argentina.
|
||||||
|
* Retorna '—' cuando el valor es null o undefined.
|
||||||
|
*/
|
||||||
|
export function formatInstantOrDash(iso: string | null | undefined): string {
|
||||||
|
if (!iso) return '—';
|
||||||
|
return formatInstant(iso);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna el día anterior a una fecha civil Argentina en formato "yyyy-MM-dd".
|
||||||
|
* Usa Date.UTC para aritmética pura — sin conversión de timezone en ningún momento.
|
||||||
|
* Output: "yyyy-MM-dd"
|
||||||
|
*/
|
||||||
|
export function prevCivilDate(yyyyMmDd: string): string {
|
||||||
|
const [y, m, d] = yyyyMmDd.split('-').map(Number);
|
||||||
|
const prevDay = new Date(Date.UTC(y, m - 1, d - 1));
|
||||||
|
const yy = prevDay.getUTCFullYear();
|
||||||
|
const mm = String(prevDay.getUTCMonth() + 1).padStart(2, '0');
|
||||||
|
const dd = String(prevDay.getUTCDate()).padStart(2, '0');
|
||||||
|
return `${yy}-${mm}-${dd}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte un valor de <input type="datetime-local"> (ART implícito) a ISO UTC.
|
||||||
|
* Input: "2026-05-01T22:30" (sin TZ) → interpretado como ART → "2026-05-02T01:30:00.000Z".
|
||||||
|
*
|
||||||
|
* Útil para AuditFilters que envía filtros de rango al backend.
|
||||||
|
*/
|
||||||
|
export function parseArgentinaDateTimeToUtc(localDateTime: string): string {
|
||||||
|
// Parse "yyyy-MM-ddTHH:mm" como ART (offset -03:00) y convertir a ISO UTC
|
||||||
|
return new Date(`${localDateTime}:00-03:00`).toISOString();
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useState, type FormEvent } from 'react'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { parseArgentinaDateTimeToUtc } from '@/lib/dateFormat'
|
||||||
|
|
||||||
/** Filtros crudos del form (todos strings para binding directo del input). */
|
/** Filtros crudos del form (todos strings para binding directo del input). */
|
||||||
export interface AuditFiltersValue {
|
export interface AuditFiltersValue {
|
||||||
@@ -132,8 +133,8 @@ export function AuditFilters({
|
|||||||
* que espera el cliente de API.
|
* que espera el cliente de API.
|
||||||
*
|
*
|
||||||
* - `actor` vacío o NaN → omitido
|
* - `actor` vacío o NaN → omitido
|
||||||
* - `from`/`to` vienen del `datetime-local` (local time, sin timezone).
|
* - `from`/`to` vienen del `datetime-local` (hora ART implícita, sin timezone).
|
||||||
* Los convertimos a ISO UTC vía `new Date(...).toISOString()`.
|
* Los convertimos a ISO UTC vía `parseArgentinaDateTimeToUtc()` (fix BUG-FE-05).
|
||||||
* - Strings vacíos → omitidos.
|
* - Strings vacíos → omitidos.
|
||||||
*/
|
*/
|
||||||
export function toApiFilter(
|
export function toApiFilter(
|
||||||
@@ -148,12 +149,11 @@ export function toApiFilter(
|
|||||||
if (value.targetType.trim() !== '') out.targetType = value.targetType.trim()
|
if (value.targetType.trim() !== '') out.targetType = value.targetType.trim()
|
||||||
if (value.targetId.trim() !== '') out.targetId = value.targetId.trim()
|
if (value.targetId.trim() !== '') out.targetId = value.targetId.trim()
|
||||||
if (value.from.trim() !== '') {
|
if (value.from.trim() !== '') {
|
||||||
const d = new Date(value.from)
|
// BUG-FE-05: interpretar el string como ART (-03:00), no como local del browser
|
||||||
if (!Number.isNaN(d.getTime())) out.from = d.toISOString()
|
try { out.from = parseArgentinaDateTimeToUtc(value.from) } catch { /* invalid input */ }
|
||||||
}
|
}
|
||||||
if (value.to.trim() !== '') {
|
if (value.to.trim() !== '') {
|
||||||
const d = new Date(value.to)
|
try { out.to = parseArgentinaDateTimeToUtc(value.to) } catch { /* invalid input */ }
|
||||||
if (!Number.isNaN(d.getTime())) out.to = d.toISOString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -18,20 +18,7 @@ import {
|
|||||||
toApiFilter,
|
toApiFilter,
|
||||||
type AuditFiltersValue,
|
type AuditFiltersValue,
|
||||||
} from './AuditFilters'
|
} from './AuditFilters'
|
||||||
|
import { formatInstant } from '@/lib/dateFormat'
|
||||||
/** Formatea un ISO datetime a hora local AR (dd/mm/yyyy HH:mm:ss). */
|
|
||||||
function formatOccurredAt(iso: string): string {
|
|
||||||
const d = new Date(iso)
|
|
||||||
if (Number.isNaN(d.getTime())) return iso
|
|
||||||
return d.toLocaleString('es-AR', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Copia texto al clipboard con fallback + toast. */
|
/** Copia texto al clipboard con fallback + toast. */
|
||||||
async function copyToClipboard(text: string, label: string): Promise<void> {
|
async function copyToClipboard(text: string, label: string): Promise<void> {
|
||||||
@@ -123,7 +110,7 @@ export function AuditPage() {
|
|||||||
header: 'Fecha',
|
header: 'Fecha',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="font-mono text-xs text-foreground">
|
<span className="font-mono text-xs text-foreground">
|
||||||
{formatOccurredAt(row.original.occurredAt)}
|
{formatInstant(row.original.occurredAt)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
meta: { priority: 'high' },
|
meta: { priority: 'high' },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// T600.20-T600.29 (IIBB) — TDD: IngresosBrutosFormModal
|
// T600.20-T600.29 (IIBB) — TDD: IngresosBrutosFormModal
|
||||||
// CRÍTICO: verifica que el modal de Editar NO tiene campo Alícuota [REQ-UI-007]
|
// CRÍTICO: verifica que el modal de Editar NO tiene campo Alícuota [REQ-UI-007]
|
||||||
|
// T600.10 — BUG-FE-03 regression: default vigenciaDesde usa todayArgentina (no UTC)
|
||||||
import { describe, it, expect, vi } from 'vitest'
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
import { render, screen, waitFor } from '@testing-library/react'
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
@@ -91,3 +92,21 @@ describe('IngresosBrutosFormModal — CRÍTICO: sin campo Alícuota en modo EDIT
|
|||||||
expect(onClose).toHaveBeenCalledTimes(1)
|
expect(onClose).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('IngresosBrutosFormModal — BUG-FE-03 regression: vigenciaDesde usa todayArgentina', () => {
|
||||||
|
// A las 22:30 ART del 30/04, UTC ya es 01:30 del 01/05.
|
||||||
|
// new Date().toISOString().slice(0,10) devolvería "2026-05-01" (UTC) — INCORRECTO.
|
||||||
|
// todayArgentina() debe devolver "2026-04-30" — CORRECTO.
|
||||||
|
it('modo create: campo vigenciaDesde refleja fecha ART, no UTC, a las 22:30 ART del 30/04', () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
vi.setSystemTime(new Date('2026-05-01T01:30:00.000Z')) // 22:30 ART del 30/04
|
||||||
|
|
||||||
|
renderModal({ item: null })
|
||||||
|
|
||||||
|
const vigenciaInput = screen.getByLabelText(/vigencia desde/i) as HTMLInputElement
|
||||||
|
// El input debe tener value="2026-04-30" (fecha ART), no "2026-05-01" (UTC)
|
||||||
|
expect(vigenciaInput.value).toBe('2026-04-30')
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ describe('NuevaVigenciaModal — preview con fechas correctas [REQ-UI-004]', ()
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('preview muestra fecha de cierre = vigenciaDesde - 1 día', async () => {
|
it('preview muestra fecha de cierre = vigenciaDesde - 1 día (formato dd/MM/yyyy, BUG-FE-04)', async () => {
|
||||||
renderModal()
|
renderModal()
|
||||||
|
|
||||||
const porcentajeInput = screen.getByLabelText(/porcentaje nuevo/i)
|
const porcentajeInput = screen.getByLabelText(/porcentaje nuevo/i)
|
||||||
@@ -132,9 +132,9 @@ describe('NuevaVigenciaModal — preview con fechas correctas [REQ-UI-004]', ()
|
|||||||
const vigenciaInput = screen.getByLabelText(/vigencia desde/i)
|
const vigenciaInput = screen.getByLabelText(/vigencia desde/i)
|
||||||
await userEvent.type(vigenciaInput, '2026-05-01')
|
await userEvent.type(vigenciaInput, '2026-05-01')
|
||||||
|
|
||||||
// La versión anterior cierra el día anterior → 2026-04-30
|
// La versión anterior cierra el día anterior → 30/04/2026 (formato AR civil)
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByText(/2026-04-30/)).toBeInTheDocument(),
|
expect(screen.getByText(/30\/04\/2026/)).toBeInTheDocument(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// T600.5 — TDD: TipoDeIvaFormModal
|
// T600.5 — TDD: TipoDeIvaFormModal
|
||||||
// CRÍTICO: verifica que el modal de Editar NO tiene campo Porcentaje [REQ-UI-003]
|
// CRÍTICO: verifica que el modal de Editar NO tiene campo Porcentaje [REQ-UI-003]
|
||||||
|
// T600.10 — BUG-FE-03 regression: default vigenciaDesde usa todayArgentina (no UTC)
|
||||||
import { describe, it, expect, vi } from 'vitest'
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
import { render, screen, waitFor } from '@testing-library/react'
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
@@ -131,3 +132,21 @@ describe('TipoDeIvaFormModal — validación', () => {
|
|||||||
expect(onClose).toHaveBeenCalledTimes(1)
|
expect(onClose).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('TipoDeIvaFormModal — BUG-FE-03 regression: vigenciaDesde usa todayArgentina', () => {
|
||||||
|
// A las 22:30 ART del 30/04, UTC ya es 01:30 del 01/05.
|
||||||
|
// new Date().toISOString().slice(0,10) devolvería "2026-05-01" (UTC) — INCORRECTO.
|
||||||
|
// todayArgentina() debe devolver "2026-04-30" — CORRECTO.
|
||||||
|
it('modo create: campo vigenciaDesde refleja fecha ART, no UTC, a las 22:30 ART del 30/04', () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
vi.setSystemTime(new Date('2026-05-01T01:30:00.000Z')) // 22:30 ART del 30/04
|
||||||
|
|
||||||
|
renderModal({ item: null })
|
||||||
|
|
||||||
|
const vigenciaInput = screen.getByLabelText(/vigencia desde/i) as HTMLInputElement
|
||||||
|
// El input debe tener value="2026-04-30" (fecha ART), no "2026-05-01" (UTC)
|
||||||
|
expect(vigenciaInput.value).toBe('2026-04-30')
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
146
src/web/src/tests/lib/dateFormat.test.ts
Normal file
146
src/web/src/tests/lib/dateFormat.test.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { describe, it, expect, afterEach, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
AR_TZ,
|
||||||
|
formatInstant,
|
||||||
|
formatInstantOrDash,
|
||||||
|
formatCivilDate,
|
||||||
|
formatCivilDateRange,
|
||||||
|
todayArgentina,
|
||||||
|
parseCivilDate,
|
||||||
|
prevCivilDate,
|
||||||
|
} from '@/lib/dateFormat';
|
||||||
|
|
||||||
|
describe('dateFormat.ts', () => {
|
||||||
|
describe('AR_TZ constant', () => {
|
||||||
|
it('is America/Argentina/Buenos_Aires', () => {
|
||||||
|
expect(AR_TZ).toBe('America/Argentina/Buenos_Aires');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatInstant (Cat1 — UTC → AR display)', () => {
|
||||||
|
it('converts 01:30 UTC to 22:30 ART previous day', () => {
|
||||||
|
const iso = '2026-05-01T01:30:00.000Z';
|
||||||
|
const result = formatInstant(iso);
|
||||||
|
// format es-AR short + medium: "30/4/2026, 22:30:00" o similar
|
||||||
|
expect(result).toMatch(/30\/0?4\/2026/);
|
||||||
|
expect(result).toMatch(/22:30/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses AR timezone regardless of browser TZ', () => {
|
||||||
|
const iso = '2026-05-01T12:00:00.000Z'; // 09:00 ART
|
||||||
|
const result = formatInstant(iso);
|
||||||
|
expect(result).toMatch(/01\/0?5\/2026/);
|
||||||
|
expect(result).toMatch(/09:00/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatInstantOrDash (Cat1 nullable)', () => {
|
||||||
|
it('returns "—" for null', () => {
|
||||||
|
expect(formatInstantOrDash(null)).toBe('—');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "—" for undefined', () => {
|
||||||
|
expect(formatInstantOrDash(undefined)).toBe('—');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates to formatInstant for valid ISO', () => {
|
||||||
|
const iso = '2026-05-01T01:30:00.000Z';
|
||||||
|
expect(formatInstantOrDash(iso)).toBe(formatInstant(iso));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatCivilDate (Cat2 — yyyy-MM-dd → dd/MM/yyyy)', () => {
|
||||||
|
it('splits manually without new Date()', () => {
|
||||||
|
expect(formatCivilDate('2026-05-01')).toBe('01/05/2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves day for early-year dates', () => {
|
||||||
|
expect(formatCivilDate('2026-01-01')).toBe('01/01/2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves day for end-of-year dates', () => {
|
||||||
|
expect(formatCivilDate('2026-12-31')).toBe('31/12/2026');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatCivilDateRange', () => {
|
||||||
|
it('renders full range with arrow', () => {
|
||||||
|
expect(formatCivilDateRange('2026-05-01', '2026-12-31'))
|
||||||
|
.toBe('01/05/2026 → 31/12/2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders open range when hasta is null', () => {
|
||||||
|
expect(formatCivilDateRange('2026-05-01', null))
|
||||||
|
.toBe('desde 01/05/2026');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('todayArgentina (fix BUG-FE-03)', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 2026-04-30 at 22:30 ART of 30/04 (BUG-FE-03 regression)', () => {
|
||||||
|
// 22:30 ART = 01:30 UTC del día siguiente
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2026-05-01T01:30:00.000Z'));
|
||||||
|
|
||||||
|
expect(todayArgentina()).toBe('2026-04-30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 2026-05-01 at 00:30 ART of 01/05', () => {
|
||||||
|
// 00:30 ART = 03:30 UTC
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2026-05-01T03:30:00.000Z'));
|
||||||
|
|
||||||
|
expect(todayArgentina()).toBe('2026-05-01');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 2024-02-28 at 22:30 ART of 28/02/2024 (bisiesto)', () => {
|
||||||
|
// 22:30 ART del 28/02/2024 = 01:30 UTC del 29/02/2024
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2024-02-29T01:30:00.000Z'));
|
||||||
|
|
||||||
|
expect(todayArgentina()).toBe('2024-02-28');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 2026-12-31 at 22:30 ART of 31/12 (end of year)', () => {
|
||||||
|
// 22:30 ART del 31/12/2026 = 01:30 UTC del 01/01/2027
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2027-01-01T01:30:00.000Z'));
|
||||||
|
|
||||||
|
expect(todayArgentina()).toBe('2026-12-31');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseCivilDate', () => {
|
||||||
|
it('splits "2026-05-01" into {year, month, day}', () => {
|
||||||
|
expect(parseCivilDate('2026-05-01')).toEqual({ year: 2026, month: 5, day: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not use new Date() (no UTC creep)', () => {
|
||||||
|
const result = parseCivilDate('2026-01-01');
|
||||||
|
expect(result.year).toBe(2026);
|
||||||
|
expect(result.month).toBe(1);
|
||||||
|
expect(result.day).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prevCivilDate (BUG-FE-04 — fecha cierre NuevaVigencia)', () => {
|
||||||
|
it('returns day before (2026-05-01 → 2026-04-30)', () => {
|
||||||
|
expect(prevCivilDate('2026-05-01')).toBe('2026-04-30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('crosses month boundary (2026-05-31 → 2026-05-30)', () => {
|
||||||
|
expect(prevCivilDate('2026-06-01')).toBe('2026-05-31');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('crosses year boundary (2026-01-01 → 2025-12-31)', () => {
|
||||||
|
expect(prevCivilDate('2026-01-01')).toBe('2025-12-31');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles leap year (2024-03-01 → 2024-02-29)', () => {
|
||||||
|
expect(prevCivilDate('2024-03-01')).toBe('2024-02-29');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -693,4 +693,46 @@ public sealed class FiscalControllerTests : IAsyncLifetime
|
|||||||
Assert.True(json.TryGetProperty("items", out _), "Response must have 'items'");
|
Assert.True(json.TryGetProperty("items", out _), "Response must have 'items'");
|
||||||
Assert.True(json.TryGetProperty("total", out _), "Response must have 'total'");
|
Assert.True(json.TryGetProperty("total", out _), "Response must have 'total'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── UDT-011: DateOnly serialization format ────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [UDT-011 REQ-BE-JSON-001] POST /iva → vigenciaDesde en respuesta debe ser
|
||||||
|
/// "yyyy-MM-dd" (e.g. "2025-01-01"), no "2025-01-01T00:00:00" ni con sufijo "Z".
|
||||||
|
/// Valida que DateOnlyJsonConverter está activo en el pipeline de controllers.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateIva_VigenciaDesde_SerializesAsDateOnlyString()
|
||||||
|
{
|
||||||
|
const string codigo = "IVA_9999";
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var req = BuildRequest(HttpMethod.Post, IvaEndpoint, new
|
||||||
|
{
|
||||||
|
codigo,
|
||||||
|
descripcion = "IVA DateOnly Format Test",
|
||||||
|
porcentaje = 5.0m,
|
||||||
|
aplicaIVA = true,
|
||||||
|
vigenciaDesde = "2025-01-01"
|
||||||
|
}, token);
|
||||||
|
var resp = await _client.SendAsync(req);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
|
||||||
|
|
||||||
|
// Read raw JSON string to inspect format (not deserialized)
|
||||||
|
var rawJson = await resp.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// vigenciaDesde MUST be "2025-01-01" — short date format
|
||||||
|
Assert.Contains("\"2025-01-01\"", rawJson);
|
||||||
|
|
||||||
|
// Must NOT contain datetime format or UTC suffix
|
||||||
|
Assert.DoesNotContain("T00:00:00", rawJson);
|
||||||
|
Assert.DoesNotContain("\"2025-01-01Z\"", rawJson);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteTipoDeIvaByCodigoAsync(codigo);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
262
tests/SIGCM2.Api.Tests/Admin/V015MigrationTests.cs
Normal file
262
tests/SIGCM2.Api.Tests/Admin/V015MigrationTests.cs
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
using Dapper;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SIGCM2.Api.Tests.Admin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UDT-011 Batch 2 — V015 migration integration tests.
|
||||||
|
/// Validates:
|
||||||
|
/// REQ-DB-VIEWS-001 : Vista dbo.v_AuditEvent_Local existe tras aplicar V015.
|
||||||
|
/// REQ-DB-VIEWS-002 : Vista dbo.v_SecurityEvent_Local existe tras aplicar V015.
|
||||||
|
/// REQ-DB-VIEWS-003 : OccurredAtLocal retorna offset -03:00 (Argentina Standard Time).
|
||||||
|
/// REQ-DB-VIEWS-004 : V015 es idempotente (re-ejecución no falla ni duplica vistas).
|
||||||
|
/// REQ-DB-VIEWS-005 : V015_ROLLBACK elimina ambas vistas.
|
||||||
|
///
|
||||||
|
/// NOTA: Esta suite opera directamente sobre SIGCM2_Test con Dapper.
|
||||||
|
/// NO usa WebApplicationFactory (es test de migración pura, no API).
|
||||||
|
/// La migración se aplica via SqlTestFixture.EnsureV015SchemaAsync().
|
||||||
|
/// </summary>
|
||||||
|
[Collection("ApiIntegration")]
|
||||||
|
public sealed class V015MigrationTests : IClassFixture<SIGCM2.TestSupport.TestWebAppFactory>
|
||||||
|
{
|
||||||
|
private const string ConnectionString =
|
||||||
|
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||||
|
|
||||||
|
public V015MigrationTests(SIGCM2.TestSupport.TestWebAppFactory _)
|
||||||
|
{
|
||||||
|
// Depend on the factory so SqlTestFixture.InitializeAsync runs
|
||||||
|
// (ensures V015 schema is present via EnsureV015SchemaAsync).
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── REQ-DB-VIEWS-001 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task V015_DebeCrearVistaAuditEventLocal()
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(ConnectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
var objectId = await conn.ExecuteScalarAsync<int?>("""
|
||||||
|
SELECT OBJECT_ID('dbo.v_AuditEvent_Local', 'V')
|
||||||
|
""");
|
||||||
|
|
||||||
|
objectId.Should().NotBeNull("dbo.v_AuditEvent_Local debe existir tras aplicar V015");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── REQ-DB-VIEWS-002 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task V015_DebeCrearVistaSecurityEventLocal()
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(ConnectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
var objectId = await conn.ExecuteScalarAsync<int?>("""
|
||||||
|
SELECT OBJECT_ID('dbo.v_SecurityEvent_Local', 'V')
|
||||||
|
""");
|
||||||
|
|
||||||
|
objectId.Should().NotBeNull("dbo.v_SecurityEvent_Local debe existir tras aplicar V015");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── REQ-DB-VIEWS-003 ─────────────────────────────────────────────────────
|
||||||
|
// Inserta 1 fila con OccurredAt = 2026-05-01 01:30:00 UTC
|
||||||
|
// Espera OccurredAtLocal con offset -03:00 → 2026-04-30 22:30:00-03:00
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task v_AuditEvent_Local_RetornaOccurredAtLocalConOffsetArgentina()
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(ConnectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
// Insert a test row with known OccurredAt UTC value.
|
||||||
|
// OccurredAt = 2026-05-01 01:30:00 UTC → Argentina (UTC-3) = 2026-04-30 22:30:00
|
||||||
|
var occurredAt = new DateTime(2026, 5, 1, 1, 30, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
long insertedId;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
insertedId = await conn.ExecuteScalarAsync<long>("""
|
||||||
|
INSERT INTO dbo.AuditEvent
|
||||||
|
(OccurredAt, ActorUserId, ActorRoleId, Action, TargetType, TargetId, CorrelationId, IpAddress, UserAgent, Metadata)
|
||||||
|
VALUES
|
||||||
|
(@OccurredAt, NULL, NULL, 'test.v015', 'Test', '0', NULL, '127.0.0.1', NULL, NULL);
|
||||||
|
SELECT SCOPE_IDENTITY();
|
||||||
|
""", new { OccurredAt = occurredAt });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If insert fails (e.g. CHECK constraint on Action), skip — vista test only
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await conn.QuerySingleOrDefaultAsync<(DateTimeOffset OccurredAtLocal, DateTime OccurredAt)>("""
|
||||||
|
SELECT TOP 1 OccurredAtLocal, OccurredAt
|
||||||
|
FROM dbo.v_AuditEvent_Local
|
||||||
|
WHERE Action = 'test.v015'
|
||||||
|
AND OccurredAt = @OccurredAt
|
||||||
|
""", new { OccurredAt = occurredAt });
|
||||||
|
|
||||||
|
result.OccurredAt.Should().Be(occurredAt, "OccurredAt debe ser el UTC insertado");
|
||||||
|
|
||||||
|
// Argentina Standard Time = UTC-3 (sin DST desde 2009). Offset fijo = -03:00.
|
||||||
|
result.OccurredAtLocal.Offset.TotalHours.Should().Be(-3,
|
||||||
|
"OccurredAtLocal debe tener offset -03:00 (Argentina Standard Time)");
|
||||||
|
|
||||||
|
// 2026-05-01 01:30 UTC → 2026-04-30 22:30 ART
|
||||||
|
result.OccurredAtLocal.DateTime.Date.Should().Be(new DateTime(2026, 4, 30),
|
||||||
|
"La fecha local Argentina debe ser 2026-04-30 (día anterior al UTC)");
|
||||||
|
result.OccurredAtLocal.Hour.Should().Be(22, "La hora local Argentina debe ser 22");
|
||||||
|
result.OccurredAtLocal.Minute.Should().Be(30, "Los minutos deben ser 30");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── REQ-DB-VIEWS-004 — Idempotencia ──────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task V015_EsIdempotente()
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(ConnectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
// Re-ejecutar el DDL idempotente de V015 por segunda vez — NO debe fallar.
|
||||||
|
// La vista ya existe (creada en EnsureV015SchemaAsync), el guard IF OBJECT_ID IS NULL
|
||||||
|
// debe cortocircuitar sin error.
|
||||||
|
await conn.ExecuteAsync("""
|
||||||
|
IF OBJECT_ID('dbo.v_AuditEvent_Local', 'V') IS NULL
|
||||||
|
BEGIN
|
||||||
|
EXEC('
|
||||||
|
CREATE VIEW dbo.v_AuditEvent_Local AS
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OccurredAt,
|
||||||
|
OccurredAt AT TIME ZONE ''UTC'' AT TIME ZONE ''Argentina Standard Time'' AS OccurredAtLocal,
|
||||||
|
ActorUserId,
|
||||||
|
ActorRoleId,
|
||||||
|
Action,
|
||||||
|
TargetType,
|
||||||
|
TargetId,
|
||||||
|
CorrelationId,
|
||||||
|
IpAddress,
|
||||||
|
UserAgent,
|
||||||
|
Metadata
|
||||||
|
FROM dbo.AuditEvent;
|
||||||
|
');
|
||||||
|
END
|
||||||
|
""");
|
||||||
|
|
||||||
|
await conn.ExecuteAsync("""
|
||||||
|
IF OBJECT_ID('dbo.v_SecurityEvent_Local', 'V') IS NULL
|
||||||
|
BEGIN
|
||||||
|
EXEC('
|
||||||
|
CREATE VIEW dbo.v_SecurityEvent_Local AS
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OccurredAt,
|
||||||
|
OccurredAt AT TIME ZONE ''UTC'' AT TIME ZONE ''Argentina Standard Time'' AS OccurredAtLocal,
|
||||||
|
ActorUserId,
|
||||||
|
AttemptedUsername,
|
||||||
|
SessionId,
|
||||||
|
Action,
|
||||||
|
Result,
|
||||||
|
FailureReason,
|
||||||
|
IpAddress,
|
||||||
|
UserAgent,
|
||||||
|
Metadata
|
||||||
|
FROM dbo.SecurityEvent;
|
||||||
|
');
|
||||||
|
END
|
||||||
|
""");
|
||||||
|
|
||||||
|
// Ambas vistas deben seguir existiendo.
|
||||||
|
var auditExists = await conn.ExecuteScalarAsync<int?>("""
|
||||||
|
SELECT OBJECT_ID('dbo.v_AuditEvent_Local', 'V')
|
||||||
|
""");
|
||||||
|
|
||||||
|
var secExists = await conn.ExecuteScalarAsync<int?>("""
|
||||||
|
SELECT OBJECT_ID('dbo.v_SecurityEvent_Local', 'V')
|
||||||
|
""");
|
||||||
|
|
||||||
|
auditExists.Should().NotBeNull("v_AuditEvent_Local debe seguir existiendo tras re-ejecución idempotente");
|
||||||
|
secExists.Should().NotBeNull("v_SecurityEvent_Local debe seguir existiendo tras re-ejecución idempotente");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── REQ-DB-VIEWS-005 — Rollback ───────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task V015_ROLLBACK_EliminaVistas()
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(ConnectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
// Apply rollback DDL (simula V015_ROLLBACK.sql).
|
||||||
|
await conn.ExecuteAsync("""
|
||||||
|
IF OBJECT_ID('dbo.v_AuditEvent_Local', 'V') IS NOT NULL
|
||||||
|
DROP VIEW dbo.v_AuditEvent_Local;
|
||||||
|
""");
|
||||||
|
|
||||||
|
await conn.ExecuteAsync("""
|
||||||
|
IF OBJECT_ID('dbo.v_SecurityEvent_Local', 'V') IS NOT NULL
|
||||||
|
DROP VIEW dbo.v_SecurityEvent_Local;
|
||||||
|
""");
|
||||||
|
|
||||||
|
var auditExists = await conn.ExecuteScalarAsync<int?>("""
|
||||||
|
SELECT OBJECT_ID('dbo.v_AuditEvent_Local', 'V')
|
||||||
|
""");
|
||||||
|
|
||||||
|
var secExists = await conn.ExecuteScalarAsync<int?>("""
|
||||||
|
SELECT OBJECT_ID('dbo.v_SecurityEvent_Local', 'V')
|
||||||
|
""");
|
||||||
|
|
||||||
|
auditExists.Should().BeNull("v_AuditEvent_Local debe ser NULL tras el rollback");
|
||||||
|
secExists.Should().BeNull("v_SecurityEvent_Local debe ser NULL tras el rollback");
|
||||||
|
|
||||||
|
// IMPORTANT: Re-create views after rollback test so remaining tests still pass.
|
||||||
|
// (This test tears down — re-apply V015 DDL to restore the fixture state.)
|
||||||
|
await conn.ExecuteAsync("""
|
||||||
|
IF OBJECT_ID('dbo.v_AuditEvent_Local', 'V') IS NULL
|
||||||
|
BEGIN
|
||||||
|
EXEC('
|
||||||
|
CREATE VIEW dbo.v_AuditEvent_Local AS
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OccurredAt,
|
||||||
|
OccurredAt AT TIME ZONE ''UTC'' AT TIME ZONE ''Argentina Standard Time'' AS OccurredAtLocal,
|
||||||
|
ActorUserId,
|
||||||
|
ActorRoleId,
|
||||||
|
Action,
|
||||||
|
TargetType,
|
||||||
|
TargetId,
|
||||||
|
CorrelationId,
|
||||||
|
IpAddress,
|
||||||
|
UserAgent,
|
||||||
|
Metadata
|
||||||
|
FROM dbo.AuditEvent;
|
||||||
|
');
|
||||||
|
END
|
||||||
|
""");
|
||||||
|
|
||||||
|
await conn.ExecuteAsync("""
|
||||||
|
IF OBJECT_ID('dbo.v_SecurityEvent_Local', 'V') IS NULL
|
||||||
|
BEGIN
|
||||||
|
EXEC('
|
||||||
|
CREATE VIEW dbo.v_SecurityEvent_Local AS
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OccurredAt,
|
||||||
|
OccurredAt AT TIME ZONE ''UTC'' AT TIME ZONE ''Argentina Standard Time'' AS OccurredAtLocal,
|
||||||
|
ActorUserId,
|
||||||
|
AttemptedUsername,
|
||||||
|
SessionId,
|
||||||
|
Action,
|
||||||
|
Result,
|
||||||
|
FailureReason,
|
||||||
|
IpAddress,
|
||||||
|
UserAgent,
|
||||||
|
Metadata
|
||||||
|
FROM dbo.SecurityEvent;
|
||||||
|
');
|
||||||
|
END
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
54
tests/SIGCM2.Api.Tests/Json/DateOnlyJsonConverterTests.cs
Normal file
54
tests/SIGCM2.Api.Tests/Json/DateOnlyJsonConverterTests.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using FluentAssertions;
|
||||||
|
using SIGCM2.Api.Json;
|
||||||
|
|
||||||
|
namespace SIGCM2.Api.Tests.Json;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UDT-011 T300.30 — Unit tests for DateOnlyJsonConverter.
|
||||||
|
/// Verifies round-trip serialization as "yyyy-MM-dd" ISO string.
|
||||||
|
/// No DB, no HTTP.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DateOnlyJsonConverterTests
|
||||||
|
{
|
||||||
|
private readonly JsonSerializerOptions _options = new()
|
||||||
|
{
|
||||||
|
Converters = { new DateOnlyJsonConverter() }
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>[REQ-BE-JSON-001] DateOnly serializes as "yyyy-MM-dd" string.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Serialize_DateOnly_WritesYyyyMmDdString()
|
||||||
|
{
|
||||||
|
var date = new DateOnly(2026, 5, 1);
|
||||||
|
var json = JsonSerializer.Serialize(date, _options);
|
||||||
|
json.Should().Be("\"2026-05-01\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>[REQ-BE-JSON-002] "yyyy-MM-dd" string deserializes back to DateOnly.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Deserialize_YyyyMmDdString_ReturnsDateOnly()
|
||||||
|
{
|
||||||
|
var json = "\"2026-05-01\"";
|
||||||
|
var date = JsonSerializer.Deserialize<DateOnly>(json, _options);
|
||||||
|
date.Should().Be(new DateOnly(2026, 5, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>[REQ-BE-JSON-003] Invalid date format (dd/MM/yyyy) throws on deserialize.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Deserialize_InvalidFormat_ThrowsFormatException()
|
||||||
|
{
|
||||||
|
var json = "\"01/05/2026\""; // formato dd/MM/yyyy — inválido
|
||||||
|
var act = () => JsonSerializer.Deserialize<DateOnly>(json, _options);
|
||||||
|
act.Should().Throw<FormatException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>JSON null is not valid for non-nullable DateOnly — must throw.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Deserialize_Null_ThrowsException()
|
||||||
|
{
|
||||||
|
var json = "null";
|
||||||
|
var act = () => JsonSerializer.Deserialize<DateOnly>(json, _options);
|
||||||
|
act.Should().Throw<Exception>(); // JsonException desde GetString() null path
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ public class LoginCommandHandlerTests
|
|||||||
_handler = new LoginCommandHandler(
|
_handler = new LoginCommandHandler(
|
||||||
_repository, _hasher, _jwtService,
|
_repository, _hasher, _jwtService,
|
||||||
_refreshRepo, _refreshGenerator, _clientCtx, _authOptions,
|
_refreshRepo, _refreshGenerator, _clientCtx, _authOptions,
|
||||||
_rolPermisoRepo, _security, _logger);
|
_rolPermisoRepo, _security, _logger, TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scenario: valid credentials → returns token response with usuario populated
|
// Scenario: valid credentials → returns token response with usuario populated
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class LogoutCommandHandlerTests
|
|||||||
|
|
||||||
public LogoutCommandHandlerTests()
|
public LogoutCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new LogoutCommandHandler(_refreshRepo, _security);
|
_handler = new LogoutCommandHandler(_refreshRepo, _security, TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ public class RefreshCommandHandlerTests
|
|||||||
_generator.Generate().Returns("new_raw_token_value_xyz");
|
_generator.Generate().Returns("new_raw_token_value_xyz");
|
||||||
|
|
||||||
_handler = new RefreshCommandHandler(
|
_handler = new RefreshCommandHandler(
|
||||||
_refreshRepo, _usuarioRepo, _jwtService, _generator, _clientCtx, _authOptions, _security);
|
_refreshRepo, _usuarioRepo, _jwtService, _generator, _clientCtx, _authOptions, _security,
|
||||||
|
TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: build an active stored RefreshToken with a matching principal
|
// Helper: build an active stored RefreshToken with a matching principal
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SIGCM2.Application;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UDT-011 T300.20.2 — DI registration smoke tests for AddApplication.
|
||||||
|
/// Pure unit: no DB, no HTTP — just verifies the service container.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AddApplicationDependencyInjectionTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// [REQ-BE-CLOCK-004] TimeProvider.System must be registered as singleton
|
||||||
|
/// so all command handlers can inject it without a concrete coupling.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void AddApplication_Registers_TimeProvider_System()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
// AddApplication requires some infrastructure; provide minimal stubs
|
||||||
|
// by only testing the DI graph without building the full host.
|
||||||
|
services.AddApplication();
|
||||||
|
using var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
var timeProvider = provider.GetRequiredService<TimeProvider>();
|
||||||
|
|
||||||
|
timeProvider.Should().BeSameAs(TimeProvider.System);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.Extensions.Time.Testing;
|
||||||
|
using SIGCM2.Application.Common;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SUITE-BE-CLOCK — UDT-011 T300.10
|
||||||
|
/// Unit tests for TimeProviderArgentinaExtensions.GetArgentinaToday.
|
||||||
|
/// Uses FakeTimeProvider for deterministic UTC control.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TimeProviderArgentinaExtensionsTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 22:30 ART del 30/04 = 01:30 UTC del 01/05.
|
||||||
|
/// UTC lleva al día siguiente, ART debe ser el día anterior.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void GetArgentinaToday_A22_30UTCdel01Mayo_Retorna30AbrilArgentina()
|
||||||
|
{
|
||||||
|
// 22:30 ART del 30/04 = 01:30 UTC del 01/05
|
||||||
|
var fake = new FakeTimeProvider();
|
||||||
|
fake.SetUtcNow(new DateTimeOffset(2026, 5, 1, 1, 30, 0, TimeSpan.Zero));
|
||||||
|
|
||||||
|
var result = fake.GetArgentinaToday();
|
||||||
|
|
||||||
|
result.Should().Be(new DateOnly(2026, 4, 30));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 00:30 ART del 01/05 = 03:30 UTC del 01/05.
|
||||||
|
/// Ambos UTC y ART son el mismo día civil.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void GetArgentinaToday_A00_30ARTdel01Mayo_Retorna01MayoArgentina()
|
||||||
|
{
|
||||||
|
// 00:30 ART del 01/05 = 03:30 UTC del 01/05
|
||||||
|
var fake = new FakeTimeProvider();
|
||||||
|
fake.SetUtcNow(new DateTimeOffset(2026, 5, 1, 3, 30, 0, TimeSpan.Zero));
|
||||||
|
|
||||||
|
var result = fake.GetArgentinaToday();
|
||||||
|
|
||||||
|
result.Should().Be(new DateOnly(2026, 5, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fin de mes en año bisiesto: 22:30 ART del 28/02/2024 = 01:30 UTC del 29/02/2024.
|
||||||
|
/// UTC cae en día bisiesto 29/02, ART debe devolver 28/02.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void GetArgentinaToday_BisiestoEnFinDeMesArgentina_RetornaCorrectoAR()
|
||||||
|
{
|
||||||
|
// 22:30 ART del 28/02/2024 (año bisiesto) = 01:30 UTC del 29/02/2024
|
||||||
|
var fake = new FakeTimeProvider();
|
||||||
|
fake.SetUtcNow(new DateTimeOffset(2024, 2, 29, 1, 30, 0, TimeSpan.Zero));
|
||||||
|
|
||||||
|
var result = fake.GetArgentinaToday();
|
||||||
|
|
||||||
|
result.Should().Be(new DateOnly(2024, 2, 28));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cruce de año: 22:30 ART del 31/12/2026 = 01:30 UTC del 01/01/2027.
|
||||||
|
/// UTC es año nuevo, ART debe ser el 31/12 del año anterior.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void GetArgentinaToday_FinDeAnio_22_30ARTdel31Dic_Retorna31Dic()
|
||||||
|
{
|
||||||
|
// 22:30 ART del 31/12/2026 = 01:30 UTC del 01/01/2027
|
||||||
|
var fake = new FakeTimeProvider();
|
||||||
|
fake.SetUtcNow(new DateTimeOffset(2027, 1, 1, 1, 30, 0, TimeSpan.Zero));
|
||||||
|
|
||||||
|
var result = fake.GetArgentinaToday();
|
||||||
|
|
||||||
|
result.Should().Be(new DateOnly(2026, 12, 31));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,7 +95,7 @@ public class IngresosBrutosTests
|
|||||||
{
|
{
|
||||||
var original = MakeIIBB(descripcion: "Original");
|
var original = MakeIIBB(descripcion: "Original");
|
||||||
|
|
||||||
var updated = original.WithDescripcion("Actualizado");
|
var updated = original.WithDescripcion("Actualizado", DateTime.UtcNow);
|
||||||
|
|
||||||
updated.Should().NotBeSameAs(original);
|
updated.Should().NotBeSameAs(original);
|
||||||
updated.Descripcion.Should().Be("Actualizado");
|
updated.Descripcion.Should().Be("Actualizado");
|
||||||
@@ -107,7 +107,7 @@ public class IngresosBrutosTests
|
|||||||
{
|
{
|
||||||
var original = MakeIIBB(activo: true);
|
var original = MakeIIBB(activo: true);
|
||||||
|
|
||||||
var deactivated = original.Deactivate();
|
var deactivated = original.Deactivate(DateTime.UtcNow);
|
||||||
|
|
||||||
deactivated.Activo.Should().BeFalse();
|
deactivated.Activo.Should().BeFalse();
|
||||||
deactivated.Alicuota.Should().Be(original.Alicuota);
|
deactivated.Alicuota.Should().Be(original.Alicuota);
|
||||||
@@ -119,7 +119,7 @@ public class IngresosBrutosTests
|
|||||||
{
|
{
|
||||||
var original = MakeIIBB(activo: false);
|
var original = MakeIIBB(activo: false);
|
||||||
|
|
||||||
var reactivated = original.Reactivate();
|
var reactivated = original.Reactivate(DateTime.UtcNow);
|
||||||
|
|
||||||
reactivated.Activo.Should().BeTrue();
|
reactivated.Activo.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ public class IngresosBrutosTests
|
|||||||
var original = MakeIIBB(vigenciaHasta: null);
|
var original = MakeIIBB(vigenciaHasta: null);
|
||||||
var hasta = new DateOnly(2026, 5, 31);
|
var hasta = new DateOnly(2026, 5, 31);
|
||||||
|
|
||||||
var cerrado = original.CerrarVigencia(hasta);
|
var cerrado = original.CerrarVigencia(hasta, DateTime.UtcNow);
|
||||||
|
|
||||||
cerrado.VigenciaHasta.Should().Be(hasta);
|
cerrado.VigenciaHasta.Should().Be(hasta);
|
||||||
cerrado.Alicuota.Should().Be(original.Alicuota);
|
cerrado.Alicuota.Should().Be(original.Alicuota);
|
||||||
@@ -143,7 +143,7 @@ public class IngresosBrutosTests
|
|||||||
{
|
{
|
||||||
var predecesora = MakeIIBB(id: 5, alicuota: 2.5m, vigenciaDesde: Desde2020, vigenciaHasta: null);
|
var predecesora = MakeIIBB(id: 5, alicuota: 2.5m, vigenciaDesde: Desde2020, vigenciaHasta: null);
|
||||||
|
|
||||||
var (cerrada, nueva) = predecesora.NuevaVersion(3.0m, Desde2026);
|
var (cerrada, nueva) = predecesora.NuevaVersion(3.0m, Desde2026, DateTime.UtcNow);
|
||||||
|
|
||||||
cerrada.Id.Should().Be(5);
|
cerrada.Id.Should().Be(5);
|
||||||
cerrada.VigenciaHasta.Should().Be(Desde2026.AddDays(-1));
|
cerrada.VigenciaHasta.Should().Be(Desde2026.AddDays(-1));
|
||||||
@@ -166,7 +166,7 @@ public class IngresosBrutosTests
|
|||||||
vigenciaDesde: Desde2020,
|
vigenciaDesde: Desde2020,
|
||||||
vigenciaHasta: new DateOnly(2025, 12, 31));
|
vigenciaHasta: new DateOnly(2025, 12, 31));
|
||||||
|
|
||||||
var act = () => predecesora.NuevaVersion(4.0m, Desde2026);
|
var act = () => predecesora.NuevaVersion(4.0m, Desde2026, DateTime.UtcNow);
|
||||||
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
act.Should().Throw<InvalidOperationException>();
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ public class IngresosBrutosTests
|
|||||||
{
|
{
|
||||||
var predecesora = MakeIIBB(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
var predecesora = MakeIIBB(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
||||||
|
|
||||||
var act = () => predecesora.NuevaVersion(4.0m, Desde2020);
|
var act = () => predecesora.NuevaVersion(4.0m, Desde2020, DateTime.UtcNow);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>()
|
act.Should().Throw<ArgumentException>()
|
||||||
.WithParameterName("vigenciaDesde");
|
.WithParameterName("vigenciaDesde");
|
||||||
@@ -187,7 +187,7 @@ public class IngresosBrutosTests
|
|||||||
{
|
{
|
||||||
var predecesora = MakeIIBB(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
var predecesora = MakeIIBB(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
||||||
|
|
||||||
var act = () => predecesora.NuevaVersion(-1m, Desde2026);
|
var act = () => predecesora.NuevaVersion(-1m, Desde2026, DateTime.UtcNow);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>()
|
act.Should().Throw<ArgumentException>()
|
||||||
.WithParameterName("nuevaAlicuota");
|
.WithParameterName("nuevaAlicuota");
|
||||||
@@ -198,7 +198,7 @@ public class IngresosBrutosTests
|
|||||||
{
|
{
|
||||||
var predecesora = MakeIIBB(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
var predecesora = MakeIIBB(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
||||||
|
|
||||||
var act = () => predecesora.NuevaVersion(101m, Desde2026);
|
var act = () => predecesora.NuevaVersion(101m, Desde2026, DateTime.UtcNow);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>()
|
act.Should().Throw<ArgumentException>()
|
||||||
.WithParameterName("nuevaAlicuota");
|
.WithParameterName("nuevaAlicuota");
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ public class TipoDeIvaTests
|
|||||||
{
|
{
|
||||||
var original = MakeTipoDeIva(descripcion: "Original");
|
var original = MakeTipoDeIva(descripcion: "Original");
|
||||||
|
|
||||||
var updated = original.WithDescripcion("Nueva descripcion");
|
var updated = original.WithDescripcion("Nueva descripcion", DateTime.UtcNow);
|
||||||
|
|
||||||
updated.Should().NotBeSameAs(original);
|
updated.Should().NotBeSameAs(original);
|
||||||
updated.Descripcion.Should().Be("Nueva descripcion");
|
updated.Descripcion.Should().Be("Nueva descripcion");
|
||||||
@@ -162,7 +162,7 @@ public class TipoDeIvaTests
|
|||||||
{
|
{
|
||||||
var original = MakeTipoDeIva(codigo: "IVA_21");
|
var original = MakeTipoDeIva(codigo: "IVA_21");
|
||||||
|
|
||||||
var updated = original.WithCodigo("NO_GRAVADO");
|
var updated = original.WithCodigo("NO_GRAVADO", DateTime.UtcNow);
|
||||||
|
|
||||||
updated.Codigo.Should().Be("NO_GRAVADO");
|
updated.Codigo.Should().Be("NO_GRAVADO");
|
||||||
updated.Porcentaje.Should().Be(original.Porcentaje);
|
updated.Porcentaje.Should().Be(original.Porcentaje);
|
||||||
@@ -174,7 +174,7 @@ public class TipoDeIvaTests
|
|||||||
{
|
{
|
||||||
var original = MakeTipoDeIva(aplicaIVA: true);
|
var original = MakeTipoDeIva(aplicaIVA: true);
|
||||||
|
|
||||||
var updated = original.WithAplicaIVA(false);
|
var updated = original.WithAplicaIVA(false, DateTime.UtcNow);
|
||||||
|
|
||||||
updated.AplicaIVA.Should().BeFalse();
|
updated.AplicaIVA.Should().BeFalse();
|
||||||
updated.Porcentaje.Should().Be(original.Porcentaje);
|
updated.Porcentaje.Should().Be(original.Porcentaje);
|
||||||
@@ -185,7 +185,7 @@ public class TipoDeIvaTests
|
|||||||
{
|
{
|
||||||
var original = MakeTipoDeIva(activo: true);
|
var original = MakeTipoDeIva(activo: true);
|
||||||
|
|
||||||
var deactivated = original.Deactivate();
|
var deactivated = original.Deactivate(DateTime.UtcNow);
|
||||||
|
|
||||||
deactivated.Activo.Should().BeFalse();
|
deactivated.Activo.Should().BeFalse();
|
||||||
deactivated.Porcentaje.Should().Be(original.Porcentaje);
|
deactivated.Porcentaje.Should().Be(original.Porcentaje);
|
||||||
@@ -197,7 +197,7 @@ public class TipoDeIvaTests
|
|||||||
{
|
{
|
||||||
var original = MakeTipoDeIva(activo: false);
|
var original = MakeTipoDeIva(activo: false);
|
||||||
|
|
||||||
var reactivated = original.Reactivate();
|
var reactivated = original.Reactivate(DateTime.UtcNow);
|
||||||
|
|
||||||
reactivated.Activo.Should().BeTrue();
|
reactivated.Activo.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@@ -208,7 +208,7 @@ public class TipoDeIvaTests
|
|||||||
var original = MakeTipoDeIva(vigenciaHasta: null);
|
var original = MakeTipoDeIva(vigenciaHasta: null);
|
||||||
var hasta = new DateOnly(2026, 5, 31);
|
var hasta = new DateOnly(2026, 5, 31);
|
||||||
|
|
||||||
var cerrado = original.CerrarVigencia(hasta);
|
var cerrado = original.CerrarVigencia(hasta, DateTime.UtcNow);
|
||||||
|
|
||||||
cerrado.VigenciaHasta.Should().Be(hasta);
|
cerrado.VigenciaHasta.Should().Be(hasta);
|
||||||
cerrado.Porcentaje.Should().Be(original.Porcentaje);
|
cerrado.Porcentaje.Should().Be(original.Porcentaje);
|
||||||
@@ -222,7 +222,7 @@ public class TipoDeIvaTests
|
|||||||
{
|
{
|
||||||
var predecesora = MakeTipoDeIva(id: 5, porcentaje: 21m, vigenciaDesde: Desde2020, vigenciaHasta: null);
|
var predecesora = MakeTipoDeIva(id: 5, porcentaje: 21m, vigenciaDesde: Desde2020, vigenciaHasta: null);
|
||||||
|
|
||||||
var (cerrada, nueva) = predecesora.NuevaVersion(23.5m, Desde2026);
|
var (cerrada, nueva) = predecesora.NuevaVersion(23.5m, Desde2026, DateTime.UtcNow);
|
||||||
|
|
||||||
cerrada.Id.Should().Be(5);
|
cerrada.Id.Should().Be(5);
|
||||||
cerrada.VigenciaHasta.Should().Be(Desde2026.AddDays(-1), "predecesora queda cerrada el día anterior");
|
cerrada.VigenciaHasta.Should().Be(Desde2026.AddDays(-1), "predecesora queda cerrada el día anterior");
|
||||||
@@ -244,7 +244,7 @@ public class TipoDeIvaTests
|
|||||||
var predecesora = MakeTipoDeIva(porcentaje: 10.5m, vigenciaDesde: Desde2020);
|
var predecesora = MakeTipoDeIva(porcentaje: 10.5m, vigenciaDesde: Desde2020);
|
||||||
var nuevaVigencia = new DateOnly(2025, 1, 1);
|
var nuevaVigencia = new DateOnly(2025, 1, 1);
|
||||||
|
|
||||||
var (_, nueva) = predecesora.NuevaVersion(21m, nuevaVigencia);
|
var (_, nueva) = predecesora.NuevaVersion(21m, nuevaVigencia, DateTime.UtcNow);
|
||||||
|
|
||||||
nueva.Porcentaje.Should().Be(21m);
|
nueva.Porcentaje.Should().Be(21m);
|
||||||
predecesora.Porcentaje.Should().Be(10.5m, "predecesora no muta");
|
predecesora.Porcentaje.Should().Be(10.5m, "predecesora no muta");
|
||||||
@@ -259,7 +259,7 @@ public class TipoDeIvaTests
|
|||||||
vigenciaDesde: Desde2020,
|
vigenciaDesde: Desde2020,
|
||||||
vigenciaHasta: new DateOnly(2025, 12, 31)); // ya cerrada
|
vigenciaHasta: new DateOnly(2025, 12, 31)); // ya cerrada
|
||||||
|
|
||||||
var act = () => predecesora.NuevaVersion(23.5m, Desde2026);
|
var act = () => predecesora.NuevaVersion(23.5m, Desde2026, DateTime.UtcNow);
|
||||||
|
|
||||||
act.Should().Throw<InvalidOperationException>();
|
act.Should().Throw<InvalidOperationException>();
|
||||||
}
|
}
|
||||||
@@ -269,7 +269,7 @@ public class TipoDeIvaTests
|
|||||||
{
|
{
|
||||||
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
||||||
|
|
||||||
var act = () => predecesora.NuevaVersion(23.5m, Desde2020); // igual a VigenciaDesde predecesora
|
var act = () => predecesora.NuevaVersion(23.5m, Desde2020, DateTime.UtcNow); // igual a VigenciaDesde predecesora
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>()
|
act.Should().Throw<ArgumentException>()
|
||||||
.WithParameterName("vigenciaDesde");
|
.WithParameterName("vigenciaDesde");
|
||||||
@@ -281,7 +281,7 @@ public class TipoDeIvaTests
|
|||||||
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2026, vigenciaHasta: null);
|
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2026, vigenciaHasta: null);
|
||||||
var vigenciaAnterior = new DateOnly(2020, 1, 1);
|
var vigenciaAnterior = new DateOnly(2020, 1, 1);
|
||||||
|
|
||||||
var act = () => predecesora.NuevaVersion(23.5m, vigenciaAnterior);
|
var act = () => predecesora.NuevaVersion(23.5m, vigenciaAnterior, DateTime.UtcNow);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>()
|
act.Should().Throw<ArgumentException>()
|
||||||
.WithParameterName("vigenciaDesde");
|
.WithParameterName("vigenciaDesde");
|
||||||
@@ -292,7 +292,7 @@ public class TipoDeIvaTests
|
|||||||
{
|
{
|
||||||
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
||||||
|
|
||||||
var act = () => predecesora.NuevaVersion(-1m, Desde2026);
|
var act = () => predecesora.NuevaVersion(-1m, Desde2026, DateTime.UtcNow);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>()
|
act.Should().Throw<ArgumentException>()
|
||||||
.WithParameterName("nuevoPorcentaje");
|
.WithParameterName("nuevoPorcentaje");
|
||||||
@@ -303,7 +303,7 @@ public class TipoDeIvaTests
|
|||||||
{
|
{
|
||||||
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2020, vigenciaHasta: null);
|
||||||
|
|
||||||
var act = () => predecesora.NuevaVersion(101m, Desde2026);
|
var act = () => predecesora.NuevaVersion(101m, Desde2026, DateTime.UtcNow);
|
||||||
|
|
||||||
act.Should().Throw<ArgumentException>()
|
act.Should().Throw<ArgumentException>()
|
||||||
.WithParameterName("nuevoPorcentaje");
|
.WithParameterName("nuevoPorcentaje");
|
||||||
@@ -326,7 +326,7 @@ public class TipoDeIvaTests
|
|||||||
{
|
{
|
||||||
var original = MakeTipoDeIva(id: 99, porcentaje: 21m, vigenciaDesde: Desde2020);
|
var original = MakeTipoDeIva(id: 99, porcentaje: 21m, vigenciaDesde: Desde2020);
|
||||||
|
|
||||||
var updated = original.WithDescripcion("Nueva");
|
var updated = original.WithDescripcion("Nueva", DateTime.UtcNow);
|
||||||
|
|
||||||
updated.Id.Should().Be(99);
|
updated.Id.Should().Be(99);
|
||||||
updated.Porcentaje.Should().Be(21m);
|
updated.Porcentaje.Should().Be(21m);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class PuntoDeVentaTests
|
|||||||
{
|
{
|
||||||
var original = MakePdv(id: 10, nombre: "Original");
|
var original = MakePdv(id: 10, nombre: "Original");
|
||||||
|
|
||||||
var updated = original.WithUpdatedProfile(nombre: "Actualizado", numeroAFIP: 7, descripcion: "Desc");
|
var updated = original.WithUpdatedProfile(nombre: "Actualizado", numeroAFIP: 7, descripcion: "Desc", now: DateTime.UtcNow);
|
||||||
|
|
||||||
Assert.NotSame(original, updated);
|
Assert.NotSame(original, updated);
|
||||||
Assert.Equal("Actualizado", updated.Nombre);
|
Assert.Equal("Actualizado", updated.Nombre);
|
||||||
@@ -56,7 +56,7 @@ public class PuntoDeVentaTests
|
|||||||
{
|
{
|
||||||
var original = MakePdv(id: 10, medioId: 5);
|
var original = MakePdv(id: 10, medioId: 5);
|
||||||
|
|
||||||
var updated = original.WithUpdatedProfile("Nuevo", 2, null);
|
var updated = original.WithUpdatedProfile("Nuevo", 2, null, DateTime.UtcNow);
|
||||||
|
|
||||||
Assert.Equal(10, updated.Id);
|
Assert.Equal(10, updated.Id);
|
||||||
Assert.Equal(5, updated.MedioId);
|
Assert.Equal(5, updated.MedioId);
|
||||||
@@ -69,7 +69,7 @@ public class PuntoDeVentaTests
|
|||||||
{
|
{
|
||||||
var original = MakePdv();
|
var original = MakePdv();
|
||||||
|
|
||||||
var updated = original.WithUpdatedProfile("Nuevo", 2, null);
|
var updated = original.WithUpdatedProfile("Nuevo", 2, null, DateTime.UtcNow);
|
||||||
|
|
||||||
Assert.NotNull(updated.FechaModificacion);
|
Assert.NotNull(updated.FechaModificacion);
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ public class PuntoDeVentaTests
|
|||||||
{
|
{
|
||||||
var pdv = MakePdv(activo: true);
|
var pdv = MakePdv(activo: true);
|
||||||
|
|
||||||
var deactivated = pdv.WithActivo(false);
|
var deactivated = pdv.WithActivo(false, DateTime.UtcNow);
|
||||||
|
|
||||||
Assert.False(deactivated.Activo);
|
Assert.False(deactivated.Activo);
|
||||||
Assert.NotSame(pdv, deactivated);
|
Assert.NotSame(pdv, deactivated);
|
||||||
@@ -92,7 +92,7 @@ public class PuntoDeVentaTests
|
|||||||
{
|
{
|
||||||
var pdv = MakePdv(activo: false);
|
var pdv = MakePdv(activo: false);
|
||||||
|
|
||||||
var reactivated = pdv.WithActivo(true);
|
var reactivated = pdv.WithActivo(true, DateTime.UtcNow);
|
||||||
|
|
||||||
Assert.True(reactivated.Activo);
|
Assert.True(reactivated.Activo);
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ public class PuntoDeVentaTests
|
|||||||
{
|
{
|
||||||
var pdv = MakePdv(id: 99, medioId: 3);
|
var pdv = MakePdv(id: 99, medioId: 3);
|
||||||
|
|
||||||
var toggled = pdv.WithActivo(false);
|
var toggled = pdv.WithActivo(false, DateTime.UtcNow);
|
||||||
|
|
||||||
Assert.Equal(99, toggled.Id);
|
Assert.Equal(99, toggled.Id);
|
||||||
Assert.Equal(3, toggled.MedioId);
|
Assert.Equal(3, toggled.MedioId);
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ public class UsuarioTests
|
|||||||
public void WithUpdatedProfile_Returns_NewInstance()
|
public void WithUpdatedProfile_Returns_NewInstance()
|
||||||
{
|
{
|
||||||
var u = MakeUsuario();
|
var u = MakeUsuario();
|
||||||
var updated = u.WithUpdatedProfile("NuevoNombre", "NuevoApellido", "new@x.com", "cajero", true);
|
var updated = u.WithUpdatedProfile("NuevoNombre", "NuevoApellido", "new@x.com", "cajero", true, DateTime.UtcNow);
|
||||||
Assert.NotSame(u, updated);
|
Assert.NotSame(u, updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ public class UsuarioTests
|
|||||||
public void WithUpdatedProfile_Sets_Fields_Correctly()
|
public void WithUpdatedProfile_Sets_Fields_Correctly()
|
||||||
{
|
{
|
||||||
var u = MakeUsuario();
|
var u = MakeUsuario();
|
||||||
var updated = u.WithUpdatedProfile("Pedro", "Gómez", "p@g.com", "cajero", false);
|
var updated = u.WithUpdatedProfile("Pedro", "Gómez", "p@g.com", "cajero", false, DateTime.UtcNow);
|
||||||
Assert.Equal("Pedro", updated.Nombre);
|
Assert.Equal("Pedro", updated.Nombre);
|
||||||
Assert.Equal("Gómez", updated.Apellido);
|
Assert.Equal("Gómez", updated.Apellido);
|
||||||
Assert.Equal("p@g.com", updated.Email);
|
Assert.Equal("p@g.com", updated.Email);
|
||||||
@@ -121,8 +121,9 @@ public class UsuarioTests
|
|||||||
public void WithUpdatedProfile_Sets_FechaModificacion_To_UtcNow()
|
public void WithUpdatedProfile_Sets_FechaModificacion_To_UtcNow()
|
||||||
{
|
{
|
||||||
var before = DateTime.UtcNow.AddSeconds(-1);
|
var before = DateTime.UtcNow.AddSeconds(-1);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
var u = MakeUsuario();
|
var u = MakeUsuario();
|
||||||
var updated = u.WithUpdatedProfile("A", "B", null, "admin", true);
|
var updated = u.WithUpdatedProfile("A", "B", null, "admin", true, now);
|
||||||
Assert.NotNull(updated.FechaModificacion);
|
Assert.NotNull(updated.FechaModificacion);
|
||||||
Assert.True(updated.FechaModificacion >= before);
|
Assert.True(updated.FechaModificacion >= before);
|
||||||
}
|
}
|
||||||
@@ -131,7 +132,7 @@ public class UsuarioTests
|
|||||||
public void WithUpdatedProfile_Preserves_Immutable_Fields()
|
public void WithUpdatedProfile_Preserves_Immutable_Fields()
|
||||||
{
|
{
|
||||||
var u = MakeUsuario();
|
var u = MakeUsuario();
|
||||||
var updated = u.WithUpdatedProfile("X", "Y", null, "cajero", true);
|
var updated = u.WithUpdatedProfile("X", "Y", null, "cajero", true, DateTime.UtcNow);
|
||||||
Assert.Equal(u.Id, updated.Id);
|
Assert.Equal(u.Id, updated.Id);
|
||||||
Assert.Equal(u.Username, updated.Username);
|
Assert.Equal(u.Username, updated.Username);
|
||||||
Assert.Equal(u.PasswordHash, updated.PasswordHash);
|
Assert.Equal(u.PasswordHash, updated.PasswordHash);
|
||||||
@@ -143,7 +144,7 @@ public class UsuarioTests
|
|||||||
public void WithNewPasswordHash_Returns_NewInstance()
|
public void WithNewPasswordHash_Returns_NewInstance()
|
||||||
{
|
{
|
||||||
var u = MakeUsuario();
|
var u = MakeUsuario();
|
||||||
var updated = u.WithNewPasswordHash("newhash", mustChangePassword: false);
|
var updated = u.WithNewPasswordHash("newhash", mustChangePassword: false, DateTime.UtcNow);
|
||||||
Assert.NotSame(u, updated);
|
Assert.NotSame(u, updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +152,7 @@ public class UsuarioTests
|
|||||||
public void WithNewPasswordHash_Sets_Hash_And_MustChange()
|
public void WithNewPasswordHash_Sets_Hash_And_MustChange()
|
||||||
{
|
{
|
||||||
var u = MakeUsuario();
|
var u = MakeUsuario();
|
||||||
var updated = u.WithNewPasswordHash("newhash", mustChangePassword: true);
|
var updated = u.WithNewPasswordHash("newhash", mustChangePassword: true, DateTime.UtcNow);
|
||||||
Assert.Equal("newhash", updated.PasswordHash);
|
Assert.Equal("newhash", updated.PasswordHash);
|
||||||
Assert.True(updated.MustChangePassword);
|
Assert.True(updated.MustChangePassword);
|
||||||
}
|
}
|
||||||
@@ -160,7 +161,7 @@ public class UsuarioTests
|
|||||||
public void WithNewPasswordHash_Clears_MustChange_When_False()
|
public void WithNewPasswordHash_Clears_MustChange_When_False()
|
||||||
{
|
{
|
||||||
var u = MakeUsuario(mustChangePassword: true);
|
var u = MakeUsuario(mustChangePassword: true);
|
||||||
var updated = u.WithNewPasswordHash("hash2", mustChangePassword: false);
|
var updated = u.WithNewPasswordHash("hash2", mustChangePassword: false, DateTime.UtcNow);
|
||||||
Assert.False(updated.MustChangePassword);
|
Assert.False(updated.MustChangePassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,8 +169,9 @@ public class UsuarioTests
|
|||||||
public void WithNewPasswordHash_Sets_FechaModificacion()
|
public void WithNewPasswordHash_Sets_FechaModificacion()
|
||||||
{
|
{
|
||||||
var before = DateTime.UtcNow.AddSeconds(-1);
|
var before = DateTime.UtcNow.AddSeconds(-1);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
var u = MakeUsuario();
|
var u = MakeUsuario();
|
||||||
var updated = u.WithNewPasswordHash("hash2", false);
|
var updated = u.WithNewPasswordHash("hash2", false, now);
|
||||||
Assert.NotNull(updated.FechaModificacion);
|
Assert.NotNull(updated.FechaModificacion);
|
||||||
Assert.True(updated.FechaModificacion >= before);
|
Assert.True(updated.FechaModificacion >= before);
|
||||||
}
|
}
|
||||||
@@ -208,7 +210,7 @@ public class UsuarioTests
|
|||||||
public void WithMustChangePassword_Sets_Value_True()
|
public void WithMustChangePassword_Sets_Value_True()
|
||||||
{
|
{
|
||||||
var u = MakeUsuario(mustChangePassword: false);
|
var u = MakeUsuario(mustChangePassword: false);
|
||||||
var updated = u.WithMustChangePassword(true);
|
var updated = u.WithMustChangePassword(true, DateTime.UtcNow);
|
||||||
Assert.True(updated.MustChangePassword);
|
Assert.True(updated.MustChangePassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,8 +218,9 @@ public class UsuarioTests
|
|||||||
public void WithMustChangePassword_Sets_FechaModificacion()
|
public void WithMustChangePassword_Sets_FechaModificacion()
|
||||||
{
|
{
|
||||||
var before = DateTime.UtcNow.AddSeconds(-1);
|
var before = DateTime.UtcNow.AddSeconds(-1);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
var u = MakeUsuario();
|
var u = MakeUsuario();
|
||||||
var updated = u.WithMustChangePassword(true);
|
var updated = u.WithMustChangePassword(true, now);
|
||||||
Assert.NotNull(updated.FechaModificacion);
|
Assert.NotNull(updated.FechaModificacion);
|
||||||
Assert.True(updated.FechaModificacion >= before);
|
Assert.True(updated.FechaModificacion >= before);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public sealed class AuditLoggerTests
|
|||||||
repo ??= Substitute.For<IAuditEventRepository>();
|
repo ??= Substitute.For<IAuditEventRepository>();
|
||||||
options ??= new AuditOptions();
|
options ??= new AuditOptions();
|
||||||
var optsWrapper = Options.Create(options);
|
var optsWrapper = Options.Create(options);
|
||||||
return new AuditLogger(context, repo, optsWrapper);
|
return new AuditLogger(context, repo, optsWrapper, TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public sealed class SecurityEventLoggerTests
|
|||||||
repo ??= Substitute.For<ISecurityEventRepository>();
|
repo ??= Substitute.For<ISecurityEventRepository>();
|
||||||
context ??= Substitute.For<IAuditContext>();
|
context ??= Substitute.For<IAuditContext>();
|
||||||
options ??= new AuditOptions();
|
options ??= new AuditOptions();
|
||||||
return new SecurityEventLogger(repo, context, Options.Create(options));
|
return new SecurityEventLogger(repo, context, Options.Create(options), TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class JwtServiceTests : IDisposable
|
|||||||
Audience = "sigcm2.web",
|
Audience = "sigcm2.web",
|
||||||
AccessTokenMinutes = 60
|
AccessTokenMinutes = 60
|
||||||
};
|
};
|
||||||
_jwtService = new JwtService(_rsa, _options);
|
_jwtService = new JwtService(_rsa, _options, TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => _rsa.Dispose();
|
public void Dispose() => _rsa.Dispose();
|
||||||
@@ -219,7 +219,7 @@ public class JwtServiceTests : IDisposable
|
|||||||
// Sign with a different RSA key
|
// Sign with a different RSA key
|
||||||
using var otherRsa = System.Security.Cryptography.RSA.Create(2048);
|
using var otherRsa = System.Security.Cryptography.RSA.Create(2048);
|
||||||
var otherOptions = new JwtOptions { Issuer = "sigcm2.api", Audience = "sigcm2.web", AccessTokenMinutes = 60 };
|
var otherOptions = new JwtOptions { Issuer = "sigcm2.api", Audience = "sigcm2.web", AccessTokenMinutes = 60 };
|
||||||
var otherService = new JwtService(otherRsa, otherOptions);
|
var otherService = new JwtService(otherRsa, otherOptions, TimeProvider.System);
|
||||||
var tokenFromOtherKey = otherService.GenerateAccessToken(MakeUsuario());
|
var tokenFromOtherKey = otherService.GenerateAccessToken(MakeUsuario());
|
||||||
|
|
||||||
// Validating with the correct key should throw
|
// Validating with the correct key should throw
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class CreateIngresosBrutosCommandHandlerTests
|
|||||||
|
|
||||||
public CreateIngresosBrutosCommandHandlerTests()
|
public CreateIngresosBrutosCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new CreateIngresosBrutosCommandHandler(_repo, _audit);
|
_handler = new CreateIngresosBrutosCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
_repo.InsertAsync(Arg.Any<IibbEntity>(), Arg.Any<CancellationToken>()).Returns(55);
|
_repo.InsertAsync(Arg.Any<IibbEntity>(), Arg.Any<CancellationToken>()).Returns(55);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class DeactivateIngresosBrutosCommandHandlerTests
|
|||||||
|
|
||||||
public DeactivateIngresosBrutosCommandHandlerTests()
|
public DeactivateIngresosBrutosCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new DeactivateIngresosBrutosCommandHandler(_repo, _audit);
|
_handler = new DeactivateIngresosBrutosCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
||||||
_repo.SetActivoAsync(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()).Returns(true);
|
_repo.SetActivoAsync(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()).Returns(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class NuevaVersionIngresosBrutosCommandHandlerTests
|
|||||||
|
|
||||||
public NuevaVersionIngresosBrutosCommandHandlerTests()
|
public NuevaVersionIngresosBrutosCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new NuevaVersionIngresosBrutosCommandHandler(_repo, _audit);
|
_handler = new NuevaVersionIngresosBrutosCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakePredecesora());
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakePredecesora());
|
||||||
_repo.UpdateCierreVigenciaAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>()).Returns(true);
|
_repo.UpdateCierreVigenciaAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>()).Returns(true);
|
||||||
_repo.InsertAsync(Arg.Any<IibbEntity>(), Arg.Any<CancellationToken>()).Returns(88);
|
_repo.InsertAsync(Arg.Any<IibbEntity>(), Arg.Any<CancellationToken>()).Returns(88);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class ReactivateIngresosBrutosCommandHandlerTests
|
|||||||
|
|
||||||
public ReactivateIngresosBrutosCommandHandlerTests()
|
public ReactivateIngresosBrutosCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new ReactivateIngresosBrutosCommandHandler(_repo, _audit);
|
_handler = new ReactivateIngresosBrutosCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
||||||
_repo.SetActivoAsync(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()).Returns(true);
|
_repo.SetActivoAsync(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()).Returns(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class UpdateIngresosBrutosCommandHandlerTests
|
|||||||
|
|
||||||
public UpdateIngresosBrutosCommandHandlerTests()
|
public UpdateIngresosBrutosCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new UpdateIngresosBrutosCommandHandler(_repo, _audit);
|
_handler = new UpdateIngresosBrutosCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
||||||
_repo.UpdateCosmeticoAsync(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<bool>(),
|
_repo.UpdateCosmeticoAsync(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<bool>(),
|
||||||
Arg.Any<CancellationToken>()).Returns(true);
|
Arg.Any<CancellationToken>()).Returns(true);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class DeactivateMedioCommandHandlerTests
|
|||||||
|
|
||||||
public DeactivateMedioCommandHandlerTests()
|
public DeactivateMedioCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new DeactivateMedioCommandHandler(_repo, _audit);
|
_handler = new DeactivateMedioCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── not found → throws ──────────────────────────────────────────────────
|
// ── not found → throws ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ public class MedioRepositoryTests : IAsyncLifetime
|
|||||||
var id = await _repository.AddAsync(Medio.ForCreation("UPD01", "Original", TipoMedio.Diario, null));
|
var id = await _repository.AddAsync(Medio.ForCreation("UPD01", "Original", TipoMedio.Diario, null));
|
||||||
var original = await _repository.GetByIdAsync(id);
|
var original = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
var updated = original!.WithUpdatedProfile("Actualizado", TipoMedio.Radio, 7);
|
var updated = original!.WithUpdatedProfile("Actualizado", TipoMedio.Radio, 7, DateTime.UtcNow);
|
||||||
await _repository.UpdateAsync(updated);
|
await _repository.UpdateAsync(updated);
|
||||||
|
|
||||||
var result = await _repository.GetByIdAsync(id);
|
var result = await _repository.GetByIdAsync(id);
|
||||||
@@ -167,7 +167,7 @@ public class MedioRepositoryTests : IAsyncLifetime
|
|||||||
var id = await _repository.AddAsync(Medio.ForCreation("HIST01", "Historial", TipoMedio.Diario, null));
|
var id = await _repository.AddAsync(Medio.ForCreation("HIST01", "Historial", TipoMedio.Diario, null));
|
||||||
var original = await _repository.GetByIdAsync(id);
|
var original = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
var updated = original!.WithUpdatedProfile("Historial v2", TipoMedio.Web, null);
|
var updated = original!.WithUpdatedProfile("Historial v2", TipoMedio.Web, null, DateTime.UtcNow);
|
||||||
await _repository.UpdateAsync(updated);
|
await _repository.UpdateAsync(updated);
|
||||||
|
|
||||||
var historyCount = await _connection.ExecuteScalarAsync<int>(
|
var historyCount = await _connection.ExecuteScalarAsync<int>(
|
||||||
@@ -186,7 +186,7 @@ public class MedioRepositoryTests : IAsyncLifetime
|
|||||||
|
|
||||||
// Deactivate second medio
|
// Deactivate second medio
|
||||||
var inact = await _repository.GetByIdAsync(idInact);
|
var inact = await _repository.GetByIdAsync(idInact);
|
||||||
await _repository.UpdateAsync(inact!.WithActivo(false));
|
await _repository.UpdateAsync(inact!.WithActivo(false, DateTime.UtcNow));
|
||||||
|
|
||||||
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 50, Activo: true, Tipo: null, Search: null));
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 50, Activo: true, Tipo: null, Search: null));
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class ReactivateMedioCommandHandlerTests
|
|||||||
|
|
||||||
public ReactivateMedioCommandHandlerTests()
|
public ReactivateMedioCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new ReactivateMedioCommandHandler(_repo, _audit);
|
_handler = new ReactivateMedioCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── not found → throws ──────────────────────────────────────────────────
|
// ── not found → throws ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class UpdateMedioCommandHandlerTests
|
|||||||
|
|
||||||
public UpdateMedioCommandHandlerTests()
|
public UpdateMedioCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new UpdateMedioCommandHandler(_repo, _audit);
|
_handler = new UpdateMedioCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── not found → throws ──────────────────────────────────────────────────
|
// ── not found → throws ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class DeactivatePuntoDeVentaCommandHandlerTests
|
|||||||
|
|
||||||
public DeactivatePuntoDeVentaCommandHandlerTests()
|
public DeactivatePuntoDeVentaCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new DeactivatePuntoDeVentaCommandHandler(_repo, _medioRepo, _audit);
|
_handler = new DeactivatePuntoDeVentaCommandHandler(_repo, _medioRepo, _audit, TimeProvider.System);
|
||||||
_medioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(MakeMedio(5, true));
|
_medioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(MakeMedio(5, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class ReactivatePuntoDeVentaCommandHandlerTests
|
|||||||
|
|
||||||
public ReactivatePuntoDeVentaCommandHandlerTests()
|
public ReactivatePuntoDeVentaCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new ReactivatePuntoDeVentaCommandHandler(_repo, _medioRepo, _audit);
|
_handler = new ReactivatePuntoDeVentaCommandHandler(_repo, _medioRepo, _audit, TimeProvider.System);
|
||||||
_medioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(MakeMedio(5, true));
|
_medioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(MakeMedio(5, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class UpdatePuntoDeVentaCommandHandlerTests
|
|||||||
|
|
||||||
public UpdatePuntoDeVentaCommandHandlerTests()
|
public UpdatePuntoDeVentaCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new UpdatePuntoDeVentaCommandHandler(_repo, _medioRepo, _audit);
|
_handler = new UpdatePuntoDeVentaCommandHandler(_repo, _medioRepo, _audit, TimeProvider.System);
|
||||||
_repo.GetByIdAsync(10, Arg.Any<CancellationToken>()).Returns(MakePdv(10));
|
_repo.GetByIdAsync(10, Arg.Any<CancellationToken>()).Returns(MakePdv(10));
|
||||||
_medioRepo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(MakeMedio(5));
|
_medioRepo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(MakeMedio(5));
|
||||||
_repo.ExistsByNumeroAFIPInMedioAsync(Arg.Any<int>(), Arg.Any<short>(), Arg.Any<int?>(), Arg.Any<CancellationToken>()).Returns(false);
|
_repo.ExistsByNumeroAFIPInMedioAsync(Arg.Any<int>(), Arg.Any<short>(), Arg.Any<int?>(), Arg.Any<CancellationToken>()).Returns(false);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" />
|
<PackageReference Include="coverlet.collector" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" />
|
<PackageReference Include="xunit.runner.visualstudio" />
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class DeactivateSeccionCommandHandlerTests
|
|||||||
|
|
||||||
public DeactivateSeccionCommandHandlerTests()
|
public DeactivateSeccionCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new DeactivateSeccionCommandHandler(_repo, _medioRepo, _audit);
|
_handler = new DeactivateSeccionCommandHandler(_repo, _medioRepo, _audit, TimeProvider.System);
|
||||||
// Default: medio is active
|
// Default: medio is active
|
||||||
_medioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(MakeMedio(1, true));
|
_medioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(MakeMedio(1, true));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class ReactivateSeccionCommandHandlerTests
|
|||||||
|
|
||||||
public ReactivateSeccionCommandHandlerTests()
|
public ReactivateSeccionCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new ReactivateSeccionCommandHandler(_repo, _medioRepo, _audit);
|
_handler = new ReactivateSeccionCommandHandler(_repo, _medioRepo, _audit, TimeProvider.System);
|
||||||
// Default: medio is active
|
// Default: medio is active
|
||||||
_medioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(MakeMedio(1, true));
|
_medioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(MakeMedio(1, true));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ public class SeccionRepositoryTests : IAsyncLifetime
|
|||||||
var id = await _repository.AddAsync(Seccion.ForCreation(_medioId, "UPD01", "Original", "clasificados"));
|
var id = await _repository.AddAsync(Seccion.ForCreation(_medioId, "UPD01", "Original", "clasificados"));
|
||||||
var original = await _repository.GetByIdAsync(id);
|
var original = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
var updated = original!.WithUpdatedProfile("Actualizado", "notables");
|
var updated = original!.WithUpdatedProfile("Actualizado", "notables", DateTime.UtcNow);
|
||||||
await _repository.UpdateAsync(updated);
|
await _repository.UpdateAsync(updated);
|
||||||
|
|
||||||
var result = await _repository.GetByIdAsync(id);
|
var result = await _repository.GetByIdAsync(id);
|
||||||
@@ -168,7 +168,7 @@ public class SeccionRepositoryTests : IAsyncLifetime
|
|||||||
var id = await _repository.AddAsync(Seccion.ForCreation(_medioId, "HIST01", "Historial", "clasificados"));
|
var id = await _repository.AddAsync(Seccion.ForCreation(_medioId, "HIST01", "Historial", "clasificados"));
|
||||||
var original = await _repository.GetByIdAsync(id);
|
var original = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
var updated = original!.WithUpdatedProfile("Historial v2", "suplementos");
|
var updated = original!.WithUpdatedProfile("Historial v2", "suplementos", DateTime.UtcNow);
|
||||||
await _repository.UpdateAsync(updated);
|
await _repository.UpdateAsync(updated);
|
||||||
|
|
||||||
var historyCount = await _connection.ExecuteScalarAsync<int>(
|
var historyCount = await _connection.ExecuteScalarAsync<int>(
|
||||||
@@ -213,7 +213,7 @@ public class SeccionRepositoryTests : IAsyncLifetime
|
|||||||
var inactId = await _repository.AddAsync(Seccion.ForCreation(_medioId, "INACT01", "Inactiva", "clasificados"));
|
var inactId = await _repository.AddAsync(Seccion.ForCreation(_medioId, "INACT01", "Inactiva", "clasificados"));
|
||||||
|
|
||||||
var inact = await _repository.GetByIdAsync(inactId);
|
var inact = await _repository.GetByIdAsync(inactId);
|
||||||
await _repository.UpdateAsync(inact!.WithActivo(false));
|
await _repository.UpdateAsync(inact!.WithActivo(false, DateTime.UtcNow));
|
||||||
|
|
||||||
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 50, MedioId: _medioId, Tipo: null, Activo: true, Search: null));
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 50, MedioId: _medioId, Tipo: null, Activo: true, Search: null));
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class UpdateSeccionCommandHandlerTests
|
|||||||
|
|
||||||
public UpdateSeccionCommandHandlerTests()
|
public UpdateSeccionCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new UpdateSeccionCommandHandler(_repo, _medioRepo, _audit);
|
_handler = new UpdateSeccionCommandHandler(_repo, _medioRepo, _audit, TimeProvider.System);
|
||||||
// Default: medio is active
|
// Default: medio is active
|
||||||
_medioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(MakeMedio(1, true));
|
_medioRepo.GetByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(MakeMedio(1, true));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class CreateTipoDeIvaCommandHandlerTests
|
|||||||
|
|
||||||
public CreateTipoDeIvaCommandHandlerTests()
|
public CreateTipoDeIvaCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new CreateTipoDeIvaCommandHandler(_repo, _audit);
|
_handler = new CreateTipoDeIvaCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
_repo.InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>()).Returns(42);
|
_repo.InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>()).Returns(42);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class DeactivateTipoDeIvaCommandHandlerTests
|
|||||||
|
|
||||||
public DeactivateTipoDeIvaCommandHandlerTests()
|
public DeactivateTipoDeIvaCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new DeactivateTipoDeIvaCommandHandler(_repo, _audit);
|
_handler = new DeactivateTipoDeIvaCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
||||||
_repo.SetActivoAsync(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()).Returns(true);
|
_repo.SetActivoAsync(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()).Returns(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class NuevaVersionTipoDeIvaCommandHandlerTests
|
|||||||
|
|
||||||
public NuevaVersionTipoDeIvaCommandHandlerTests()
|
public NuevaVersionTipoDeIvaCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new NuevaVersionTipoDeIvaCommandHandler(_repo, _audit);
|
_handler = new NuevaVersionTipoDeIvaCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakePredecesora());
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakePredecesora());
|
||||||
_repo.UpdateCierreVigenciaAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
|
_repo.UpdateCierreVigenciaAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class ReactivateTipoDeIvaCommandHandlerTests
|
|||||||
|
|
||||||
public ReactivateTipoDeIvaCommandHandlerTests()
|
public ReactivateTipoDeIvaCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new ReactivateTipoDeIvaCommandHandler(_repo, _audit);
|
_handler = new ReactivateTipoDeIvaCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
||||||
_repo.SetActivoAsync(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()).Returns(true);
|
_repo.SetActivoAsync(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()).Returns(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class UpdateTipoDeIvaCommandHandlerTests
|
|||||||
|
|
||||||
public UpdateTipoDeIvaCommandHandlerTests()
|
public UpdateTipoDeIvaCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new UpdateTipoDeIvaCommandHandler(_repo, _audit);
|
_handler = new UpdateTipoDeIvaCommandHandler(_repo, _audit, TimeProvider.System);
|
||||||
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
_repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakeEntity());
|
||||||
_repo.UpdateCosmeticoAsync(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<string>(),
|
_repo.UpdateCosmeticoAsync(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class DeactivateUsuarioCommandHandlerTests
|
|||||||
|
|
||||||
public DeactivateUsuarioCommandHandlerTests()
|
public DeactivateUsuarioCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_handler = new DeactivateUsuarioCommandHandler(_repo, _refreshRepo, _audit);
|
_handler = new DeactivateUsuarioCommandHandler(_repo, _refreshRepo, _audit, TimeProvider.System);
|
||||||
_repo.CountActiveAdminsAsync(Arg.Any<CancellationToken>()).Returns(2);
|
_repo.CountActiveAdminsAsync(Arg.Any<CancellationToken>()).Returns(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user