feat(bd): V016 create Rubro table con SYSTEM_VERSIONING (CAT-001)
- dbo.Rubro: adjacency list, self-FK, soft-delete, temporal retention 10y - Filtered unique index UQ_Rubro_ParentId_Nombre_Activo + covering IX_Rubro_ParentId_Activo - Permission catalogo:rubros:gestionar seeded + assigned to admin role - V016_ROLLBACK.sql: full reversal script - RubrosOptions class (MaxDepth=10) + appsettings.json Rubros section - services.Configure<RubrosOptions> registered in Infrastructure DI - database/README.md updated with V013-V016 entries
This commit is contained in:
@@ -29,6 +29,10 @@ database/
|
||||
| **V010** | **`V010__audit_infrastructure.sql`** | **UDT-010** | **Infra de auditoría + Temporal Tables. Ver nota abajo.** |
|
||||
| V011 | `V011__create_medio_seccion.sql` | ADM-001 | Medio + Seccion (temporal, retention 10y) + permiso `administracion:secciones:gestionar` |
|
||||
| V012 | `V012__seed_medios.sql` | ADM-001 | Seed idempotente de Medios ELDIA y ELPLATA |
|
||||
| V013 | `V013__create_puntos_de_venta.sql` | ADM-008 | PuntosDeVenta (temporal, retention 10y) + permiso `administracion:puntos_de_venta:gestionar` |
|
||||
| V014 | `V014__create_tablas_fiscales.sql` | ADM-009 | TiposDeIva + IngresosBrutos (versioning por cadena) + permisos fiscales |
|
||||
| V015 | `V015__create_local_timezone_views.sql` | UDT-011 | Vistas admin con OccurredAt convertido a hora Argentina |
|
||||
| **V016** | **`V016__create_rubro.sql`** | **CAT-001** | **Rubro (adjacency list, temporal 10y) + permiso `catalogo:rubros:gestionar`** |
|
||||
|
||||
## Convenciones
|
||||
|
||||
|
||||
82
database/migrations/V016_ROLLBACK.sql
Normal file
82
database/migrations/V016_ROLLBACK.sql
Normal file
@@ -0,0 +1,82 @@
|
||||
-- V016_ROLLBACK.sql
|
||||
-- Reversa de V016__create_rubro.sql.
|
||||
--
|
||||
-- ⚠️ ADVERTENCIA: ejecutar ELIMINA dbo.Rubro, dbo.Rubro_History,
|
||||
-- el permiso 'catalogo:rubros:gestionar' y sus asignaciones.
|
||||
--
|
||||
-- Uso intended: ROLLBACK en entornos NO-productivos.
|
||||
-- Prerequisito: no deben existir FKs vivas apuntando a Rubro (p.ej., Producto, Tarifario).
|
||||
-- Si CAT-002..006 o PRC-001 ya están aplicados, agregar TarifarioBaseId FK,
|
||||
-- este rollback fallará — usar backup.
|
||||
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET NOCOUNT ON;
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 1. Apagar SYSTEM_VERSIONING + remover PERIOD en Rubro
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Rubro') AND temporal_type = 2)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Rubro SET (SYSTEM_VERSIONING = OFF);
|
||||
PRINT 'Rubro: SYSTEM_VERSIONING OFF.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.periods WHERE object_id = OBJECT_ID('dbo.Rubro'))
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Rubro DROP PERIOD FOR SYSTEM_TIME;
|
||||
PRINT 'Rubro: PERIOD FOR SYSTEM_TIME dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF COL_LENGTH('dbo.Rubro', 'ValidFrom') IS NOT NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Rubro DROP CONSTRAINT IF EXISTS DF_Rubro_ValidFrom;
|
||||
ALTER TABLE dbo.Rubro DROP CONSTRAINT IF EXISTS DF_Rubro_ValidTo;
|
||||
ALTER TABLE dbo.Rubro DROP COLUMN ValidFrom, ValidTo;
|
||||
PRINT 'Rubro: ValidFrom/ValidTo dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.Rubro_History', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE dbo.Rubro_History;
|
||||
PRINT 'Rubro_History dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 2. Drop índices + tabla Rubro
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Self-FK must be dropped before dropping the table (SQL Server handles it
|
||||
-- automatically when the table is dropped, but explicit is safer).
|
||||
|
||||
IF OBJECT_ID(N'dbo.Rubro', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE dbo.Rubro;
|
||||
PRINT 'Table dbo.Rubro dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 3. Remover permiso 'catalogo:rubros:gestionar' + RolPermiso
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
DELETE rp
|
||||
FROM dbo.RolPermiso rp
|
||||
JOIN dbo.Permiso p ON p.Id = rp.PermisoId
|
||||
WHERE p.Codigo = 'catalogo:rubros:gestionar';
|
||||
GO
|
||||
|
||||
DELETE FROM dbo.Permiso
|
||||
WHERE Codigo = 'catalogo:rubros:gestionar';
|
||||
GO
|
||||
|
||||
PRINT '';
|
||||
PRINT 'V016 rolled back. dbo.Rubro and dbo.Rubro_History removed.';
|
||||
PRINT 'catalogo:rubros:gestionar permission and role assignment removed.';
|
||||
GO
|
||||
152
database/migrations/V016__create_rubro.sql
Normal file
152
database/migrations/V016__create_rubro.sql
Normal file
@@ -0,0 +1,152 @@
|
||||
-- V016__create_rubro.sql
|
||||
-- CAT-001: Árbol N-ario de Rubros — tabla fundacional del catálogo comercial.
|
||||
--
|
||||
-- Cambios:
|
||||
-- 1. dbo.Rubro (adjacency list, self-FK, soft-delete, SYSTEM_VERSIONING ON, retention 10 años).
|
||||
-- 2. Índice filtrado unique UQ_Rubro_ParentId_Nombre_Activo (unicidad CI por padre en activos).
|
||||
-- 3. Índice cubriente IX_Rubro_ParentId_Activo (child lookups ordenados).
|
||||
-- 4. Permiso 'catalogo:rubros:gestionar' + asignación a rol 'admin'.
|
||||
--
|
||||
-- Patrón: V011 (dbo.Medio con SYSTEM_VERSIONING + PAGE compression + MERGE permisos).
|
||||
-- Idempotente: seguro para re-ejecutar.
|
||||
-- Reversa: V016_ROLLBACK.sql.
|
||||
-- Run on: SIGCM2 (dev) y SIGCM2_Test (integration tests).
|
||||
--
|
||||
-- Notas:
|
||||
-- - TarifarioBaseId es INT NULL SIN FK — la FK se agrega en PRC-001.
|
||||
-- - UQ_Rubro_ParentId_Nombre_Activo cubre solo ParentId IS NOT NULL;
|
||||
-- para roots (ParentId IS NULL) la unicidad CI la garantiza Application
|
||||
-- via ExistsByNombreUnderParentAsync(null, ...) — SQL Server trata NULLs
|
||||
-- como distintos en índices únicos. Ver Design §9 Risk 1.
|
||||
-- - FechaCreacion / FechaModificacion: DATETIME2(3) alineado con Medio/Seccion.
|
||||
-- - ValidFrom / ValidTo: DATETIME2(3) GENERATED ALWAYS HIDDEN (idéntico a V011).
|
||||
--
|
||||
-- Source of truth: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.5 📋 Auditoría.md
|
||||
-- SDD Design: engram sdd/cat-001-arbol-nario-rubros/design
|
||||
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET NOCOUNT ON;
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 1. dbo.Rubro
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
IF OBJECT_ID(N'dbo.Rubro', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.Rubro (
|
||||
Id INT IDENTITY(1,1) NOT NULL CONSTRAINT PK_Rubro PRIMARY KEY,
|
||||
ParentId INT NULL,
|
||||
Nombre NVARCHAR(200) NOT NULL COLLATE SQL_Latin1_General_CP1_CI_AI,
|
||||
Orden INT NOT NULL CONSTRAINT DF_Rubro_Orden DEFAULT(0),
|
||||
Activo BIT NOT NULL CONSTRAINT DF_Rubro_Activo DEFAULT(1),
|
||||
TarifarioBaseId INT NULL, -- FK reservada para PRC-001 (sin constraint por ahora)
|
||||
FechaCreacion DATETIME2(3) NOT NULL CONSTRAINT DF_Rubro_FechaCreacion DEFAULT(SYSUTCDATETIME()),
|
||||
FechaModificacion DATETIME2(3) NULL,
|
||||
CONSTRAINT FK_Rubro_Parent FOREIGN KEY (ParentId) REFERENCES dbo.Rubro(Id) ON DELETE NO ACTION
|
||||
);
|
||||
PRINT 'Table dbo.Rubro created.';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Table dbo.Rubro already exists — skip.';
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 2. SYSTEM_VERSIONING — Rubro
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
IF COL_LENGTH('dbo.Rubro', 'ValidFrom') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Rubro
|
||||
ADD
|
||||
ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
|
||||
CONSTRAINT DF_Rubro_ValidFrom DEFAULT(SYSUTCDATETIME()),
|
||||
ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL
|
||||
CONSTRAINT DF_Rubro_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
|
||||
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
|
||||
PRINT 'Rubro: PERIOD FOR SYSTEM_TIME added.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Rubro') AND temporal_type = 2)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Rubro
|
||||
SET (SYSTEM_VERSIONING = ON (
|
||||
HISTORY_TABLE = dbo.Rubro_History,
|
||||
HISTORY_RETENTION_PERIOD = 10 YEARS
|
||||
));
|
||||
PRINT 'Rubro: SYSTEM_VERSIONING = ON (history: dbo.Rubro_History, retention: 10 years).';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Rubro: SYSTEM_VERSIONING already ON — skip.';
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'Rubro_History' AND schema_id = SCHEMA_ID('dbo'))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys.partitions p
|
||||
JOIN sys.tables t ON t.object_id = p.object_id
|
||||
WHERE t.name = 'Rubro_History' AND p.data_compression = 2
|
||||
)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Rubro_History REBUILD WITH (DATA_COMPRESSION = PAGE);
|
||||
PRINT 'Rubro_History: rebuilt with PAGE compression.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 3. Índices
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Unicidad CI por nombre bajo el mismo padre (solo filas activas + ParentId NOT NULL).
|
||||
-- Para roots (ParentId IS NULL) la unicidad la garantiza Application layer.
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UQ_Rubro_ParentId_Nombre_Activo' AND object_id = OBJECT_ID('dbo.Rubro'))
|
||||
BEGIN
|
||||
CREATE UNIQUE INDEX UQ_Rubro_ParentId_Nombre_Activo
|
||||
ON dbo.Rubro(ParentId, Nombre)
|
||||
WHERE Activo = 1 AND ParentId IS NOT NULL;
|
||||
PRINT 'Index UQ_Rubro_ParentId_Nombre_Activo created.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- Cubriente para child lookups ordenados por Orden.
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Rubro_ParentId_Activo' AND object_id = OBJECT_ID('dbo.Rubro'))
|
||||
BEGIN
|
||||
CREATE INDEX IX_Rubro_ParentId_Activo
|
||||
ON dbo.Rubro(ParentId, Activo)
|
||||
INCLUDE (Nombre, Orden);
|
||||
PRINT 'Index IX_Rubro_ParentId_Activo created.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 4. Permiso: catalogo:rubros:gestionar + asignación a rol 'admin'
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
MERGE dbo.Permiso AS t
|
||||
USING (VALUES
|
||||
('catalogo:rubros:gestionar', N'Gestionar rubros del catálogo', N'Crear, editar, mover y desactivar rubros del árbol de catálogo comercial', 'catalogo')
|
||||
) AS s (Codigo, Nombre, Descripcion, Modulo)
|
||||
ON t.Codigo = s.Codigo
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (Codigo, Nombre, Descripcion, Modulo)
|
||||
VALUES (s.Codigo, s.Nombre, s.Descripcion, s.Modulo);
|
||||
GO
|
||||
|
||||
MERGE dbo.RolPermiso AS t
|
||||
USING (
|
||||
SELECT r.Id AS RolId, p.Id AS PermisoId
|
||||
FROM (VALUES
|
||||
('admin', 'catalogo:rubros:gestionar')
|
||||
) AS x (RolCodigo, PermisoCodigo)
|
||||
JOIN dbo.Rol r ON r.Codigo = x.RolCodigo
|
||||
JOIN dbo.Permiso p ON p.Codigo = x.PermisoCodigo
|
||||
) AS s ON t.RolId = s.RolId AND t.PermisoId = s.PermisoId
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (RolId, PermisoId) VALUES (s.RolId, s.PermisoId);
|
||||
GO
|
||||
|
||||
PRINT '';
|
||||
PRINT 'V016 applied successfully — dbo.Rubro (temporal, retention 10y) + permiso catalogo:rubros:gestionar.';
|
||||
PRINT 'Next: V017 (future — TBD by next UDT).';
|
||||
GO
|
||||
@@ -32,5 +32,8 @@
|
||||
],
|
||||
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ]
|
||||
},
|
||||
"Rubros": {
|
||||
"MaxDepth": 10
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
13
src/api/SIGCM2.Application/Rubros/RubrosOptions.cs
Normal file
13
src/api/SIGCM2.Application/Rubros/RubrosOptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace SIGCM2.Application.Rubros;
|
||||
|
||||
/// Bound from appsettings section "Rubros".
|
||||
/// Controls the maximum allowed depth of the N-ary rubro tree.
|
||||
/// Resolvable via IOptions<RubrosOptions> in any handler that enforces the depth rule.
|
||||
public sealed class RubrosOptions
|
||||
{
|
||||
public const string SectionName = "Rubros";
|
||||
|
||||
/// Maximum tree depth (0 = root level). Default: 10.
|
||||
/// Depth-10 means a root + 9 levels of children.
|
||||
public int MaxDepth { get; set; } = 10;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Abstractions.Security;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.Auth;
|
||||
using SIGCM2.Application.Rubros;
|
||||
using SIGCM2.Infrastructure.Http;
|
||||
using SIGCM2.Infrastructure.Messaging;
|
||||
using SIGCM2.Infrastructure.Persistence;
|
||||
@@ -77,6 +78,9 @@ public static class DependencyInjection
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddScoped<IClientContext, ClientContext>();
|
||||
|
||||
// CAT-001: Rubros options (MaxDepth) — overridable via appsettings "Rubros".
|
||||
services.Configure<RubrosOptions>(configuration.GetSection(RubrosOptions.SectionName));
|
||||
|
||||
// UDT-010: Audit options (SanitizedKeys blacklist) — overridable via appsettings "Audit".
|
||||
services.Configure<AuditOptions>(configuration.GetSection(AuditOptions.SectionName));
|
||||
services.AddScoped<IAuditContext, SIGCM2.Infrastructure.Audit.AuditContext>();
|
||||
|
||||
Reference in New Issue
Block a user