feat(bd): V018 crea dbo.Product + SqlTestFixture consolida V018 + permisos catalogo (PRD-002 W6)

This commit is contained in:
2026-04-19 13:46:11 -03:00
parent b4f17d6961
commit d7fb3105fa
3 changed files with 351 additions and 1 deletions

View File

@@ -63,6 +63,9 @@ public sealed class SqlTestFixture : IAsyncLifetime
// V017 (PRD-001): ensure dbo.ProductType + temporal + permiso 'catalogo:tipos:gestionar'.
await EnsureV017SchemaAsync();
// V018 (PRD-002): ensure dbo.Product + temporal + permiso 'catalogo:productos:gestionar'.
await EnsureV018SchemaAsync();
_respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
{
DbAdapter = DbAdapter.SqlServer,
@@ -91,6 +94,8 @@ public sealed class SqlTestFixture : IAsyncLifetime
new Respawn.Graph.Table("dbo", "Rubro_History"),
// PRD-001 (V017): ProductType es temporal — history no puede deletearse directo.
new Respawn.Graph.Table("dbo", "ProductType_History"),
// PRD-002 (V018): Product es temporal — history no puede deletearse directo.
new Respawn.Graph.Table("dbo", "Product_History"),
]
});
@@ -213,7 +218,11 @@ public sealed class SqlTestFixture : IAsyncLifetime
-- V014 (ADM-009): permiso para tablas fiscales
('administracion:fiscal:gestionar', N'Gestionar tablas fiscales', N'Gestionar tablas fiscales (IVA, IIBB)', 'administracion'),
-- V016 (CAT-001): permiso para gestionar árbol de rubros
('catalogo:rubros:gestionar', N'Gestionar rubros del catálogo', N'Crear, editar, mover y desactivar rubros del árbol de catálogo comercial', 'catalogo')
('catalogo:rubros:gestionar', N'Gestionar rubros del catálogo', N'Crear, editar, mover y desactivar rubros del árbol de catálogo comercial', 'catalogo'),
-- V017 (PRD-001): permiso para gestionar tipos de producto
('catalogo:tipos:gestionar', N'Gestionar tipos de producto', N'Crear, editar y desactivar ProductTypes del catálogo (flags + límites multimedia)', 'catalogo'),
-- V018 (PRD-002): permiso para gestionar productos del catálogo
('catalogo:productos:gestionar', N'Gestionar productos del catálogo', N'Crear, editar y desactivar productos del catálogo comercial', 'catalogo')
) AS s (Codigo, Nombre, Descripcion, Modulo)
ON t.Codigo = s.Codigo
WHEN NOT MATCHED BY TARGET THEN
@@ -261,6 +270,10 @@ public sealed class SqlTestFixture : IAsyncLifetime
('admin', 'administracion:fiscal:gestionar'),
-- V016 (CAT-001)
('admin', 'catalogo:rubros:gestionar'),
-- V017 (PRD-001)
('admin', 'catalogo:tipos:gestionar'),
-- V018 (PRD-002)
('admin', 'catalogo:productos:gestionar'),
('cajero', 'ventas:contado:crear'),
('cajero', 'ventas:contado:modificar'),
('cajero', 'ventas:contado:cobrar'),
@@ -1011,4 +1024,102 @@ public sealed class SqlTestFixture : IAsyncLifetime
await _connection.ExecuteAsync(createUqIndex);
await _connection.ExecuteAsync(createCoveringIndex);
}
/// <summary>
/// PRD-002 (V018): applies dbo.Product schema + temporal + filtered UQ + covering indexes
/// idempotentemente. Mirrors V018__create_product.sql.
/// Permiso 'catalogo:productos:gestionar' y asignación a admin se siembran
/// desde SeedPermisosCanonicalAsync / SeedRolPermisosCanonicalAsync (post-respawn).
/// </summary>
public async Task EnsureV018SchemaAsync()
{
const string createProduct = """
IF OBJECT_ID(N'dbo.Product', N'U') IS NULL
BEGIN
CREATE TABLE dbo.Product (
Id INT IDENTITY(1,1) NOT NULL CONSTRAINT PK_Product PRIMARY KEY,
Nombre NVARCHAR(300) COLLATE SQL_Latin1_General_CP1_CI_AI NOT NULL,
MedioId INT NOT NULL,
ProductTypeId INT NOT NULL,
RubroId INT NULL,
BasePrice DECIMAL(18,4) NOT NULL,
PriceDurationDays INT NULL,
IsActive BIT NOT NULL CONSTRAINT DF_Product_IsActive DEFAULT(1),
FechaCreacion DATETIME2(3) NOT NULL CONSTRAINT DF_Product_FechaCreacion DEFAULT(SYSUTCDATETIME()),
FechaModificacion DATETIME2(3) NULL,
CONSTRAINT FK_Product_Medio FOREIGN KEY (MedioId) REFERENCES dbo.Medio(Id) ON DELETE NO ACTION,
CONSTRAINT FK_Product_ProductType FOREIGN KEY (ProductTypeId) REFERENCES dbo.ProductType(Id) ON DELETE NO ACTION,
CONSTRAINT FK_Product_Rubro FOREIGN KEY (RubroId) REFERENCES dbo.Rubro(Id) ON DELETE NO ACTION,
CONSTRAINT CK_Product_BasePrice_NonNegative CHECK (BasePrice >= 0),
CONSTRAINT CK_Product_PriceDurationDays_Positive CHECK (PriceDurationDays IS NULL OR PriceDurationDays >= 1)
);
END
""";
const string addProductPeriod = """
IF COL_LENGTH('dbo.Product', 'ValidFrom') IS NULL
BEGIN
ALTER TABLE dbo.Product
ADD
ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
CONSTRAINT DF_Product_ValidFrom DEFAULT(SYSUTCDATETIME()),
ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL
CONSTRAINT DF_Product_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
END
""";
const string setProductVersioning = """
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Product') AND temporal_type = 2)
BEGIN
ALTER TABLE dbo.Product
SET (SYSTEM_VERSIONING = ON (
HISTORY_TABLE = dbo.Product_History,
HISTORY_RETENTION_PERIOD = 10 YEARS
));
END
""";
const string createUqIndex = """
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UQ_Product_MedioId_ProductTypeId_Nombre_Active' AND object_id = OBJECT_ID('dbo.Product'))
BEGIN
CREATE UNIQUE INDEX UQ_Product_MedioId_ProductTypeId_Nombre_Active
ON dbo.Product (MedioId, ProductTypeId, Nombre)
WHERE IsActive = 1;
END
""";
const string createProductTypeIdx = """
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Product_ProductTypeId_IsActive' AND object_id = OBJECT_ID('dbo.Product'))
BEGIN
CREATE INDEX IX_Product_ProductTypeId_IsActive
ON dbo.Product (ProductTypeId, IsActive);
END
""";
const string createMedioIdx = """
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Product_MedioId_IsActive' AND object_id = OBJECT_ID('dbo.Product'))
BEGIN
CREATE INDEX IX_Product_MedioId_IsActive
ON dbo.Product (MedioId, IsActive);
END
""";
const string createRubroIdx = """
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Product_RubroId_IsActive' AND object_id = OBJECT_ID('dbo.Product'))
BEGIN
CREATE INDEX IX_Product_RubroId_IsActive
ON dbo.Product (RubroId, IsActive)
WHERE RubroId IS NOT NULL;
END
""";
await _connection.ExecuteAsync(createProduct);
await _connection.ExecuteAsync(addProductPeriod);
await _connection.ExecuteAsync(setProductVersioning);
await _connection.ExecuteAsync(createUqIndex);
await _connection.ExecuteAsync(createProductTypeIdx);
await _connection.ExecuteAsync(createMedioIdx);
await _connection.ExecuteAsync(createRubroIdx);
}
}