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

Merged
dmolinari merged 24 commits from feature/UDT-011 into main 2026-04-18 13:57:49 +00:00
Owner

UDT-011: Localización Temporal Argentina

Resumen

Fix transversal cross-layer del manejo de fechas y zonas horarias en SIG-CM2.0. Introduce el estándar Cat1/Cat2 (instantes UTC vs fechas civiles argentinas), el patrón TimeProvider inyectado en backend, la utility dateFormat.ts en frontend, las vistas SQL v_*_Local para queries admin, y la regla automática prescriptiva (4 artefactos). Cierra 5 bugs de frontend con impacto fiscal/legal.


Bugs fixeados

Bug Descripción Fix
BUG-FE-01 AuditPage.tsx usaba formatOccurredAt local sin timeZone — display incorrecto en UTC+x Reemplazado por formatInstant() de @/lib/dateFormat
BUG-FE-02 UsersTable, MedioDetailPage, SeccionDetailPage, PuntoDeVentaDetailPage — 4 funciones formatDate() locales duplicadas sin TZ Eliminadas, reemplazadas por formatInstantOrDash()
BUG-FE-03 TipoDeIvaFormModal + IngresosBrutosFormModalvigenciaDesde defaulteaba a string vacío en vez de hoy AR → vigencia incorrecta si se cargaba a las 22:30 ART todayArgentina() en defaultValues y reset create
BUG-FE-04 NuevaVigenciaModal + NuevaVigenciaIibbModalfechaCierre() usaba new Date().toISOString().slice(0,10) → UTC creep, mostraba día anterior prevCivilDate() + formatCivilDate() sin new Date()
BUG-FE-05 AuditFiltersdatetime-local input convertía a UTC con toISOString() → filtro con offset incorrecto parseArgentinaDateTimeToUtc() en toApiFilter

Decisiones arquitectónicas clave

  1. TimeProvider inyectadoTimeProvider (built-in .NET 8+) registrado como singleton en DI. Todos los handlers/infra/domain reciben TimeProvider por constructor. DateTime.UtcNow inline eliminado. Tests usan FakeTimeProvider (NuGet Microsoft.Extensions.TimeProvider.Testing).

  2. DateOnly Cat1/Cat2 — Backend: DateTime Kind=Utc para Cat1, DateOnly para Cat2. DateOnlyJsonConverter registrado globalmente (yyyy-MM-dd). Frontend sabe que fechas sin Z son Cat2 y NO deben pasar por new Date().

  3. dateFormat.ts utility — Único lugar del proyecto que formatea/parsea fechas en frontend. Exports: AR_TZ, formatInstant, formatInstantOrDash, formatCivilDate, formatCivilDateRange, todayArgentina, parseCivilDate, prevCivilDate, parseArgentinaDateTimeToUtc.

  4. Vistas V015dbo.v_AuditEvent_Local y dbo.v_SecurityEvent_Local en SIGCM2_Test. Aditivas (no rompen schema). Para queries admin en SSMS sin conversión manual.

  5. Regla automática 4 artefactos2.17 ⏰ Localización Temporal Argentina.md + sección ⏰ REGLA DE FECHAS Y ZONAS HORARIAS en INSTRUCCIONES_IA.md + sig-cm2/conventions/fechas-timezones en engram + sección ### Fechas y zonas horarias en skill-registry compact rules.


Scope

IN:

  • Backend: TimeProvider injection en todos los handlers + domain + infra
  • Backend: DateOnlyJsonConverter global
  • Backend: TimeProviderArgentinaExtensions (cross-platform Windows/Linux IANA)
  • Frontend: dateFormat.ts utility (Cat1 + Cat2 display + parse)
  • Frontend: BUG-FE-01..05 corregidos (5 componentes)
  • DB: V015 vistas v_*_Local en SIGCM2_Test
  • Docs: 2.17, INSTRUCCIONES_IA.md, engram conventions, skill-registry

