feat(application): CRUD handlers + validators + DI de ProductType (PRD-001)

Create/Update/Deactivate handlers con TransactionScope + audit; validators FluentValidation;
DI wiring NullProductQueryRepository + 5 handlers; SqlTestFixture V017 + permiso count 25→26.
This commit is contained in:
2026-04-19 09:46:31 -03:00
parent 3c9e852379
commit 5c8f19bf39
19 changed files with 1103 additions and 6 deletions

View File

@@ -60,6 +60,9 @@ public sealed class SqlTestFixture : IAsyncLifetime
// V016 (CAT-001): ensure dbo.Rubro + temporal + permiso 'catalogo:rubros:gestionar'.
await EnsureV016SchemaAsync();
// V017 (PRD-001): ensure dbo.ProductType + temporal + permiso 'catalogo:tipos:gestionar'.
await EnsureV017SchemaAsync();
_respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
{
DbAdapter = DbAdapter.SqlServer,
@@ -86,6 +89,8 @@ public sealed class SqlTestFixture : IAsyncLifetime
new Respawn.Graph.Table("dbo", "IngresosBrutos"),
// CAT-001 (V016): Rubro es temporal — history no puede deletearse directo.
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"),
]
});
@@ -933,4 +938,77 @@ public sealed class SqlTestFixture : IAsyncLifetime
await _connection.ExecuteAsync(createUqIndex);
await _connection.ExecuteAsync(createCoveringIndex);
}
private async Task EnsureV017SchemaAsync()
{
const string createProductType = """
IF OBJECT_ID(N'dbo.ProductType', N'U') IS NULL
BEGIN
CREATE TABLE dbo.ProductType (
Id INT IDENTITY(1,1) NOT NULL CONSTRAINT PK_ProductType PRIMARY KEY,
Nombre NVARCHAR(200) COLLATE SQL_Latin1_General_CP1_CI_AI NOT NULL,
HasDuration BIT NOT NULL CONSTRAINT DF_ProductType_HasDuration DEFAULT(0),
RequiresText BIT NOT NULL CONSTRAINT DF_ProductType_RequiresText DEFAULT(0),
RequiresCategory BIT NOT NULL CONSTRAINT DF_ProductType_RequiresCategory DEFAULT(0),
IsBundle BIT NOT NULL CONSTRAINT DF_ProductType_IsBundle DEFAULT(0),
AllowImages BIT NOT NULL CONSTRAINT DF_ProductType_AllowImages DEFAULT(0),
MaxImages INT NULL,
MaxImageSizeMB DECIMAL(10,2) NULL,
MaxImageWidth INT NULL,
MaxImageHeight INT NULL,
IsActive BIT NOT NULL CONSTRAINT DF_ProductType_IsActive DEFAULT(1),
FechaCreacion DATETIME2(3) NOT NULL CONSTRAINT DF_ProductType_FechaCreacion DEFAULT(SYSUTCDATETIME()),
FechaModificacion DATETIME2(3) NULL
);
END
""";
const string addProductTypePeriod = """
IF COL_LENGTH('dbo.ProductType', 'ValidFrom') IS NULL
BEGIN
ALTER TABLE dbo.ProductType
ADD
ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
CONSTRAINT DF_ProductType_ValidFrom DEFAULT(SYSUTCDATETIME()),
ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL
CONSTRAINT DF_ProductType_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
END
""";
const string setProductTypeVersioning = """
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.ProductType') AND temporal_type = 2)
BEGIN
ALTER TABLE dbo.ProductType
SET (SYSTEM_VERSIONING = ON (
HISTORY_TABLE = dbo.ProductType_History,
HISTORY_RETENTION_PERIOD = 10 YEARS
));
END
""";
const string createUqIndex = """
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UQ_ProductType_Nombre_Activo' AND object_id = OBJECT_ID('dbo.ProductType'))
BEGIN
CREATE UNIQUE INDEX UQ_ProductType_Nombre_Activo
ON dbo.ProductType(Nombre)
WHERE IsActive = 1;
END
""";
const string createCoveringIndex = """
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_ProductType_IsActive_Cover' AND object_id = OBJECT_ID('dbo.ProductType'))
BEGIN
CREATE INDEX IX_ProductType_IsActive_Cover
ON dbo.ProductType(IsActive)
INCLUDE (Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle, AllowImages);
END
""";
await _connection.ExecuteAsync(createProductType);
await _connection.ExecuteAsync(addProductTypePeriod);
await _connection.ExecuteAsync(setProductTypeVersioning);
await _connection.ExecuteAsync(createUqIndex);
await _connection.ExecuteAsync(createCoveringIndex);
}
}