-- V018__create_product.sql -- PRD-002: Product — entidad vendible concreta del catálogo comercial. -- -- Cambios: -- 1. dbo.Product (FK Medio/ProductType/Rubro, SYSTEM_VERSIONING ON, retention 10 años). -- 2. Índices: filtered UQ por (MedioId, ProductTypeId, Nombre) activos; cover por ProductTypeId -- (para IProductQueryRepository); cover por MedioId; cover filtrado por RubroId. -- 3. Permiso 'catalogo:productos:gestionar' + asignación a rol 'admin'. -- -- Patrón: V017 (dbo.ProductType con SYSTEM_VERSIONING + PAGE compression + MERGE permisos). -- Idempotente: seguro para re-ejecutar. -- Reversa: V018_ROLLBACK.sql. -- Run on: SIGCM2 (dev) y SIGCM2_Test (integration tests). -- -- Notas: -- - SIN seed de datos — PRD-008 (V019) seedea los 12 productos legacy. -- - Validación de flags (RequiresCategory, HasDuration) vive en Application layer: -- un ProductType puede cambiar flags; la Product queda en estado snapshot. -- - UQ filtered WHERE IsActive=1: permite reusar nombres tras soft-delete. -- -- SDD Design: engram sdd/prd-002-product-crud/design SET QUOTED_IDENTIFIER ON; SET ANSI_NULLS ON; SET NOCOUNT ON; GO -- ═══════════════════════════════════════════════════════════════════════ -- 1. dbo.Product -- ═══════════════════════════════════════════════════════════════════════ 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) ); PRINT 'Table dbo.Product created.'; END ELSE PRINT 'Table dbo.Product already exists — skip.'; GO -- ═══════════════════════════════════════════════════════════════════════ -- 2. SYSTEM_VERSIONING — Product -- ═══════════════════════════════════════════════════════════════════════ 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); PRINT 'Product: PERIOD FOR SYSTEM_TIME added.'; END GO 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 )); PRINT 'Product: SYSTEM_VERSIONING = ON (history: dbo.Product_History, retention: 10 years).'; END ELSE PRINT 'Product: SYSTEM_VERSIONING already ON — skip.'; GO IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'Product_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 = 'Product_History' AND p.data_compression = 2 ) BEGIN ALTER TABLE dbo.Product_History REBUILD WITH (DATA_COMPRESSION = PAGE); PRINT 'Product_History: rebuilt with PAGE compression.'; END GO -- ═══════════════════════════════════════════════════════════════════════ -- 3. Índices -- ═══════════════════════════════════════════════════════════════════════ -- Filtered UQ: unicidad activa por (Medio, Tipo, Nombre). Permite reusar nombres tras soft-delete. 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; PRINT 'Index UQ_Product_MedioId_ProductTypeId_Nombre_Active created.'; END GO -- Cover para IProductQueryRepository.ExistsActiveByProductTypeAsync 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); PRINT 'Index IX_Product_ProductTypeId_IsActive created.'; END GO -- Cover para list filtered by MedioId 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); PRINT 'Index IX_Product_MedioId_IsActive created.'; END GO -- Cover para list filtered by RubroId 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; PRINT 'Index IX_Product_RubroId_IsActive created.'; END GO -- ═══════════════════════════════════════════════════════════════════════ -- 4. Permiso: catalogo:productos:gestionar + asignación a rol 'admin' -- ═══════════════════════════════════════════════════════════════════════ MERGE dbo.Permiso AS t USING (VALUES ('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 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:productos: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 'V018 applied — dbo.Product (temporal, retention 10y) + permiso catalogo:productos:gestionar.'; PRINT 'Next: V019 (PRD-008 — seed 12 productos legacy).'; GO