ADM-008: Puntos de Venta (CRUD fundacional) #19

Merged
dmolinari merged 18 commits from feature/ADM-008 into main 2026-04-17 17:31:21 +00:00
Owner

Resumen

Implementa ADM-008 del roadmap (Fase 1 — Maestros fundacionales del CRITICAL PATH).

Scope recortado post-smoke: solo CRUD de PuntoDeVenta con Temporal Tables + auditoría + cascada de inactividad Medio→PdV + feature frontend completa.

Durante el smoke el usuario aclaró un punto arquitectónico crítico: SIG-CM2.0 no genera números de factura AFIP — los asigna IMAC (subsistema de Plataforma Infogestión) externamente. Un worker futuro (INT-001) poleará una vista de Infogestión para asociar NumeroOrdenInterno ↔ NumeroFacturaAFIP + CAI. En consecuencia, se eliminó toda la infraestructura de "reserva de número" que inicialmente había implementado (tabla SecuenciaComprobante, SP usp_ReservarNumeroComprobante, enum TipoComprobante, handlers Reservar/Próximo, endpoints, panel UI, tests de concurrencia).

Resuelve OQ-ADM-008 (cardinalidad PdV×Medio → 1:N) y OQ-ADM-008-C (numeración AFIP fuera de scope, diferida a INT-001).

Archivos clave

Backend

  • database/migrations/V013__create_puntos_de_venta.sql — DDL PuntoDeVenta + Temporal Tables (retention 10 años, PAGE compression) + permiso + drops idempotentes de artefactos previos (SecuenciaComprobante + SP)
  • src/api/SIGCM2.Domain/Entities/PuntoDeVenta.cs — sealed record + factory ForCreation + With* + invariantes
  • src/api/SIGCM2.Domain/Exceptions/{PuntoDeVentaNotFoundException, NumeroAFIPDuplicadoException}.cs
  • src/api/SIGCM2.Application/PuntosDeVenta/ — 6 handlers CRUD (Create/Update/Deactivate/Reactivate/GetById/List) con IAuditLogger + TransactionScope (fail-closed)
  • src/api/SIGCM2.Infrastructure/Persistence/PuntoDeVentaRepository.cs — Dapper + mapping SqlException 2627 → NumeroAFIPDuplicadoException
  • src/api/SIGCM2.Api/Controllers/PuntosDeVentaController.cs — 6 endpoints CRUD
  • src/api/SIGCM2.Api/Filters/ExceptionFilter.cs — mapping 404 / 409

