Commit Graph

168 Commits

Author SHA1 Message Date
39160bbb83 feat(api): PuntosDeVentaController + ExceptionFilter mappings ADM-008
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.
2026-04-17 12:34:30 -03:00
489359f0b8 feat(infrastructure): PuntoDeVentaRepository con Dapper + mapping SqlException + registro DI 2026-04-17 12:29:16 -03:00
50f6f2b67a feat(application): repository abstraction + DTOs + validators + handlers CRUD PuntosDeVenta con auditoría + retry deadlock 2026-04-17 12:28:11 -03:00
43877bd4a1 feat(domain): entidad PuntoDeVenta + SecuenciaComprobante + TipoComprobante + excepciones 2026-04-17 12:21:45 -03:00
bef8977c5c feat(db): migration V013 + SP usp_ReservarNumeroComprobante para ADM-008
- Tabla PuntoDeVenta con Temporal Tables + UNIQUE(MedioId, NumeroAFIP)
- Tabla SecuenciaComprobante con Temporal Tables + UNIQUE(PdvId, TipoComprobante)
- Permiso administracion:puntos_de_venta:gestionar (guion_bajo: CK_Permiso_Codigo_Format)
- SP usp_ReservarNumeroComprobante con SERIALIZABLE + THROW 50001/50002/50003
- V013_ROLLBACK.sql incluido

Smoke tests SIGCM2_Test:
- TEST 1: primera reserva devuelve 1 (lazy init) OK
- TEST 2: segunda reserva devuelve 2 OK
- TEST 3: PdV inactivo -> SqlException 50001 'punto_de_venta_inactivo' OK
- TEST 4: Medio inactivo -> SqlException 50002 'medio_inactivo' OK

