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.
All command handlers that call domain mutators now inject TimeProvider
via constructor and use _timeProvider.GetUtcNow().UtcDateTime as the
explicit 'now' argument. Replaces previous direct DateTime.UtcNow usage.
Remove DateTime.UtcNow calls from all With*/Deactivate/Reactivate/
CerrarVigencia/NuevaVersion domain methods. Caller (Application layer)
is now responsible for passing the UTC timestamp obtained via
_timeProvider.GetUtcNow().UtcDateTime.
Eliminar SecuenciaComprobante entity, TipoComprobante enum, DeadlockTransientException,
PuntoDeVentaInactivoException, carpetas Reservar/ y ProximoNumero/ de Application,
métodos ReservarNumeroAsync/GetUltimoNumeroAsync del repositorio, endpoints
POST /secuencias/.../reservar y GET /secuencias/.../proximo del controller,
y mapping PuntoDeVentaInactivoException del ExceptionFilter.
Seis ajustes post-verify detectados durante la corrida full de tests:
1. PuntoDeVentaRepository: UQ_PuntoDeVenta_Medio_AFIP (no _MedioId_NumeroAFIP)
— el catch de unique violation no disparaba → 500 en race duplicado.
2. Application.DependencyInjection: registro de 8 handlers PuntosDeVenta
— sin esto, dispatcher arrojaba "No service registered" → 500.
3. ReservarNumeroCommandHandler: backoff ampliado a 5 retries
[25, 75, 200, 500, 1200]ms para soportar 50 threads concurrentes.
4. SecuenciaComprobante: SYSTEM_VERSIONING = OFF (AD8 revisitado).
Under UPDATE concurrente sobre misma fila, el engine arroja
"transaction time earlier than period start time" — limitación
conocida de Temporal Tables con alta contención de UPDATEs.
Decisión: secuencia es operacional, no configuración → sin history.
V013 y SqlTestFixture actualizados para ser idempotentes.
5. SqlTestFixture: EnsureV013SchemaAsync idempotente + PuntoDeVenta_History
en TablesToIgnore + permiso administracion:puntos_de_venta:gestionar
en seed canónico + asignación a rol admin.
6. Tests: conteos 22→23 permisos (V013 agrega uno); repository fixtures
ignoran PuntoDeVenta_History; test UpdatePdv_WhenPdvInactive eliminado
(over-specified — spec no bloquea update en PdV inactivo, solo en Medio
padre inactivo; alineado con frontend que permite editar PdV inactivo).
Resultado: 190/190 Api.Tests y tests específicos ADM-008 verdes
(Domain 13, Application 42, Api 21 = 76 tests nuevos). El único failure
residual (AuditEventRepositoryTests.QueryAsync_Limit_EmitsCursor) es
pre-existente y no relacionado a ADM-008.
Covers: verify report CRITICAL (UQ name mismatch) + WARNINGs descubiertos
durante la ejecución (DI registro, temporal tables concurrency, permiso
fixture, counts de tests pre-existentes).
8 endpoints en /api/v1/admin/puntos-de-venta con permiso administracion:puntos_de_venta:gestionar.
ExceptionFilter: +PuntoDeVentaNotFoundException (404), +PuntoDeVentaInactivoException (409), +NumeroAFIPDuplicadoException (409).
MedioInactivoException ya mapeado por ADM-001; no duplicado.
- IMedioRepository, ISeccionRepository interfaces
- MediosQuery, SeccionesQuery common records
- TipoSeccion static AllowedTipos helper
- Medios: 6 use cases (Create/Update/Deactivate/Reactivate/List/GetById) with validators, handlers and DTOs
- Secciones: 6 use cases mirroring Medios; Create validates MedioId active via IMedioRepository
- 52 unit tests (xUnit + NSubstitute) all green; audit LogAsync asserted per mutating handler
- DI registrations for all 12 handlers and validators auto-scanned via AddValidatorsFromAssemblyContaining
Entities sealed immutable con factory ForCreation + copy-with methods
WithUpdatedProfile/WithActivo (Codigo inmutable en Medio; MedioId y
Codigo inmutables en Seccion — enforzado en Validators en B4).
Exceptions: MedioCodigoDuplicado (UQ global), SeccionCodigoDuplicadoEnMedio
(UQ compuesto), MedioNotFound, SeccionNotFound. Todas heredan de
DomainException.
Agrega Quartz.Extensions.Hosting 3.13.1 al catálogo central.
SIGCM2.Infrastructure/Audit/Jobs/:
- AuditPartitionManagerJob — mensual (cron '0 0 2 1 * ?', UTC). Extiende
pf_AuditEvent_Monthly y pf_SecurityEvent_Monthly con SPLIT RANGE para el
mes+2 (mantiene +1 de buffer). Idempotente: verifica existencia antes.
- AuditRetentionEnforcerJob — anual (cron '0 0 3 1 1 ?', UTC). DELETE rows
> 10 años en AuditEvent y > 5 años en SecurityEvent. Temporal history se
purga solo vía HISTORY_RETENTION_PERIOD del engine.
- AuditIntegrityCheckJob — semanal domingos (cron '0 0 1 ? * SUN', UTC).
Valida SYSTEM_VERSIONING=ON + partitions próximos 3 meses. Emite
SecurityEvent 'system.integrity_alert' failure via ISecurityEventLogger
cuando detecta inconsistencias.
AuditMaintenanceRegistration.cs:
- services.AddAuditMaintenance(configuration) wraps AddQuartz + AddQuartzHostedService
con los 3 triggers crónicos.
Program.cs:
- builder.Services.AddAuditMaintenance(configuration) wired ONLY en entornos
productivos — skipeado en 'Testing' para que los integration tests no
disparen los triggers cron durante el ciclo de vida del TestWebAppFactory.
Row-based DELETE en RetentionEnforcerJob es la opción conservadora para la
primera generación — cuando los volúmenes lo justifiquen (>200M filas), se
upgradea a SWITCH OUT + DROP para partition-level drop. Documentado en
comentario de la clase.
Tests (Strict TDD, integration):
- AuditJobsTests (3): PartitionManager crea target boundary + idempotencia,
RetentionEnforcer purga > threshold (10y audit, 5y security), IntegrityCheck
all-OK no emite alert.
Suite: 381/381 Application.Tests + 147/147 Api.Tests = 528/528 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-6 #REQ-SEC-5, design, tasks#B11}
AuditController:
- GET /api/v1/audit/events?actorUserId&targetType&targetId&from&to&cursor&limit
- Protected by [RequirePermission("administracion:auditoria:ver")] — reuses
the existing permission (V005/V006 seed assigns it to admin).
- 400 on limit out of [1,100] or from > to.
- Cursor-based DESC pagination via AuditEventRepository.QueryAsync.
AuditHealthCheck (IHealthCheck):
- Validates SYSTEM_VERSIONING ON on Usuario/Rol/Permiso/RolPermiso.
- Validates partition boundaries exist for next 3 months (both AuditEvent and
SecurityEvent functions).
- Reports last audit event age (lenient 24h to accommodate dev/test quiet envs).
- Validates HISTORY_RETENTION_PERIOD == 10 YEARS on all 4 tables.
Key fix during impl: sys.tables.history_retention_period is stored in UNITS
(1=INFINITE, 3=DAY, 4=WEEK, 5=MONTH, 6=YEAR), NOT seconds. Assertion: period=10
AND unit=6 (10 YEARS).
- Mapped at /health/audit via app.MapHealthChecks with tag 'audit'.
Tests (Strict TDD, integration against SIGCM2_Test):
- AuditControllerTests (5): without-auth 401, without-permission 403 (cajero),
admin with filter returns events, invalid limit 400, from>to 400.
- AuditHealthCheckTests (1): returns Healthy with V010 applied.
Suite: 378/378 Application.Tests + 147/147 Api.Tests = 525/525 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-7/8, design, tasks#B10}