OUT (explícitamente fuera de scope):

  • Quartz jobs (diferido a issue #24 — NestedScheduler + MaintenanceJob aún usan DateTime.UtcNow inline)
  • Aplicar V015 en SIGCM2 productivo local (decisión del equipo)

Tests

Baseline final: 1237/1237 (298 FE + 709 App + 230 Api)

Suite Count Tipo
Frontend Vitest 298/298 Unit + RTL
Application.Tests (xUnit) 709/709 Unit
Api.Tests (xUnit) 230/230 Integration

Tests nuevos escritos en UDT-011:

  • V015MigrationTests.cs — 5 tests (vistas timezone)
  • TimeProviderArgentinaExtensionsTests.cs — 4 tests
  • DateOnlyJsonConverterTests.cs — 4 unit + 1 integration
  • dateFormat.test.ts — 21 tests (14 originales + 7 nuevos funciones extra)
  • TipoDeIvaFormModal.test.tsx — BUG-FE-03 regression
  • IngresosBrutosFormModal.test.tsx — BUG-FE-03 regression

Acción requerida del reviewer

  • Si querés consultar dbo.v_AuditEvent_Local o dbo.v_SecurityEvent_Local en SSMS local, debés aplicar V015 manualmente:
    # En tu SIGCM2_Test local:
    sqlcmd -S . -d SIGCM2_Test -i database/migrations/V015__create_local_timezone_views.sql
    
  • Si solo mergeás el PR (sin consultar las vistas en SSMS), no hay acción requerida. Las vistas son aditivas y no afectan el runtime de la aplicación.

Follow-ups

  • #24 — Quartz jobs con TimeProvider (diferido, abierto como followup): NestedJobSchedulerService, AuditMaintenanceJob, y SecurityEventMaintenanceJob aún usan DateTime.UtcNow inline. UDT-011 NO cierra el #24. Requiere análisis adicional (Quartz IJobExecutionContext no inyecta TimeProvider directamente).

Artefactos SDD (engram, project: sig-cm2)

Artefacto Topic Key
Exploración sdd/udt-011-localizacion-temporal-argentina/explore
Propuesta sdd/udt-011-localizacion-temporal-argentina/proposal
Spec sdd/udt-011-localizacion-temporal-argentina/spec
Design sdd/udt-011-localizacion-temporal-argentina/design
Tasks sdd/udt-011-localizacion-temporal-argentina/tasks
Apply Progress sdd/udt-011-localizacion-temporal-argentina/apply-progress
PR Body sdd/udt-011-localizacion-temporal-argentina/pr-body
Convención sig-cm2/conventions/fechas-timezones

Métricas

  • Tasks SDD: 53
  • Commits: 23 sobre main (d4b2183)
  • Archivos cambiados: ~35 (backend + frontend + DB + docs)
  • Batches de implementación: 7 (Batch 1..6 + 7a local)

Smoke E2E ejecutado

Smoke corrido el 2026-04-18 contra http://localhost:5212 (SIGCM2 local) con credenciales admin válidas.

Endpoint HTTP Resultado
POST /api/v1/auth/login 200 JWT obtenido OK
GET /api/v1/admin/fiscal/iva 200 6 items, vigenciaDesde: "2026-04-18" Cat2 sin sufijo
POST /api/v1/admin/fiscal/iva (IVA_135, 13.5%) 201 id=7 creado, vigenciaDesde: "2026-04-18"
POST /api/v1/admin/fiscal/iva/7/nueva-version (14%, 2026-05-02) 201 id=8, predecesoraId=7
GET /api/v1/admin/fiscal/iva/8/historial 200 2 filas, cadena predecesorId=7→8
GET /api/v1/audit/events?limit=10 200 tipo_iva.create (id=22) + tipo_iva.nueva_version (id=23)
GET http://localhost:5173/ 200 Vite dev server OK

Verificación crítica DateOnlyJsonConverter (BUG-FE-03):

"vigenciaDesde": "2026-04-18"

Formato yyyy-MM-dd sin T, sin Z, sin offset. DateOnlyJsonConverter activo y funcionando.

AuditEvent ejemplo (id=23):

{
  "action": "tipo_iva.nueva_version",
  "occurredAt": "2026-04-18T13:40:00.053",
  "actorUsername": "admin",
  "targetId": "8",
  "metadata": "{\"predecesoraId\":7,\"nuevoId\":8,\"porcentajeNuevo\":14.0,\"vigenciaDesde\":\"2026-05-02\"}"
}

v_AuditEvent_Local: No aplicada en SIGCM2 productivo local (solo en SIGCM2_Test). Ver instrucciones en sección "Acción requerida del reviewer".

Build frontend (npm run build): 25 errores TS preexistentes en main (MedioForm, SeccionForm, pagination — deuda técnica anterior a UDT-011). UDT-011 no introduce ningún error TS nuevo. Diff de errores feature vs main: idénticos.

## UDT-011: Localización Temporal Argentina ### Resumen Fix transversal cross-layer del manejo de fechas y zonas horarias en SIG-CM2.0. Introduce el estándar Cat1/Cat2 (instantes UTC vs fechas civiles argentinas), el patrón `TimeProvider` inyectado en backend, la utility `dateFormat.ts` en frontend, las vistas SQL `v_*_Local` para queries admin, y la regla automática prescriptiva (4 artefactos). Cierra 5 bugs de frontend con impacto fiscal/legal. --- ### Bugs fixeados | Bug | Descripción | Fix | |-----|-------------|-----| | BUG-FE-01 | `AuditPage.tsx` usaba `formatOccurredAt` local sin `timeZone` — display incorrecto en UTC+x | Reemplazado por `formatInstant()` de `@/lib/dateFormat` | | BUG-FE-02 | `UsersTable`, `MedioDetailPage`, `SeccionDetailPage`, `PuntoDeVentaDetailPage` — 4 funciones `formatDate()` locales duplicadas sin TZ | Eliminadas, reemplazadas por `formatInstantOrDash()` | | BUG-FE-03 | `TipoDeIvaFormModal` + `IngresosBrutosFormModal` — `vigenciaDesde` defaulteaba a string vacío en vez de hoy AR → vigencia incorrecta si se cargaba a las 22:30 ART | `todayArgentina()` en `defaultValues` y `reset` create | | BUG-FE-04 | `NuevaVigenciaModal` + `NuevaVigenciaIibbModal` — `fechaCierre()` usaba `new Date().toISOString().slice(0,10)` → UTC creep, mostraba día anterior | `prevCivilDate()` + `formatCivilDate()` sin `new Date()` | | BUG-FE-05 | `AuditFilters` — `datetime-local` input convertía a UTC con `toISOString()` → filtro con offset incorrecto | `parseArgentinaDateTimeToUtc()` en `toApiFilter` | --- ### Decisiones arquitectónicas clave 1. **TimeProvider inyectado** — `TimeProvider` (built-in .NET 8+) registrado como singleton en DI. Todos los handlers/infra/domain reciben `TimeProvider` por constructor. `DateTime.UtcNow` inline eliminado. Tests usan `FakeTimeProvider` (NuGet `Microsoft.Extensions.TimeProvider.Testing`). 2. **DateOnly Cat1/Cat2** — Backend: `DateTime` Kind=Utc para Cat1, `DateOnly` para Cat2. `DateOnlyJsonConverter` registrado globalmente (`yyyy-MM-dd`). Frontend sabe que fechas sin `Z` son Cat2 y NO deben pasar por `new Date()`. 3. **`dateFormat.ts` utility** — Único lugar del proyecto que formatea/parsea fechas en frontend. Exports: `AR_TZ`, `formatInstant`, `formatInstantOrDash`, `formatCivilDate`, `formatCivilDateRange`, `todayArgentina`, `parseCivilDate`, `prevCivilDate`, `parseArgentinaDateTimeToUtc`. 4. **Vistas V015** — `dbo.v_AuditEvent_Local` y `dbo.v_SecurityEvent_Local` en `SIGCM2_Test`. Aditivas (no rompen schema). Para queries admin en SSMS sin conversión manual. 5. **Regla automática 4 artefactos** — `2.17 ⏰ Localización Temporal Argentina.md` + sección `⏰ REGLA DE FECHAS Y ZONAS HORARIAS` en `INSTRUCCIONES_IA.md` + `sig-cm2/conventions/fechas-timezones` en engram + sección `### Fechas y zonas horarias` en skill-registry compact rules. --- ### Scope **IN:** - Backend: TimeProvider injection en todos los handlers + domain + infra - Backend: DateOnlyJsonConverter global - Backend: TimeProviderArgentinaExtensions (cross-platform Windows/Linux IANA) - Frontend: dateFormat.ts utility (Cat1 + Cat2 display + parse) - Frontend: BUG-FE-01..05 corregidos (5 componentes) - DB: V015 vistas v_*_Local en SIGCM2_Test - Docs: 2.17, INSTRUCCIONES_IA.md, engram conventions, skill-registry **OUT (explícitamente fuera de scope):** - Quartz jobs (diferido a issue #24 — NestedScheduler + MaintenanceJob aún usan DateTime.UtcNow inline) - Aplicar V015 en SIGCM2 productivo local (decisión del equipo) --- ### Tests **Baseline final: 1237/1237** (298 FE + 709 App + 230 Api) | Suite | Count | Tipo | |-------|-------|------| | Frontend Vitest | 298/298 | Unit + RTL | | Application.Tests (xUnit) | 709/709 | Unit | | Api.Tests (xUnit) | 230/230 | Integration | Tests nuevos escritos en UDT-011: - `V015MigrationTests.cs` — 5 tests (vistas timezone) - `TimeProviderArgentinaExtensionsTests.cs` — 4 tests - `DateOnlyJsonConverterTests.cs` — 4 unit + 1 integration - `dateFormat.test.ts` — 21 tests (14 originales + 7 nuevos funciones extra) - `TipoDeIvaFormModal.test.tsx` — BUG-FE-03 regression - `IngresosBrutosFormModal.test.tsx` — BUG-FE-03 regression --- ### Acción requerida del reviewer - Si querés consultar `dbo.v_AuditEvent_Local` o `dbo.v_SecurityEvent_Local` en SSMS local, debés aplicar V015 manualmente: ```bash # En tu SIGCM2_Test local: sqlcmd -S . -d SIGCM2_Test -i database/migrations/V015__create_local_timezone_views.sql ``` - Si solo mergeás el PR (sin consultar las vistas en SSMS), **no hay acción requerida**. Las vistas son aditivas y no afectan el runtime de la aplicación. --- ### Follow-ups - **#24 — Quartz jobs con TimeProvider** (diferido, abierto como `followup`): `NestedJobSchedulerService`, `AuditMaintenanceJob`, y `SecurityEventMaintenanceJob` aún usan `DateTime.UtcNow` inline. UDT-011 NO cierra el #24. Requiere análisis adicional (Quartz `IJobExecutionContext` no inyecta `TimeProvider` directamente). --- ### Artefactos SDD (engram, project: sig-cm2) | Artefacto | Topic Key | |-----------|-----------| | Exploración | `sdd/udt-011-localizacion-temporal-argentina/explore` | | Propuesta | `sdd/udt-011-localizacion-temporal-argentina/proposal` | | Spec | `sdd/udt-011-localizacion-temporal-argentina/spec` | | Design | `sdd/udt-011-localizacion-temporal-argentina/design` | | Tasks | `sdd/udt-011-localizacion-temporal-argentina/tasks` | | Apply Progress | `sdd/udt-011-localizacion-temporal-argentina/apply-progress` | | PR Body | `sdd/udt-011-localizacion-temporal-argentina/pr-body` | | Convención | `sig-cm2/conventions/fechas-timezones` | --- ### Métricas - Tasks SDD: 53 - Commits: 23 sobre main (`d4b2183`) - Archivos cambiados: ~35 (backend + frontend + DB + docs) - Batches de implementación: 7 (Batch 1..6 + 7a local) --- ## Smoke E2E ejecutado Smoke corrido el 2026-04-18 contra `http://localhost:5212` (SIGCM2 local) con credenciales admin válidas. | Endpoint | HTTP | Resultado | |----------|------|-----------| | `POST /api/v1/auth/login` | 200 | JWT obtenido OK | | `GET /api/v1/admin/fiscal/iva` | 200 | 6 items, `vigenciaDesde: "2026-04-18"` ✅ Cat2 sin sufijo | | `POST /api/v1/admin/fiscal/iva` (IVA_135, 13.5%) | 201 | id=7 creado, `vigenciaDesde: "2026-04-18"` ✅ | | `POST /api/v1/admin/fiscal/iva/7/nueva-version` (14%, 2026-05-02) | 201 | id=8, predecesoraId=7 ✅ | | `GET /api/v1/admin/fiscal/iva/8/historial` | 200 | 2 filas, cadena predecesorId=7→8 ✅ | | `GET /api/v1/audit/events?limit=10` | 200 | `tipo_iva.create` (id=22) + `tipo_iva.nueva_version` (id=23) ✅ | | `GET http://localhost:5173/` | 200 | Vite dev server OK ✅ | **Verificación crítica DateOnlyJsonConverter (BUG-FE-03):** ```json "vigenciaDesde": "2026-04-18" ``` Formato `yyyy-MM-dd` sin `T`, sin `Z`, sin offset. DateOnlyJsonConverter activo y funcionando. **AuditEvent ejemplo (id=23):** ```json { "action": "tipo_iva.nueva_version", "occurredAt": "2026-04-18T13:40:00.053", "actorUsername": "admin", "targetId": "8", "metadata": "{\"predecesoraId\":7,\"nuevoId\":8,\"porcentajeNuevo\":14.0,\"vigenciaDesde\":\"2026-05-02\"}" } ``` **`v_AuditEvent_Local`:** No aplicada en SIGCM2 productivo local (solo en SIGCM2_Test). Ver instrucciones en sección "Acción requerida del reviewer". **Build frontend (`npm run build`):** 25 errores TS preexistentes en `main` (MedioForm, SeccionForm, pagination — deuda técnica anterior a UDT-011). UDT-011 no introduce ningún error TS nuevo. Diff de errores feature vs main: idénticos.
dmolinari added 23 commits 2026-04-18 13:43:38 +00:00
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.
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.
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.
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.
dmolinari added 1 commit 2026-04-18 13:56:16 +00:00
dmolinari merged commit 8d2618e6e5 into main 2026-04-18 13:57:49 +00:00
dmolinari deleted branch feature/UDT-011 2026-04-18 13:57:49 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dmolinari/SIG-CM2.0#25