ADM-009: Tablas Fiscales (IVA + IIBB) — append-only versioned ref data #22

Merged
dmolinari merged 36 commits from feature/ADM-009 into main 2026-04-18 11:45:13 +00:00
Owner

Resumen

ADM-009 provee los maestros fiscales (TipoDeIva, IngresosBrutos) que alimentan toda la cadena de facturación. Implementado con pattern append-only versioned ref data para garantizar trazabilidad fiscal correcta en NC/ND sobre facturas históricas.

Scope (in)

BD — Migración V014

  • Tablas TipoDeIva + IngresosBrutos con Temporal Tables (SYSTEM_VERSIONING + retention 10 años).
  • Columna PredecesorId INT NULL con FK self para cadena de versiones.
  • Seed obligatorio: 4 TipoDeIva canónicos (EXENTO, NO_GRAVADO, IVA_105, IVA_21) + 24 provincias (23 INDEC + CABA).
  • Permiso administracion:fiscal:gestionar + asignación a rol admin.

Backend — Clean Architecture

  • Domain: entidades sealed + factories + NuevaVersion() retorna tupla (predecesora cerrada, nueva). Porcentaje/Alicuota INMUTABLES (no hay WithPorcentaje).
  • Application: 16 handlers CQRS (8 IVA + 8 IIBB) con TransactionScope(ReadCommitted, AsyncFlow) + IAuditLogger fail-closed.
  • Infrastructure: repos Dapper con raw SQL, OUTPUT INSERTED.Id, CTE recursivo con OPTION (MAXRECURSION 100) para historial, guard optimistic WHERE VigenciaHasta IS NULL en cierre de vigencia.
  • API: FiscalController con 16 endpoints (/api/v1/admin/fiscal/{iva,iibb}/*), defensa raw-body en PATCH contra Porcentaje/Alicuota, ExceptionFilter con contrato unificado { error, message }.

Frontend — Feature-sliced

  • src/web/src/features/fiscal/{iva,iibb}/ con TanStack Query (staleTime: 15_000) + react-hook-form + zod v4.
  • Modal "Editar" sin campo Porcentaje/Alicuota (UI-level enforcement de inmutabilidad).
  • Modal "Nueva vigencia" con preview + confirmación explícita + color distinto.
  • Tooltip de historial con single request al backend (no N+1).
  • Banner global de advertencia antes de mutaciones.
  • Tokens --warning-bg + --warning-border agregados al Design System (v2.5).

Scope (out — explícito)

  • Regla RNI 50% → DIFERIDA a issue #20. Bloquea FAC-*, NO a ADM-009. Requiere confirmación contable/legal.
  • Snapshot de Porcentaje en Presupuesto/Aviso → trabajo de FAC-/VTA-. Este PR deja contrato establecido: FAC-* debe guardar TipoDeIvaId como FK estable + Porcentaje como snapshot defensivo.
  • Código AFIP en TipoDeIva → se agrega cuando INT-001 lo consuma.
  • ADM-006 (Configuración Global) → independiente; ADM-009 no lo requiere.

Decisión arquitectónica clave — append-only

TipoDeIva.Porcentaje e IngresosBrutos.Alicuota son INMUTABLES una vez creados. Cambiar el valor numérico NO es un PATCH — es un POST /{iva|iibb}/{id}/nueva-version que:

  1. Crea nueva fila con PredecesorId = {id}.
  2. Cierra predecesora con VigenciaHasta = nuevaVigencia - 1 día.
  3. Registra evento de auditoría tipo_iva.nueva_version / ingresos_brutos.nueva_version.
  4. Todo en un TransactionScope atómico — fail-closed si audit throws.

Razón: una NC/ND en 2028 contra factura 2025 debe recalcular con la alícuota de 2025, no la vigente hoy. Append-only convierte TipoDeIvaId en token fiscal permanente que FAC-*/INT-001 pueden consumir ciegamente.

Tests

Suite Resultado
SIGCM2.Application.Tests 704 / 704
SIGCM2.Api.Tests 220 / 220
Frontend (Vitest + RTL + MSW) 275 / 275
TOTAL 1199 / 1199

TDD estricto: Red → Green → Refactor en cada capa. Incluye test de reflection que verifica que WithPorcentaje/WithAlicuota NO existen en las entidades (defensa en profundidad contra regresiones de mutabilidad).

Smoke E2E realizado

  • Login + JWT
  • Create IVA_99 27% → 201
  • Nueva versión 28.5% vigente 2026-05-01 → 201
  • Historial → cadena [5, 6] con PredecesorId correcto
  • PATCH con porcentaje en body → 409 inmutable_usar_nueva_version
  • AuditEvent: 2 entradas (tipo_iva.create + tipo_iva.nueva_version) con actor + metadata

⚠️ Acción requerida del reviewer

Antes de mergear — aplicar V014 en tu entorno local:

# Aplicar migración:
sqlcmd -S TECNICA3 -d SIGCM2 -i database/migrations/V014__create_tablas_fiscales.sql

# Para rollback si lo necesitás:
sqlcmd -S TECNICA3 -d SIGCM2 -i database/migrations/V014_ROLLBACK.sql

La base de tests SIGCM2_Test se aplica automáticamente vía SqlTestFixture.EnsureV014SchemaAsync().

Follow-ups abiertos (NO cerrar con este PR)

  • #20[ADM-009] OQ-008: definir si recargo RNI 50% vive en Configuracion o constante de dominio. Bloquea FAC-* hasta confirmación contable/legal.
  • #21[ADM-008] Migrar PuntoDeVentaForm a sintaxis Zod v4 (bug preexistente). Bug latente en main desde ADM-008. Este PR solo migra los 4 componentes fiscales de ADM-009.

Artefactos SDD (engram — project: sig-cm2)

Fase Topic key
Explore sdd/adm-009-tablas-fiscales/explore
Proposal v2 sdd/adm-009-tablas-fiscales/proposal
Spec sdd/adm-009-tablas-fiscales/spec
Design sdd/adm-009-tablas-fiscales/design
Tasks sdd/adm-009-tablas-fiscales/tasks
Apply progress sdd/adm-009-tablas-fiscales/apply-progress
Decisions sdd/adm-009-tablas-fiscales/decisions
Tech debt sdd/adm-009-tablas-fiscales/tech-debt

Métricas

  • 91 tasks SDD ejecutadas en 7 batches con Strict TDD.
  • 36 commits atómicos.
  • 2 tech debts eliminados en cleanup pre-merge (V014 seed PascalCase + V014MigrationTests filtros específicos).
  • 1 bug preexistente detectado y documentado como follow-up (#21).
## Resumen ADM-009 provee los maestros fiscales (`TipoDeIva`, `IngresosBrutos`) que alimentan toda la cadena de facturación. Implementado con pattern **append-only versioned ref data** para garantizar trazabilidad fiscal correcta en NC/ND sobre facturas históricas. ## Scope (in) **BD — Migración V014** - Tablas `TipoDeIva` + `IngresosBrutos` con Temporal Tables (SYSTEM_VERSIONING + retention 10 años). - Columna `PredecesorId INT NULL` con FK self para cadena de versiones. - Seed obligatorio: 4 TipoDeIva canónicos (EXENTO, NO_GRAVADO, IVA_105, IVA_21) + 24 provincias (23 INDEC + CABA). - Permiso `administracion:fiscal:gestionar` + asignación a rol `admin`. **Backend — Clean Architecture** - Domain: entidades sealed + factories + `NuevaVersion()` retorna tupla `(predecesora cerrada, nueva)`. `Porcentaje`/`Alicuota` INMUTABLES (no hay `WithPorcentaje`). - Application: 16 handlers CQRS (8 IVA + 8 IIBB) con `TransactionScope(ReadCommitted, AsyncFlow)` + `IAuditLogger` fail-closed. - Infrastructure: repos Dapper con raw SQL, `OUTPUT INSERTED.Id`, **CTE recursivo con `OPTION (MAXRECURSION 100)`** para historial, guard optimistic `WHERE VigenciaHasta IS NULL` en cierre de vigencia. - API: `FiscalController` con 16 endpoints (`/api/v1/admin/fiscal/{iva,iibb}/*`), **defensa raw-body** en PATCH contra `Porcentaje`/`Alicuota`, ExceptionFilter con contrato unificado `{ error, message }`. **Frontend — Feature-sliced** - `src/web/src/features/fiscal/{iva,iibb}/` con TanStack Query (`staleTime: 15_000`) + react-hook-form + zod v4. - Modal "Editar" **sin campo Porcentaje/Alicuota** (UI-level enforcement de inmutabilidad). - Modal "Nueva vigencia" con preview + confirmación explícita + color distinto. - Tooltip de historial con **single request** al backend (no N+1). - Banner global de advertencia antes de mutaciones. - Tokens `--warning-bg` + `--warning-border` agregados al Design System (v2.5). ## Scope (out — explícito) - **Regla RNI 50%** → DIFERIDA a issue #20. Bloquea FAC-*, NO a ADM-009. Requiere confirmación contable/legal. - **Snapshot de `Porcentaje` en Presupuesto/Aviso** → trabajo de FAC-*/VTA-*. Este PR deja contrato establecido: FAC-* debe guardar `TipoDeIvaId` como FK estable + `Porcentaje` como snapshot defensivo. - **Código AFIP en `TipoDeIva`** → se agrega cuando INT-001 lo consuma. - **ADM-006 (Configuración Global)** → independiente; ADM-009 no lo requiere. ## Decisión arquitectónica clave — append-only `TipoDeIva.Porcentaje` e `IngresosBrutos.Alicuota` son **INMUTABLES** una vez creados. Cambiar el valor numérico NO es un `PATCH` — es un `POST /{iva|iibb}/{id}/nueva-version` que: 1. Crea nueva fila con `PredecesorId = {id}`. 2. Cierra predecesora con `VigenciaHasta = nuevaVigencia - 1 día`. 3. Registra evento de auditoría `tipo_iva.nueva_version` / `ingresos_brutos.nueva_version`. 4. Todo en un `TransactionScope` atómico — fail-closed si audit throws. **Razón**: una NC/ND en 2028 contra factura 2025 debe recalcular con la alícuota de 2025, no la vigente hoy. Append-only convierte `TipoDeIvaId` en token fiscal permanente que FAC-*/INT-001 pueden consumir ciegamente. ## Tests | Suite | Resultado | |-------|-----------| | SIGCM2.Application.Tests | **704 / 704** ✅ | | SIGCM2.Api.Tests | **220 / 220** ✅ | | Frontend (Vitest + RTL + MSW) | **275 / 275** ✅ | | **TOTAL** | **1199 / 1199** | TDD estricto: Red → Green → Refactor en cada capa. Incluye test de **reflection** que verifica que `WithPorcentaje`/`WithAlicuota` NO existen en las entidades (defensa en profundidad contra regresiones de mutabilidad). ## Smoke E2E realizado - Login + JWT ✅ - Create IVA_99 27% → 201 ✅ - Nueva versión 28.5% vigente 2026-05-01 → 201 ✅ - Historial → cadena `[5, 6]` con `PredecesorId` correcto ✅ - `PATCH` con `porcentaje` en body → **409 `inmutable_usar_nueva_version`** ✅ - AuditEvent: 2 entradas (`tipo_iva.create` + `tipo_iva.nueva_version`) con actor + metadata ✅ ## ⚠️ Acción requerida del reviewer **Antes de mergear** — aplicar V014 en tu entorno local: ```bash # Aplicar migración: sqlcmd -S TECNICA3 -d SIGCM2 -i database/migrations/V014__create_tablas_fiscales.sql # Para rollback si lo necesitás: sqlcmd -S TECNICA3 -d SIGCM2 -i database/migrations/V014_ROLLBACK.sql ``` La base de tests `SIGCM2_Test` se aplica automáticamente vía `SqlTestFixture.EnsureV014SchemaAsync()`. ## Follow-ups abiertos (NO cerrar con este PR) - **#20** — `[ADM-009] OQ-008: definir si recargo RNI 50% vive en Configuracion o constante de dominio`. Bloquea FAC-* hasta confirmación contable/legal. - **#21** — `[ADM-008] Migrar PuntoDeVentaForm a sintaxis Zod v4 (bug preexistente)`. Bug latente en `main` desde ADM-008. Este PR solo migra los 4 componentes fiscales de ADM-009. ## Artefactos SDD (engram — project: `sig-cm2`) | Fase | Topic key | |------|-----------| | Explore | `sdd/adm-009-tablas-fiscales/explore` | | Proposal v2 | `sdd/adm-009-tablas-fiscales/proposal` | | Spec | `sdd/adm-009-tablas-fiscales/spec` | | Design | `sdd/adm-009-tablas-fiscales/design` | | Tasks | `sdd/adm-009-tablas-fiscales/tasks` | | Apply progress | `sdd/adm-009-tablas-fiscales/apply-progress` | | Decisions | `sdd/adm-009-tablas-fiscales/decisions` | | Tech debt | `sdd/adm-009-tablas-fiscales/tech-debt` | ## Métricas - **91 tasks SDD** ejecutadas en 7 batches con Strict TDD. - **36 commits atómicos**. - **2 tech debts** eliminados en cleanup pre-merge (V014 seed PascalCase + V014MigrationTests filtros específicos). - **1 bug preexistente detectado** y documentado como follow-up (#21).
dmolinari added 36 commits 2026-04-18 11:40:14 +00:00
Tokens usados en banner de advertencia fiscal (ADM-009). Incluye variante
light (amber claro) y dark (amber oscuro), mapeados en @theme inline de Tailwind.
TipoDeIva types (UpdateRequest sin Porcentaje), ivaApi.ts con 8 endpoints,
ApiError contract { error, message } alineado con backend ADM-009.
useTiposDeIvaList, useTipoDeIva, useHistorialTipoDeIva (lazy enable),
mutations con invalidateQueries. staleTime: 15_000 en todas las queries.
Query keys estables: ['fiscal', 'iva', ...].
Columnas: Codigo, Descripcion, Porcentaje%, Vigencia (abierta si null),
Estado (badge), Version con HistorialCadenaTooltip lazy. Acciones: editar,
nueva vigencia, deactivate/reactivate toggle. 10 tests RTL pasan.
Modal de edicion solo cosmeticos (Codigo, Descripcion, AplicaIVA, Activo).
Campo Porcentaje ausente en modo edit — verificado con queryByLabelText null [REQ-UI-003].
Modo create incluye Porcentaje inicial + VigenciaDesde. 10 tests RTL pasan.
Preview en tiempo real: nuevo porcentaje, fecha cierre = vigenciaDesde-1d.
Banner warning con tokens DS. Boton disabled si form invalido [REQ-UI-004].
7 tests RTL pasan incluyendo verificacion de fecha cierre correcta.
Banner advertencia visible al mount con tokens warning-bg/warning-border [REQ-UI-005].
Filtros por codigo y activo. Paginacion server-side. Modales create/edit/nueva-version
controlados por estado local. 12 tests RTL pasan.
Types (ProvinciaArgentina 24 valores + PROVINCIA_DISPLAY), iibbApi.ts,
useIngresosBrutos hooks, tabla con columna Provincia, FormModal sin Alicuota
en edit [REQ-UI-007], NuevaVigenciaIibbModal con preview, TiposDeIibbPage con
banner. 8 tests RTL pasan (iibb). Total fiscal: 47/47 tests.
Ambas rutas protegidas con requiredPermissions=['administracion:fiscal:gestionar'].
Integradas en ProtectedPage con MustChangePasswordGate y ProtectedLayout.
dmolinari merged commit a804ef3c7b into main 2026-04-18 11:45:13 +00:00
dmolinari deleted branch feature/ADM-009 2026-04-18 11:45:13 +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#22