Compare commits

..

10 Commits

Author SHA1 Message Date
408c97559b chore(web/udt-011): grep final confirma 0 anti-patterns en src/web/src fuera de dateFormat.ts 2026-04-18 10:27:13 -03:00
ef4b02be3b fix(web/udt-011): AuditFilters datetime-local usa parseArgentinaDateTimeToUtc (fix BUG-FE-05) 2026-04-18 10:26:56 -03:00
03a02c63d5 refactor(web/udt-011): eliminar 4 funciones formatDate duplicadas y formatOccurredAt, usar dateFormat utility (fix BUG-FE-01, BUG-FE-02) 2026-04-18 10:26:29 -03:00
71d0928389 fix(web/udt-011): NuevaVigenciaModal preview usa prevCivilDate+formatCivilDate sin Date() (fix BUG-FE-04) 2026-04-18 10:24:15 -03:00
20b5863908 fix(web/udt-011): IngresosBrutosFormModal default vigenciaDesde usa todayArgentina 2026-04-18 10:22:47 -03:00
7e23a16062 fix(web/udt-011): TipoDeIvaFormModal default vigenciaDesde usa todayArgentina (fix BUG-FE-03) 2026-04-18 10:22:43 -03:00
2ea7678129 feat(web/udt-011): dateFormat.ts utility (formatInstant, formatCivilDate, todayArgentina, etc.) 2026-04-18 10:17:47 -03:00
bc3e5d99a1 test(web/udt-011): dateFormat.ts utility tests (Red — 6 funciones + edge cases) 2026-04-18 10:17:43 -03:00
9bc191c3ae test(udt-011): T400.40 — update tests for TimeProvider injection and explicit now params
Fix all test compilation errors caused by T400.10/T400.20/T400.30:
- Handler constructors: add TimeProvider.System as last argument
- Domain mutator calls: add DateTime.UtcNow as explicit 'now' argument
- AuditLogger/SecurityEventLogger Build() helpers: add TimeProvider.System
- JwtService test constructors: add TimeProvider.System
Cat2 coverage already present in TimeProviderArgentinaExtensionsTests.cs:
FakeTimeProvider proves GetArgentinaToday() returns ART civil date, not UTC.
2026-04-18 10:12:32 -03:00
a9838427a4 feat(udt-011): T400.30 — inject TimeProvider into Infrastructure critical services
AuditLogger, SecurityEventLogger: inject TimeProvider and use
_timeProvider.GetUtcNow().UtcDateTime for occurredAt timestamps.
JwtService: inject TimeProvider; use GetUtcNow() for token IssuedAt/Expires.
DI: update JwtService factory to pass sp.GetRequiredService<TimeProvider>().
Repositories: remove ?? DateTime.UtcNow fallback in UpdateAsync since callers
always provide FechaModificacion via domain mutators.
2026-04-18 10:12:24 -03:00
57 changed files with 450 additions and 169 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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();

View File

@@ -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,
}); });
} }

View File

@@ -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,
}); });
} }

View File

@@ -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,
}); });
} }

View File

@@ -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),

View File

@@ -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: () => {

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,
}, },
{ {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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' },

View 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();
}

View File

@@ -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

View File

@@ -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' },

View File

@@ -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()
})
})

View File

@@ -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(),
) )
}) })
}) })

View File

@@ -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, beforeAll, afterAll, afterEach, vi } from 'vitest' import { describe, it, expect, beforeAll, afterAll, afterEach, 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()
})
})

View 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');
});
});
});

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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");

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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]

View File

@@ -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]

View File

@@ -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

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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);

View File

@@ -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 ──────────────────────────────────────────────────

View File

@@ -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));

View File

@@ -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 ──────────────────────────────────────────────────

View File

@@ -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 ──────────────────────────────────────────────────

View File

@@ -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));
} }

View File

@@ -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));
} }

View File

@@ -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);

View File

@@ -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));
} }

View File

@@ -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));
} }

View File

@@ -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));

View File

@@ -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));
} }

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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>())

View File

@@ -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);
} }

View File

@@ -16,7 +16,7 @@ public class ReactivateUsuarioCommandHandlerTests
public ReactivateUsuarioCommandHandlerTests() public ReactivateUsuarioCommandHandlerTests()
{ {
_handler = new ReactivateUsuarioCommandHandler(_repo, _audit); _handler = new ReactivateUsuarioCommandHandler(_repo, _audit, TimeProvider.System);
} }
private static Usuario MakeUser(int id = 5, bool activo = false) private static Usuario MakeUser(int id = 5, bool activo = false)

View File

@@ -18,7 +18,7 @@ public class ResetUsuarioPasswordCommandHandlerTests
public ResetUsuarioPasswordCommandHandlerTests() public ResetUsuarioPasswordCommandHandlerTests()
{ {
_handler = new ResetUsuarioPasswordCommandHandler(_repo, _hasher, _refreshRepo, _audit); _handler = new ResetUsuarioPasswordCommandHandler(_repo, _hasher, _refreshRepo, _audit, TimeProvider.System);
_hasher.Hash(Arg.Any<string>()).Returns(args => "$2a$12$hashof_" + args[0]); _hasher.Hash(Arg.Any<string>()).Returns(args => "$2a$12$hashof_" + args[0]);
} }

View File

@@ -20,7 +20,7 @@ public class UpdateUsuarioCommandHandlerTests
public UpdateUsuarioCommandHandlerTests() public UpdateUsuarioCommandHandlerTests()
{ {
_handler = new UpdateUsuarioCommandHandler(_repo, _rolRepo, _refreshRepo, _audit); _handler = new UpdateUsuarioCommandHandler(_repo, _rolRepo, _refreshRepo, _audit, TimeProvider.System);
// Default: rol exists and is active // Default: rol exists and is active
_rolRepo.ExistsActiveByCodigoAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns(true); _rolRepo.ExistsActiveByCodigoAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns(true);