Frontend

  • src/web/src/features/puntos-de-venta/ — types, api, hooks, table, form, detail, banners, 4 pages
  • Routing /admin/puntos-de-venta/* + sidebar admin (guard administracion:puntos_de_venta:gestionar)

Tests

  • 13 Domain + 42 Application + ~16 Api integration (CRUD puro) + 20 frontend Vitest
  • dotnet test tests/SIGCM2.Api.Tests → 190/190 verde
  • vitest run → 20/20 verde

Docs (Obsidian, fuera del repo por convención)

  • 2.5 Auditoría.mdPuntoDeVenta agregado al catálogo
  • 2.10 UDTs Módulo Administración.md — scope ADM-008 recortado + OQ-ADM-008-C resuelta
  • STATUS.md[x] ADM-008 con resumen ajustado

Decisiones relevantes

  • OQ-ADM-008 (1:N): un Medio tiene N PdVs; cada PdV pertenece a exactamente 1 Medio. Alineado con Seccion.MedioId NOT NULL.
  • OQ-ADM-008-C (numeración AFIP fuera de scope): IMAC asigna los números. Diferido a INT-001 worker de polling — vista SQL concreta guardada en memoria engram architecture/imac-polling-view.
  • Permiso con guion bajo: administracion:puntos_de_venta:gestionar — CHECK constraint CK_Permiso_Codigo_Format solo acepta [a-z0-9_:].
  • Cascada Medio → PdV: mutaciones de PdV devuelven 409 medio_inactivo si el Medio padre está inactivo. PuntoDeVenta.NumeroAFIP se mantiene como configuración fija (va en el payload a IMAC).
  • Aprendizaje guardado en engram (conventions/db-smoke-readiness): cada UDT con migración debe aplicarla en SIGCM2 Y SIGCM2_Test antes de declarar "ready for smoke".

Followups generados

  • INT-001 (IMAC Worker): implementar SIGCM2.Worker.ImacFacturaSync con Quartz.NET que polea la vista de Plataforma Infogestión (SQL exacto en memoria engram architecture/imac-polling-view) para asociar número de orden interno ↔ número de factura AFIP + CAI.
  • FAC-001: cuando se diseñe facturación, considerar que la numeración viene de afuera (IMAC). El lado SIG-CM2 solo emite el payload con NumeroOrdenInterno + NumeroAFIP del PdV.

Test plan

  • dotnet build → 0 errores, 0 warnings
  • dotnet test tests/SIGCM2.Api.Tests → 190/190
  • dotnet test tests/SIGCM2.Application.Tests --filter "FullyQualifiedName~PuntoDeVenta" → 55/55
  • cd src/web && vitest run src/tests/features/puntos-de-venta → 20/20
  • V013 aplicada en SIGCM2 dev + SIGCM2_Test
  • Smoke UI: login admin → /admin/puntos-de-venta → crear PdV para ELDIA (NumeroAFIP=1, nombre "Central") → editar → deactivate → reactivate → confirmar auditoría en AuditEvent

Commits relevantes

  • bef8977 — migration V013 inicial (con reserva, después recortado)
  • 43877bd, 50f6f2b, 489359f — Domain + Application + Infra
  • 39160bb, 4877954 — Api + integration tests
  • d61292a, 4b96cde, 0560452, 4720f67 — Frontend feature completa
  • 65787db — verify-loop fixes (UQ constraint name, DI registro, extended backoff)
  • 4368c42 — docs inicial
  • 40482ca, 7d432a9, 6be637b, 6458ee0cirugía post-smoke (eliminar reserva de número)
  • fc77576 — limpieza final (import huérfano + comentario stale)
## Resumen Implementa **ADM-008** del roadmap (Fase 1 — Maestros fundacionales del CRITICAL PATH). **Scope recortado post-smoke**: solo CRUD de `PuntoDeVenta` con Temporal Tables + auditoría + cascada de inactividad Medio→PdV + feature frontend completa. Durante el smoke el usuario aclaró un punto arquitectónico crítico: **SIG-CM2.0 no genera números de factura AFIP** — los asigna IMAC (subsistema de Plataforma Infogestión) externamente. Un worker futuro (INT-001) poleará una vista de Infogestión para asociar `NumeroOrdenInterno ↔ NumeroFacturaAFIP + CAI`. En consecuencia, se eliminó toda la infraestructura de "reserva de número" que inicialmente había implementado (tabla `SecuenciaComprobante`, SP `usp_ReservarNumeroComprobante`, enum `TipoComprobante`, handlers Reservar/Próximo, endpoints, panel UI, tests de concurrencia). Resuelve **OQ-ADM-008** (cardinalidad PdV×Medio → **1:N**) y **OQ-ADM-008-C** (numeración AFIP fuera de scope, diferida a INT-001). ## Archivos clave ### Backend - `database/migrations/V013__create_puntos_de_venta.sql` — DDL `PuntoDeVenta` + Temporal Tables (retention 10 años, PAGE compression) + permiso + drops idempotentes de artefactos previos (SecuenciaComprobante + SP) - `src/api/SIGCM2.Domain/Entities/PuntoDeVenta.cs` — sealed record + factory `ForCreation` + `With*` + invariantes - `src/api/SIGCM2.Domain/Exceptions/{PuntoDeVentaNotFoundException, NumeroAFIPDuplicadoException}.cs` - `src/api/SIGCM2.Application/PuntosDeVenta/` — 6 handlers CRUD (Create/Update/Deactivate/Reactivate/GetById/List) con `IAuditLogger` + `TransactionScope` (fail-closed) - `src/api/SIGCM2.Infrastructure/Persistence/PuntoDeVentaRepository.cs` — Dapper + mapping SqlException 2627 → `NumeroAFIPDuplicadoException` - `src/api/SIGCM2.Api/Controllers/PuntosDeVentaController.cs` — 6 endpoints CRUD - `src/api/SIGCM2.Api/Filters/ExceptionFilter.cs` — mapping 404 / 409 ### Frontend - `src/web/src/features/puntos-de-venta/` — types, api, hooks, table, form, detail, banners, 4 pages - Routing `/admin/puntos-de-venta/*` + sidebar admin (guard `administracion:puntos_de_venta:gestionar`) ### Tests - 13 Domain + 42 Application + ~16 Api integration (CRUD puro) + 20 frontend Vitest - `dotnet test tests/SIGCM2.Api.Tests` → 190/190 verde - `vitest run` → 20/20 verde ### Docs (Obsidian, fuera del repo por convención) - `2.5 Auditoría.md` — `PuntoDeVenta` agregado al catálogo - `2.10 UDTs Módulo Administración.md` — scope ADM-008 recortado + OQ-ADM-008-C resuelta - `STATUS.md` — `[x]` ADM-008 con resumen ajustado ## Decisiones relevantes - **OQ-ADM-008 (1:N)**: un Medio tiene N PdVs; cada PdV pertenece a exactamente 1 Medio. Alineado con `Seccion.MedioId NOT NULL`. - **OQ-ADM-008-C (numeración AFIP fuera de scope)**: IMAC asigna los números. Diferido a INT-001 worker de polling — vista SQL concreta guardada en memoria engram `architecture/imac-polling-view`. - **Permiso con guion bajo**: `administracion:puntos_de_venta:gestionar` — CHECK constraint `CK_Permiso_Codigo_Format` solo acepta `[a-z0-9_:]`. - **Cascada Medio → PdV**: mutaciones de PdV devuelven 409 `medio_inactivo` si el Medio padre está inactivo. `PuntoDeVenta.NumeroAFIP` se mantiene como configuración fija (va en el payload a IMAC). - **Aprendizaje guardado en engram (conventions/db-smoke-readiness)**: cada UDT con migración debe aplicarla en `SIGCM2` Y `SIGCM2_Test` antes de declarar "ready for smoke". ## Followups generados - **INT-001 (IMAC Worker)**: implementar `SIGCM2.Worker.ImacFacturaSync` con Quartz.NET que polea la vista de Plataforma Infogestión (SQL exacto en memoria engram `architecture/imac-polling-view`) para asociar número de orden interno ↔ número de factura AFIP + CAI. - **FAC-001**: cuando se diseñe facturación, considerar que la numeración viene de afuera (IMAC). El lado SIG-CM2 solo emite el payload con `NumeroOrdenInterno` + `NumeroAFIP` del PdV. ## Test plan - [x] `dotnet build` → 0 errores, 0 warnings - [x] `dotnet test tests/SIGCM2.Api.Tests` → 190/190 - [x] `dotnet test tests/SIGCM2.Application.Tests --filter "FullyQualifiedName~PuntoDeVenta"` → 55/55 - [x] `cd src/web && vitest run src/tests/features/puntos-de-venta` → 20/20 - [x] V013 aplicada en `SIGCM2` dev + `SIGCM2_Test` - [x] Smoke UI: login admin → `/admin/puntos-de-venta` → crear PdV para ELDIA (`NumeroAFIP=1`, nombre "Central") → editar → deactivate → reactivate → confirmar auditoría en `AuditEvent` ## Commits relevantes - `bef8977` — migration V013 inicial (con reserva, después recortado) - `43877bd`, `50f6f2b`, `489359f` — Domain + Application + Infra - `39160bb`, `4877954` — Api + integration tests - `d61292a`, `4b96cde`, `0560452`, `4720f67` — Frontend feature completa - `65787db` — verify-loop fixes (UQ constraint name, DI registro, extended backoff) - `4368c42` — docs inicial - `40482ca`, `7d432a9`, `6be637b`, `6458ee0` — **cirugía post-smoke** (eliminar reserva de número) - `fc77576` — limpieza final (import huérfano + comentario stale)
dmolinari added 12 commits 2026-04-17 16:05:45 +00:00
- 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
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.
T5.3: 18 tests cubriendo 401/403, create, get, list, update, deactivate, reactivate, reservar, proximo.
T5.4: 50 tasks paralelas → 50 numeros distintos sin duplicados.
T5.5: 100 reservas en serie → {1..100} en orden.
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).
dmolinari added 1 commit 2026-04-17 16:38:24 +00:00
Gap detectado durante smoke: la DetailPage tenia los hooks
useReservarNumero/useProximoNumero creados en Batch 6 pero faltaba
el componente que los consume.

SecuenciasPanel.tsx: tabla con los 6 tipos AFIP (FacturaA/B/C, NC A/B/C),
proximo numero por tipo, boton Reservar. Toast con el numero reservado.
Deshabilitado si PdV o Medio padre estan inactivos.

Integrado en PuntoDeVentaDetailPage bajo guard de permiso.
dmolinari added 5 commits 2026-04-17 17:25:00 +00:00
SecuenciaComprobante, usp_ReservarNumeroComprobante y TipoComprobante no tienen
propósito de negocio: IMAC/Infogestión asigna NumeroFactura+CAI externamente.
V013 ahora solo gestiona PuntoDeVenta + temporal table + permiso AFIP.
Sección 0 aplica drops idempotentes para limpiar SIGCM2_Test y reinstalaciones.
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.
Eliminar secuencias.api.ts, useReservarNumero.ts, SecuenciasPanel.tsx,
TipoComprobante enum y tipos ReservarNumeroResponse/ProximoNumeroResponse.
Quitar SecuenciasPanel del PuntoDeVentaDetailPage.
Eliminar SecuenciaComprobanteTests, ReservarNumeroCommandHandlerTests,
GetProximoNumeroQueryHandlerTests y 7 tests de integración en
PuntosDeVentaControllerTests (reserva/proximo/concurrencia/secuencialidad).
SqlTestFixture ahora limpia SecuenciaComprobante+SP si existen (drops idempotentes)
y solo crea PuntoDeVenta + temporal table.
- PuntoDeVentaTests.cs: quitar using SIGCM2.Domain.Enums (quedo huerfano tras
  eliminar TipoComprobante).
- SqlTestFixture.cs: actualizar comentario de EnsureV013SchemaAsync para
  reflejar scope recortado (solo PdV + permiso, drops idempotentes de
  SecuenciaComprobante + SP).
dmolinari changed title from ADM-008: Puntos de Venta (FacturasNumeros) — CRUD + SP reserva atómica to ADM-008: Puntos de Venta (CRUD fundacional) 2026-04-17 17:25:52 +00:00
dmolinari merged commit a82d51ff7a into main 2026-04-17 17:31:21 +00:00
dmolinari deleted branch feature/ADM-008 2026-04-17 17:31:21 +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#19