Covers: REQ-PDV-001/003/009, REQ-SEC-CMB-001/002/003/004
2026-04-17 12:16:56 -03:00
b7ac9831f9 Merge pull request '#18 fix(adm-001): cascada de inactividad Medio→Seccion'
Closes #16. Medio inactivo bloquea update/deactivate/reactivate de sus Secciones con 409 medio_inactivo. Fail-closed (sin AuditEvent si el throw sucede antes). Banner + disabled buttons en UI.
2026-04-17 14:50:03 +00:00
3829c93af6 test(secciones): cobertura cascada de inactividad — issue #16 2026-04-17 11:46:14 -03:00
4fb25356a3 feat(web): banner y disabled de secciones de medio inactivo — issue #16 2026-04-17 11:46:09 -03:00
455954fa98 feat(api): mapping 409 medio_inactivo en ExceptionFilter — issue #16 2026-04-17 11:46:05 -03:00
870cbe91b3 feat(secciones): validar medio activo en update/deactivate/reactivate — issue #16 2026-04-17 11:46:01 -03:00
1ad6633cdd feat(domain): MedioInactivoException (issue #16) 2026-04-17 11:45:56 -03:00
91d353655d Merge pull request '#15 ADM-001: Medios y Secciones (fundacional)'
ADM-001 entrega el catálogo fundacional Medio + Seccion con Temporal Tables, auditoría y frontend CMS (9 commits, 831 tests / 0 fails). Desbloquea 20+ UDTs downstream.
2026-04-17 14:37:15 +00:00
740298a9e1 fix(web): reemplazar <select> nativos por shadcn Select (dark mode compat) — ADM-001
Reemplaza 13 <select>/<option> nativos en 8 archivos por el componente
shadcn Select (Radix UI). Los selects nativos ignoraban los tokens del
design system en dark mode, causando texto invisible. Se agrega mock de
pointer capture APIs en test setup para compatibilidad de Radix con jsdom.
2026-04-17 10:13:20 -03:00
6b946f6080 feat(web): Medios + Secciones admin UI + hooks + routing — ADM-001 B7+B8
- API clients + TanStack Query hooks for medios and secciones
- CRUD pages: List, Create, Edit, Detail for both entities
- Components: MediosTable, MedioForm, DeactivateMedioModal,
  SeccionesTable, SeccionForm, DeactivateSeccionModal, SeccionesFilters
- TipoMedio enum (int→label) and TipoSeccion display helpers
- CanPerform permission gates: administracion:medios:gestionar,
  administracion:secciones:gestionar
- Routes /admin/medios/** and /admin/secciones/** in router.tsx
- Sidebar items (Newspaper + Columns3 icons) with requiredPermission
- Vitest+RTL+MSW tests: 11 test files, 38 new cases — 207 pass total
2026-04-16 19:28:30 -03:00
13480ad8c2 feat(api): MediosController + SeccionesController + ExceptionFilter mappings — ADM-001 B6
- POST/GET/PUT + deactivate/reactivate endpoints for /api/v1/admin/medios
- POST/GET/PUT + deactivate/reactivate endpoints for /api/v1/admin/secciones
- ExceptionFilter: add Medio/Seccion 404+409 mappings after RolInUseException
- Integration tests: 19 scenarios covering 401/403/201/404/409/idempotency/AuditEvent
- All 166 Api.Tests + 458 Application.Tests passing
2026-04-16 19:16:33 -03:00
a6f4011806 fix(tests): resolve ADM-001 regressions in Api.Tests fixture
- Update hardcoded permiso count from 21 → 22 in AuthControllerTests and
  PermisosEndpointTests after V011 added 'administracion:secciones:gestionar'
- The TestSupport SqlTestFixture already had Medio_History/Seccion_History in
  TablesToIgnore; tests were failing due to stale binaries (needed rebuild)
2026-04-16 19:08:32 -03:00
2f0da2d720 feat(infra): MedioRepository + SeccionRepository + integration tests — ADM-001 B5 2026-04-16 19:04:09 -03:00
a1a8e6e0cb fix(tests): realign test expectations with V011 (ADM-001) seed — 22 permisos + Medios fixture 2026-04-16 19:04:06 -03:00
f672de78ce feat(medios,secciones): application layer + handlers TDD — ADM-001 B3+B4
- 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
2026-04-16 18:53:57 -03:00
bb98dbf217 feat(domain): Medio + Seccion entities + 4 exceptions — ADM-001 B2
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.
2026-04-16 18:45:46 -03:00
ff7d8986fd feat(db): Medio + Seccion (temporal tables + seed) — ADM-001 B1
V011 crea dbo.Medio y dbo.Seccion con SYSTEM_VERSIONING ON (retention 10
anios) y PAGE compression en history; siembra el permiso
'administracion:secciones:gestionar' y lo asigna a rol admin. El permiso
'administracion:medios:gestionar' ya existia desde V005.

V012 siembra Medios fundacionales ELDIA y ELPLATA (MERGE idempotente).

Rollbacks V011/V012 validados estructuralmente; aplicacion y
reaplicacion verificadas en SIGCM2_Test y SIGCM2. Fixture de tests
actualizado: EnsureV011SchemaAsync, SeedMediosCanonicalAsync, ignora
Medio_History y Seccion_History en Respawner.
2026-04-16 18:13:54 -03:00
7c0646be0d Merge pull request 'UDT-010: Infraestructura de Auditoría y Trazabilidad — Closes #6' (#14) from feature/UDT-010 into main 2026-04-16 20:30:17 +00:00
9eac044752 feat(jobs): 3 audit maintenance jobs (Quartz.NET, UDT-010 B11)
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}
2026-04-16 17:10:43 -03:00
b526df2125 feat(web): /admin/audit page + filtros (UDT-010 B12)
Read-only audit timeline per design #D-9. Delegated to sub-agent, completed
before rate limit cutoff; verified with vitest 161/161 passing.

New files:
- src/web/src/api/audit.ts — axios client: listAuditEvents(filter)
- src/web/src/features/admin/audit/useAuditEvents.ts — TanStack Query hook
- src/web/src/pages/admin/audit/AuditPage.tsx — DataTable + 4 filters + cursor
  pagination 'Cargar más' button. Columns: OccurredAt (local time formatted),
  ActorUsername, Action (badge), TargetType + TargetId, IpAddress, CorrelationId
  (copy button with toast).
- src/web/src/pages/admin/audit/AuditFilters.tsx — 4 filters form.
- src/web/src/tests/features/admin/audit/useAuditEvents.test.ts — hook unit.
- src/web/src/tests/features/admin/audit/AuditPage.test.tsx — component test
  with MSW handler mock.

Modified:
- src/web/src/router.tsx — /admin/audit route, protected by auth + permission
  'administracion:auditoria:ver'.
- src/web/src/components/layout/AppSidebar.tsx — sidebar entry (icon, visible
  only with the required permission, uses existing permission-filtering pattern).

OUT of scope (deferred to ADM-004):
- Row drilldown modal with full metadata JSON formatted.
- CSV export.
- Timeline-per-entity visualization.

Design System v2.4 conventions respected: DataTable component from
@/components/ui/data-table (no raw <table>), tokens only (no hex inline),
density compact, Radix tooltip for copy button, sonner toast on copy.

Vitest run: 29 test files / 161 tests passing. No regressions in existing
frontend tests.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec, design#D-9, tasks#B12}
2026-04-16 17:07:13 -03:00
2bb90118ab feat(api): GET /audit/events + /health/audit (UDT-010 B10)
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}
2026-04-16 17:05:40 -03:00
b619c05762 feat(audit): security events en Auth + authorization handlers (UDT-010 B9)
Instruments auth pipeline with ISecurityEventLogger per #REQ-AUTH-SEC:

LoginCommandHandler:
- login success → action=login result=success actorUserId=user.Id
- login failure disaggregated internally (client still sees 401 unified):
  user_not_found / user_inactive / invalid_password
  — attempts captured with attemptedUsername + FailureReason

LogoutCommandHandler:
- action=logout result=success actorUserId=cmd.UsuarioId

RefreshCommandHandler:
- refresh.issue success on successful rotation
- refresh.reuse_detected failure when revoked token is presented (chain
  revoke already happens; we add the security event with metadata.familyId)
- refresh.issue failure for: token_expired / sub_mismatch / user_not_found /
  user_inactive

PermissionAuthorizationHandler:
- permission.denied failure on require-permission rejection, with metadata
  { permissionRequired, endpoint, method }. ActorUserId from JWT sub.

DI: ISecurityEventLogger was already registered by B6 (AddInfrastructure).

Test updates: 4 test classes now inject ISecurityEventLogger mock:
- LoginCommandHandlerTests, LogoutCommandHandlerTests, RefreshCommandHandlerTests
- PermissionAuthorizationHandlerTests (Api.Tests)

Suite: 378/378 Application.Tests + 141/141 Api.Tests = 519/519 passing.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-SEC-2/3/4/5 #REQ-AUTH-SEC,
design, tasks#B9}
2026-04-16 13:59:27 -03:00
a3f01bc6c9 feat(audit): enchufar audit en handlers de Rol (UDT-010 B8)
4 command handlers del módulo Roles + Permisos ahora auditan:

| Handler                              | Action                 |
|--------------------------------------|------------------------|
| CreateRolCommandHandler              | rol.create             |
| UpdateRolCommandHandler              | rol.update             |
| DeactivateRolCommandHandler          | rol.deactivate         |
| AssignPermisosToRolCommandHandler    | rol.permisos_update    |

Mismo patrón que B7 (using block + post-commit reads outside scope).

Metadata:
- rol.create: after={Codigo, Nombre, Descripcion}
- rol.update: {before, after} diff
- rol.permisos_update: {before, after} con arrays de codigos ordenados

AssignPermisosToRolCommandHandler captura 'before' leyendo
GetByRolCodigoAsync antes del TransactionScope para poder emitir el diff.

4 test classes actualizados con mock de IAuditLogger.

Suite: 378/378 Application.Tests + 141/141 Api.Tests = 519/519 passing.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-RM-AUD, design, tasks#B8}
2026-04-16 13:54:47 -03:00
26efb74c22 feat(audit): enchufar audit en handlers de Usuario — Closes #6
7 command handlers del módulo Usuarios ahora auditan via IAuditLogger:

| Handler                                 | Action                  |
|-----------------------------------------|-------------------------|
| CreateUsuarioCommandHandler             | usuario.create          |
| UpdateUsuarioCommandHandler             | usuario.update          |
| DeactivateUsuarioCommandHandler         | usuario.deactivate      |
| ReactivateUsuarioCommandHandler         | usuario.reactivate      |
| ChangeMyPasswordCommandHandler          | usuario.password_change |
| ResetUsuarioPasswordCommandHandler      | usuario.password_reset  |
| UpdateUsuarioPermisosOverridesHandler   | usuario.permisos_update |

Patrón por handler (per design #D-1):
  using (var tx = new TransactionScope(Required, ReadCommitted, AsyncFlowEnabled))
  {
      await repo.UpdateAsync(...);
      await audit.LogAsync(...);
      tx.Complete();
  }
  // post-commit reads OUTSIDE the using block
  var updated = await repo.GetDetailAsync(...);

Metadata captured:
- usuario.create: after={username, nombre, apellido, email, rol} — NO password.
- usuario.update: {before, after} diff of editable fields.
- usuario.password_reset: {targetId} only — tempPassword is NEVER persisted to
  audit (returned to caller once, never stored).
- usuario.permisos_update: {before, after} of grant/deny override lists.

Key fix during implementation: initially used 'using var tx = ...' (bare
declaration). This kept the TransactionScope active for the rest of the method,
causing 'The current TransactionScope is already complete' when post-commit
reads (GetDetailAsync) tried to enlist. Solution: explicit 'using (var tx = ...)
{ ... }' block that disposes the scope before post-commit reads.

AuditContextMissingException surfaces from AuditLogger when IAuditContext
lacks ActorUserId — fail-closed per #REQ-AUD-4. In integration tests, the
middleware populates ActorUserId from the JWT sub of the authenticated admin.

Test updates: 6 existing unit test classes now inject IAuditLogger mock:
- CreateUsuarioCommandHandlerTests
- UpdateUsuarioCommandHandlerTests
- DeactivateUsuarioCommandHandlerTests
- ReactivateUsuarioCommandHandlerTests
- ChangeMyPasswordCommandHandlerTests
- ResetUsuarioPasswordCommandHandlerTests

Follow-up #6 ([Auditoría] Registrar admin creador en alta de usuarios) is
closed: CreateUsuarioCommandHandler now records ActorUserId = admin JWT sub
on every user creation. TODO comment removed.

Suite: 378/378 Application.Tests + 141/141 Api.Tests = 519/519 passing.

Closes #6
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-UM-AUD, design, tasks#B7}
2026-04-16 13:49:44 -03:00
a3d6214d09 feat(infra): AuditLogger + SecurityEventLogger impl (UDT-010 B6)
Composes the audit emission layer per design #D-8:

SIGCM2.Infrastructure/Audit/AuditLogger.cs (IAuditLogger):
- Enriches from IAuditContext (ActorUserId/ActorRoleId/Ip/UserAgent/CorrelationId).
- Sanitizes metadata via JsonSanitizer + AuditOptions.SanitizedKeys.
- Persists via IAuditEventRepository.InsertAsync.
- Fail-closed: throws AuditContextMissingException when ActorUserId is null.
- Translates Guid.Empty correlation id to null (DB column is nullable; Empty
  indicates 'no middleware ran').
- Uses System.DateTime.UtcNow for occurredAt.

SIGCM2.Infrastructure/Audit/SecurityEventLogger.cs (ISecurityEventLogger):
- NOT fail-closed: null ActorUserId is valid (login failures, anonymous
  permission.denied events).
- Ip/UserAgent pulled from IAuditContext; metadata sanitized the same way.
- Persists via ISecurityEventRepository.

DI: AddScoped for both loggers in AddInfrastructure.

Tests (Strict TDD, mocks for IAuditContext/IAuditEventRepository/
ISecurityEventRepository):
- AuditLoggerTests (6): happy path with full context, fail-closed null actor,
  metadata sanitization, null metadata pass-through, repo-throws-bubbles-up
  (critical for TransactionScope rollback), custom SanitizedKeys from options.
- SecurityEventLoggerTests (4): login.success with context, login.failure
  with null actor + attemptedUsername, metadata sanitization,
  permission.denied with both actor and attemptedUsername null.

Two initial failures were fixed by replacing 'null' literal arguments in
NSubstitute Received(...) assertions with Arg.Is<T?>(x => x == null) —
NSubstitute does not always match null literals when mixed with Arg.Any<T>().

Suite: 378/378 Application.Tests + 141/141 Api.Tests = 519/519 passing.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-4 #REQ-SEC-2/3, design#D-8, tasks#B6}
2026-04-16 13:41:10 -03:00
300badda73 feat(infra): audit + security event repositories (UDT-010 B5)
Introduces persistence layer for audit and security events per design #D-6:

SIGCM2.Application/Audit/:
- IAuditEventRepository: InsertAsync + QueryAsync with cursor pagination
- ISecurityEventRepository: InsertAsync only (no query — SecurityEvent is
  queried only from an admin dashboard deferred to ADM-004)
- AuditEventQueryResult: (Items, NextCursor) record

SIGCM2.Infrastructure/Audit/:
- AuditEventCursor (public): base64(OccurredAt:O|Id) opaque cursor for
  DESC pagination. TryDecode is fail-open — malformed cursor returns null
  and the query starts from the top.
- AuditEventRepository: Dapper INSERT via OUTPUT INSERTED.Id + dynamic
  WHERE composition with parameterized filters (zero SQL injection risk).
  LEFT JOIN to dbo.Usuario to populate ActorUsername in AuditEventDto.
  Pagination fetches Limit+1 rows to detect "more pages"; emits cursor
  from the Nth row when overflow observed.
- SecurityEventRepository: straight INSERT for login/logout/refresh/
  permission.denied events.

DI: AddScoped for both repos in AddInfrastructure.

Integration tests (Strict TDD): 13 total, all against SIGCM2_Test.
- AuditEventRepositoryTests (10): insert-roundtrip, filter-by-actor,
  filter-by-target, filter-by-date-range, cursor pagination across 3 pages
  (no overlap/no gap), malformed-cursor fail-open, LEFT JOIN Usuario
  populates username, cursor encode/decode roundtrip, cursor malformed
  variants.
- SecurityEventRepositoryTests (3): insert success, insert failure with
  null ActorUserId + AttemptedUsername, CK_SecurityEvent_Result rejection.

Suite: 368/368 Application.Tests + 141/141 Api.Tests = 509/509 passing.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-2,7 #REQ-SEC-1,
design#D-6, tasks#B5}
2026-04-16 13:38:05 -03:00
0b4af4c332 feat(api): audit context middleware + scoped impl (UDT-010 B4)
Wires the request-scoped audit context per design #D-2:

Middleware pipeline in Program.cs:
  app.UseCors()
  app.UseMiddleware<CorrelationIdMiddleware>()  // PRE-AUTH
  app.UseAuthentication()
  app.UseMiddleware<AuditActorMiddleware>()     // POST-AUTH
  app.UseAuthorization()
  app.MapControllers()

SIGCM2.Api/Middleware/CorrelationIdMiddleware.cs:
- Preserves client-sent X-Correlation-Id header when a valid GUID, otherwise
  generates Guid.NewGuid(). Stores in HttpContext.Items (audit:correlationId).
- Captures Ip (Connection.RemoteIpAddress) + UserAgent header into Items.
- Echoes the correlation id back via response header (OnStarting + immediate
  set — immediate set makes unit testing against DefaultHttpContext reliable).

SIGCM2.Api/Middleware/AuditActorMiddleware.cs:
- Reads JWT 'sub' claim from authenticated HttpContext.User, parses to int,
  stores as audit:actorUserId. Anonymous / non-numeric sub leaves it unset.

SIGCM2.Infrastructure/Audit/AuditContext.cs (IAuditContext scoped impl):
- Reads Items entries via IHttpContextAccessor. Returns null / Guid.Empty
  when no HttpContext is available (jobs, tests without middleware).
- ActorRoleId intentionally null for now — rol code → id resolution is
  deferred; the logger may resolve it at persist time in a later batch.

DI registration (Infrastructure/DependencyInjection.cs):
- services.AddScoped<IAuditContext, AuditContext>()

Tests (Strict TDD):
- CorrelationIdMiddlewareTests (6): generates/preserves/handles-malformed
  correlation id, sets response header, captures ip/ua, calls next.
- AuditActorMiddlewareTests (5): authenticated/anonymous/no-sub/non-numeric/
  calls-next.
- AuditContextTests (7): reads from Items, null-http-context defaults,
  ActorRoleId currently null.

Suite: 355/355 Application.Tests + 141/141 Api.Tests = 496/496 passing.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-3/9, design#D-2, tasks#B4}
2026-04-16 13:32:13 -03:00
08d6622e43 feat(infra): JsonSanitizer + AuditOptions binding (UDT-010 B3)
Adds the metadata sanitization layer per #REQ-AUD-5:

SIGCM2.Infrastructure/Audit/JsonSanitizer.cs (static class):
- Sanitize(object?, IReadOnlyCollection<string>) -> string?
- Serializes via System.Text.Json + JsonNode recursive traversal.
- Strips blacklisted keys at every nesting level (objects + arrays).
- Case-insensitive match (ToLowerInvariant on both sides).
- Null input -> null output (never throws).
- Output is always valid JSON (ISJSON=1 compatible — satisfies AuditEvent CHECK).

SIGCM2.Application/Audit/AuditOptions.cs:
- Documented the IConfiguration array-binding quirk: config is ADDITIVE
  (append at higher indices), not REPLACE. Intentional for security — defaults
  like 'password'/'token'/'cvv' must not be silently dropped.

SIGCM2.Infrastructure/DependencyInjection.cs:
- services.Configure<AuditOptions>(configuration.GetSection(AuditOptions.SectionName))
  wired in AddInfrastructure().

Tests (Strict TDD, RED -> GREEN):
- JsonSanitizerTests (10): null/empty-blacklist/flat/nested/arrays/case-insensitive/
  primitives/round-trip-valid-json/string-as-value/default-keys-effective.
- AuditOptionsBindingTests (2): defaults when section absent + additive override.

One test needed adjustment during GREEN: 'AlreadySerializedJsonString' originally
asserted against an encoding-specific literal; rewrote to use JsonDocument
round-trip (validates behavior without coupling to encoder quirks).

Suite: 348/348 Application.Tests + 130/130 Api.Tests = 478/478 passing.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-5, design#D-5, tasks#B3}
2026-04-16 13:28:37 -03:00
68f96b90c7 feat(application): audit abstractions (UDT-010 B2)
Introduces the contract layer for audit logging per design #D-8:

SIGCM2.Application/Audit/:
- IAuditContext — request-scoped accessor for ActorUserId/ActorRoleId/
  Ip/UserAgent/CorrelationId. Populated by CorrelationIdMiddleware +
  AuditActorMiddleware (B4).
- IAuditLogger.LogAsync(action, targetType, targetId, metadata?, ct) —
  domain-level audit emitter. Enlists in ambient TransactionScope
  (fail-closed per #REQ-AUD-4).
- ISecurityEventLogger.LogAsync(action, result, actorUserId?, attemptedUsername?,
  sessionId?, failureReason?, metadata?, ct) — security-events emitter
  separate from IAuditLogger (different retention, no transaction scope,
  captures login/logout/refresh/permission.denied).
- AuditOptions — bindable POCO with SanitizedKeys[] defaults (used by
  JsonSanitizer in B3).
- AuditEventDto — read projection for GET /api/v1/audit/events (B10).
- AuditEventFilter — query filter record with default Limit=50.

SIGCM2.Domain/Exceptions/:
- AuditContextMissingException : DomainException — fail-closed sentinel
  thrown when IAuditLogger is called without ActorUserId in a user-scoped
  command (#REQ-AUD-4).

Tests (Strict TDD — shape contract, RED → GREEN):
- tests/SIGCM2.Application.Tests/Audit/AuditAbstractionsTests.cs: 11 tests
  covering nullability, signatures, default options, record equality.

Suite: 336/336 Application.Tests (prev 325 + 11 new). 130/130 Api.Tests.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-3/4/5, design#D-8, tasks#B2}
2026-04-16 13:23:11 -03:00
c95bc7fe01 fix(tests): extend Respawn + collection config for UDT-010 temporal tables
Follow-up of B1 (V010 migration). Issues found when running the full suite
cross-assembly:

1. Respawn 'Cannot delete rows from a temporal history table' error:
   4 per-class Respawner configs in SIGCM2.Application.Tests did not
   include the newly-created *_History tables introduced by V010
   (Usuario_History / Rol_History / Permiso_History / RolPermiso_History).
   The engine rejects direct DELETE on system-versioned history tables.
   Extended TablesToIgnore in all 4 configs.

2. FK_RefreshToken_Usuario violation in RolRepositoryTests.InitializeAsync:
   Manual 'DELETE FROM Usuario' failed when residual RefreshTokens from
   prior suites existed. Added 'DELETE FROM RefreshToken' before the
   Usuario cleanup to respect FK order. Latent bug surfaced by a new
   test-run ordering — not UDT-010 specific, but fixed in scope.

3. UQ_Usuario_Username duplicate admin race:
   TransactionScopeSpikeTests (B0) and V010MigrationTests (B1) were
   missing [Collection("ApiIntegration")], causing them to run in
   parallel with the rest of SIGCM2.Api.Tests and race on SeedAdmin.
   Serialized by adding the Collection attribute.

Suite now passes cross-assembly: 130/130 Api.Tests + 336/336 Application.Tests.

Refs: sdd/udt-010-auditoria-trazabilidad/apply-progress (B1 follow-up)
2026-04-16 13:22:56 -03:00
1c79dfa0a4 feat(db): V010 audit infrastructure + temporal tables
Applied to SIGCM2 (dev) and SIGCM2_Test.

V010__audit_infrastructure.sql (idempotent, ~280 LoC):
- Filegroups AUDIT_HOT + AUDIT_COLD with physical files (per-DB logical names
  via DB_NAME() prefix to avoid collision in dev/test).
- pf/ps_AuditEvent_Monthly + pf/ps_SecurityEvent_Monthly (RANGE RIGHT,
  DATETIME2(3), 14 boundaries 2026-01..2027-02 → 15 partitions). Job extends
  forward monthly in B11.
- dbo.AuditEvent (partitioned, clustered PK on OccurredAt+Id) + 4 indexes
  (Actor/Target/Action/Correlation) with PAGE compression.
- dbo.SecurityEvent (partitioned) + 3 indexes (Actor/Action_Result/Ip_Failure).
- CHECK constraints: Action LIKE '%.%', ISJSON(Metadata), Result IN (success|failure).
- SYSTEM_VERSIONING ON in Usuario/Rol/Permiso/RolPermiso with 10 YEARS retention +
  PAGE compression in history tables.
- No hard FK on ActorUserId → Usuario.Id (soft FK — audit must survive user deletion).

V010_ROLLBACK.sql: emergency reversal (WARNING: destroys all audit history).

database/README.md: migration order + V010 prod-apply notes.

tests/SIGCM2.TestSupport/SqlTestFixture.cs:
- EnsureV010SchemaAsync() validates audit infra is applied (fails fast with
  clear message if not — migration itself requires ALTER DATABASE privileges
  and is applied manually via sqlcmd).
- Respawn TablesToIgnore extended with *_History (engine rejects direct DELETE
  on system-versioned history tables).

tests/SIGCM2.Api.Tests/Audit/V010MigrationTests.cs — 5 smoke tests:
- AuditEvent insert+roundtrip with CorrelationId.
- CK_AuditEvent_Action rejects Action without '.'.
- CK_AuditEvent_Metadata rejects non-JSON.
- CK_SecurityEvent_Result rejects invalid Result.
- Usuario SYSTEM_VERSIONING: temporal query FOR SYSTEM_TIME AS OF returns
  pre-update state + Usuario_History populated.

Suite: 130/130 passing (previous 124 + spike B0 + 5 new B1). No regressions.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-1,2, #REQ-SEC-1,
design#D-4, tasks}
2026-04-16 13:10:04 -03:00
2d1d187f6e chore(udt-010): bootstrap rama + spike anti-MSDTC
Validates design decision #D-1 (TransactionScope ambient over IUnitOfWork):
TransactionScope with TransactionScopeAsyncFlowOption.Enabled does NOT
escalate to MSDTC when multiple SqlConnections share the same connection
string. Test passes (DistributedIdentifier == Guid.Empty).

Unblocks UDT-010 batches B1-B14.

Refs: sdd/udt-010-auditoria-trazabilidad/{design,tasks}
2026-04-16 12:56:17 -03:00
d201d9e08e Merge pull request 'Design System: bootstrap tokens + paleta brand El Día' (#13) from chore/design-system-tokens into main 2026-04-16 15:00:26 +00:00
fa76d0055a feat(web): infra UI completa pre-ADM-001 — DataTable + 8 shadcn + MCP global
shadcn MCP server registrado globalmente (claude mcp add -s user shadcn -- npx -y shadcn@latest mcp). Disponible en cualquier sesion/proyecto.

8 componentes shadcn nuevos via CLI (cd src/web && npx shadcn@latest add ...):
- table, select, popover, pagination, breadcrumb, alert-dialog
- command, dialog (deps de combobox y alert-dialog)
Total instalados ahora: 22

Fix gotcha shadcn CLI: agregado compilerOptions.paths al root tsconfig.json
(sino crea folder literal '@/' en lugar de resolver el alias). Antes solo
estaba en tsconfig.app.json que el CLI no lee.

@tanstack/react-table 8.21 instalado.

Nuevo componente <DataTable> generico (src/web/src/components/ui/data-table.tsx):
- Wrapper sobre TanStack Table
- Priority columns: meta { priority: 'high' | 'medium' | 'low' }
  → hidden md:table-cell / hidden lg:table-cell automatico
- Tap-to-expand row mobile (chevron auto-aparece cuando hay cols hidden,
  click despliega panel con hidden cells como dl/dt/dd)
- Loading state con DataTableSkeleton
- Empty state customizable
- onRowClick callback con stop-propagation correcto en chevron
- 14 tests cubriendo todas las features

Refactor UsersTable a DataTable como dogfood (mismo output visual,
columnas con priority alta/media/baja). 150 tests frontend totales verde.

Doc Obsidian 2.14 v2.4 actualizado con seccion DataTable completa,
componentes ampliados a 22, MCP global, y gotcha del tsconfig.

Engram sig-cm2/design-system actualizado a v2.4.

Skill registry actualizado con compact rules de DataTable y MCP.
2026-04-16 11:54:14 -03:00
5f7d9e6b89 docs(skill-registry): actualizar compact rules design system v2.3
Sincroniza con doc Obsidian 2.14 v2.3 y engram sig-cm2/design-system:
- Agrega violet accent y nueva guidance
- Density bumpeada a 40px (input h-10), radius 10px base
- Utilities CSS (.glass/.gradient-mesh/.grid-bg/.surface/.focus-glow)
- Card variants ampliados
- Tooltip Radix obligatorio (no CSS absolute en sidebars)
- Sidebar colapsable con useSidebar
- TooltipProvider y Toaster ya en App.tsx
- Browser autofill fix mencionado
2026-04-16 11:32:07 -03:00
83d76b95d4 feat(web): tooltips Radix + toggle sidebar al lado del brand
Cambios:
- Instalado @radix-ui/react-tooltip 1.2.8 (componente faltante de
  shadcn/ui que faltaba en el set inicial).
- Nuevo src/web/src/components/ui/tooltip.tsx (shadcn pattern):
  TooltipProvider, Tooltip, TooltipTrigger, TooltipContent con
  animaciones data-state (fade + zoom + slide direccional).
- App.tsx: TooltipProvider envuelve toda la app con delayDuration 150ms.
- AppSidebar refactorizado:
  - Toggle button MOVIDO al header (top), al lado izquierdo del nombre
    'SIG-CM 2.0'. Eliminado boton bottom (era redundante).
  - Cuando collapsed: solo el toggle visible centrado (68px width).
  - Cuando expanded: [Toggle] [SIG-CM 2.0] aligned left.
  - Quitado overflow-hidden del aside (era lo que impedia que los
    tooltips fueran visibles — los clipping containers padres tampoco
    importan ahora porque Radix portalea el tooltip a body).
  - Tooltips en TODOS los items collapsed (incluido el toggle) y en
    items disabled muestra 'Label · Próximamente'.
  - Eliminado el componente CSS-only SidebarTooltip (reemplazado por
    Radix que se renderiza fuera del DOM tree con Portal).

El bug original era que tanto el aside con overflow-hidden como el
ProtectedLayout con overflow-hidden clipean cualquier elemento que
intente escapar via absolute positioning. Radix Portal soluciona
eso renderizando el tooltip en document.body.

Tests 136/136 verde.
2026-04-16 11:26:41 -03:00
7b7ef1c137 feat(web): sidebar colapsable con tooltips + fix scroll horizontal
Cambios:
- Nuevo hook useSidebar() con persistencia en localStorage
  ('sidebar-collapsed' = '1'/'0').
- SidebarNav refactorizado:
  - Width controlled internamente (w-60 expanded, w-[68px] collapsed)
  - Toggle button al pie con PanelLeftClose/Open icon
  - Brand mark con gradient brand+violet (consistencia con login)
  - Active indicator: barra vertical sutil a la izquierda cuando expanded,
    bg accent cuando collapsed
  - SectionLabel se reemplaza por divider sutil cuando collapsed
- Custom SidebarTooltip puro CSS (sin radix dep nuevo) que aparece
  a la derecha del item con animacion fade + slide al hover. Funciona
  con group-hover/item y group-hover/toggle (Tailwind named groups).
- Items disabled muestran badge 'Próx.' chico (era 'Próximamente' largo)
  y en tooltip cuando collapsed: 'Label · Próximamente'.
- Fix scroll horizontal: overflow-x-hidden en nav, truncate en spans,
  shrink-0 en iconos y badges. Layout robusto a labels largos.
- ProtectedLayout deja de hardcodear lg:w-60 — sidebar controla su width.
- AppHeader Sheet (mobile) usa <SidebarNav forceExpanded /> para que
  en mobile siempre se vea expanded sin importar el state desktop.

Tests 136/136 verde.
2026-04-16 11:21:42 -03:00
41b6882b5c feat(web): mas contraste cards/tables sobre bg + utility .surface
Cambios de tokens:
- Light --background: 0.988 -> 0.962 (slate cool, hace pop el card white)
- Dark --background: 0.135 -> 0.130 (mas oscuro)
- Dark --card: 0.180 -> 0.220 (salto +0.090 vs bg, antes solo +0.045)
- Dark --popover: 0.200 -> 0.245 (popovers/dropdowns aun mas elevados)
- Dark --secondary/muted/accent/input: bumpeados al nivel correspondiente
  para que la jerarquia visual mantenga proporciones

Card variants:
- default: shadow-sm -> shadow-md (mas elevacion default)
- nuevo variant 'flat' (sin shadow) para cuando se necesite

Nueva utility .surface:
- Mismo treatment visual que <Card variant='default'> pero como clase
  CSS — para contenedores que no usan el componente Card (ej: tablas,
  listas custom). bg-card + border + radius + shadow-md.

UsersTable refactorizado para usar .surface en lugar de border manual.
Cualquier futura tabla/lista usa .surface por consistencia.

Tests 136/136 verde.
2026-04-16 11:15:36 -03:00
278e1cf378 fix(web): light mode profundidad + grid global + autofill fix
Cambios:
- index.css: fix de browser autofill (Chrome/Safari forzaba bg amarillo +
  texto blanco que rompia contraste). Override -webkit-text-fill-color
  + box-shadow inset para mantener tokens del DS. Esta era la causa real
  de las 'letras blancas en gris' que se veian en login.
- index.css: utility .grid-bg global (7% opacity light, 10% dark) — para
  usar como fondo cuadriculado en todos los layouts.
- PublicLayout: agrego .grid-bg layer + bg-background explicito + glow
  blobs mas intensos (25%/20% en light vs 10% antes). Light ahora
  tiene la misma profundidad visual que dark.
- ProtectedLayout: agrego .grid-bg + glow blobs sutiles en corners para
  dar profundidad al dashboard y todas las secciones internas. Resalta
  futuros componentes glass.

Tests: 136/136 verde.
2026-04-16 11:10:06 -03:00
3bc2625e21 fix(web): agregar ThemeToggle en PublicLayout (login)
El ThemeToggle solo vivia en AppHeader (ProtectedLayout), por lo que
desde /login era imposible cambiar el tema. Movido a esquina superior
derecha con z-index 20 sobre el gradient mesh.

useTheme defaultea a system preference, pero el usuario tiene que poder
override desde cualquier pantalla — incluido el login.
2026-04-16 11:05:39 -03:00
6e6c729bac feat(web): design system v2 — tech sophisticated con glass + gradient mesh
Cambios principales:
- Agregado violet accent (oklch 0.62 0.20 280) para combo tech con brand cyan
- Neutrals con shift sutil hacia hue 250 (slate-violet)
- Dark mode con bg oklch(0.135 0.018 252) — no pure black, feel mas tech
- Inputs con token --input propio (white en light, elevado en dark) y --input-border mas prominente. Fixea problema de input gris feo
- Card soporta variant glass/elevated/default
- Multi-layer shadows reales (shadow-sm/md/lg/xl/glow)
- Gradient mesh utility (.gradient-mesh + token --gradient-mesh)
- Clase .glass para glassmorphism (backdrop-blur 20px + saturate 180%)
- Border radius default 10px (era 8px) — mas moderno
- Headings con tracking-tight -0.015em

LoginPage redesigned:
- PublicLayout con gradient mesh + 2 glow blobs (brand+violet) + grid sutil
- Card variant glass para el form
- Logo mark con bg-gradient-to-br from-brand-500 to-violet-500
- Inputs con bg propio + ring brand glow al focus

Tests: 136/136 verde.
Doc Obsidian 2.14 actualizado v2.0. Engram sig-cm2/design-system actualizado.
2026-04-16 11:02:59 -03:00
c488e2430d feat(web): bootstrap design system con paleta brand El Dia
- Reemplazo de tokens HSL por OKLCH (Tailwind 4 native)
- Brand color #008fbe escalado a brand-50..950
- Neutral cool slate (complementa brand)
- Semantic: success/warning/destructive como tokens
- Background tinted off-white (no pure white) para warmth
- Dark mode usa neutral-950 (no pure black)
- Brand utilities expuestos via @theme inline (bg-brand-500, etc)
- Focus rings con ring brand color
- Selection con brand-200/800

Skill registry actualizado con compact rules de design system para auto-inyeccion en sub-agents.

Source of truth: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.14 Design System.md
Engram: sig-cm2/design-system
2026-04-16 10:46:07 -03:00
492705c076 Merge pull request 'UDT-009: Overrides de PermisosJson por usuario — cierre módulo Auth' (#12) from feature/UDT-009 into main 2026-04-16 13:12:23 +00:00
6822d56e11 fix(web): montar Toaster + feedback toast en PermisosEditor [UDT-009]
Sonner estaba como dependencia pero el componente Toaster nunca se monto
en el arbol de la app. ChangeMyPasswordPage ya usaba toast() pero no
mostraba nada visualmente. Agregado <Toaster richColors closeButton /> en
App.tsx (top-right) y toasts de exito/error en PermisosEditor.handleSave
para confirmar al usuario que el cambio se persistio.
2026-04-16 10:11:04 -03:00
a30b10ebff fix(web): UsuarioPermisos shape nested para matchear backend [UDT-009]
El backend devuelve { rolPermisos, overrides: {grant, deny}, effective }
(nested) segun spec, pero el frontend lo lee como {grant, deny} planos.
Causaba TypeError: permisoData.grant is not iterable al abrir tab Permisos.
Tests del frontend actualizados con el shape correcto.
2026-04-16 10:06:43 -03:00
b7882613a4 feat(web): UserEditPage con tabs Perfil+Permisos [UDT-009] 2026-04-15 21:50:55 -03:00