feat(bd): V019 crea dbo.ProductPrices + SP + índices (PRD-003)
This commit is contained in:
@@ -66,6 +66,9 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
||||
// V018 (PRD-002): ensure dbo.Product + temporal + permiso 'catalogo:productos:gestionar'.
|
||||
await EnsureV018SchemaAsync();
|
||||
|
||||
// V019 (PRD-003): ensure dbo.ProductPrices + temporal + SP usp_AddProductPrice.
|
||||
await EnsureV019SchemaAsync();
|
||||
|
||||
_respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
|
||||
{
|
||||
DbAdapter = DbAdapter.SqlServer,
|
||||
@@ -96,6 +99,8 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
||||
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"),
|
||||
// PRD-003 (V019): ProductPrices es temporal — history protegida por SYSTEM_VERSIONING.
|
||||
new Respawn.Graph.Table("dbo", "ProductPrices_History"),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1122,4 +1127,144 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
||||
await _connection.ExecuteAsync(createMedioIdx);
|
||||
await _connection.ExecuteAsync(createRubroIdx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PRD-003 (V019): applies dbo.ProductPrices + SYSTEM_VERSIONING + indexes + SP usp_AddProductPrice
|
||||
/// idempotently to the test database. Mirrors V019__create_product_prices.sql.
|
||||
/// </summary>
|
||||
public async Task EnsureV019SchemaAsync()
|
||||
{
|
||||
const string createTable = """
|
||||
IF OBJECT_ID(N'dbo.ProductPrices', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.ProductPrices (
|
||||
Id BIGINT IDENTITY(1,1) NOT NULL
|
||||
CONSTRAINT PK_ProductPrices PRIMARY KEY,
|
||||
ProductId INT NOT NULL,
|
||||
Price DECIMAL(12,2) NOT NULL,
|
||||
PriceValidFrom DATE NOT NULL,
|
||||
PriceValidTo DATE NULL,
|
||||
FechaCreacion DATETIME2(3) NOT NULL
|
||||
CONSTRAINT DF_ProductPrices_FechaCreacion DEFAULT(SYSUTCDATETIME()),
|
||||
CONSTRAINT FK_ProductPrices_Product
|
||||
FOREIGN KEY (ProductId) REFERENCES dbo.Product(Id) ON DELETE NO ACTION,
|
||||
CONSTRAINT CK_ProductPrices_Price_Positive
|
||||
CHECK (Price > 0),
|
||||
CONSTRAINT CK_ProductPrices_ValidRange
|
||||
CHECK (PriceValidTo IS NULL OR PriceValidTo >= PriceValidFrom)
|
||||
);
|
||||
END
|
||||
""";
|
||||
|
||||
const string addPeriod = """
|
||||
IF COL_LENGTH('dbo.ProductPrices', 'SysStartTime') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.ProductPrices
|
||||
ADD
|
||||
SysStartTime DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
|
||||
CONSTRAINT DF_ProductPrices_SysStartTime DEFAULT(SYSUTCDATETIME()),
|
||||
SysEndTime DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL
|
||||
CONSTRAINT DF_ProductPrices_SysEndTime DEFAULT(CONVERT(DATETIME2(3),'9999-12-31 23:59:59.999')),
|
||||
PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime);
|
||||
END
|
||||
""";
|
||||
|
||||
const string setVersioning = """
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.ProductPrices') AND temporal_type = 2)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.ProductPrices
|
||||
SET (SYSTEM_VERSIONING = ON (
|
||||
HISTORY_TABLE = dbo.ProductPrices_History,
|
||||
HISTORY_RETENTION_PERIOD = 10 YEARS
|
||||
));
|
||||
END
|
||||
""";
|
||||
|
||||
const string createActiveIndex = """
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UX_ProductPrices_Active' AND object_id = OBJECT_ID('dbo.ProductPrices'))
|
||||
BEGIN
|
||||
CREATE UNIQUE INDEX UX_ProductPrices_Active
|
||||
ON dbo.ProductPrices (ProductId)
|
||||
WHERE PriceValidTo IS NULL;
|
||||
END
|
||||
""";
|
||||
|
||||
const string createLookupIndex = """
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_ProductPrices_Lookup' AND object_id = OBJECT_ID('dbo.ProductPrices'))
|
||||
BEGIN
|
||||
CREATE INDEX IX_ProductPrices_Lookup
|
||||
ON dbo.ProductPrices (ProductId, PriceValidFrom DESC)
|
||||
INCLUDE (Price, PriceValidTo);
|
||||
END
|
||||
""";
|
||||
|
||||
const string createSp = """
|
||||
IF OBJECT_ID(N'dbo.usp_AddProductPrice', N'P') IS NULL
|
||||
EXEC('CREATE PROCEDURE dbo.usp_AddProductPrice AS RETURN 0');
|
||||
""";
|
||||
|
||||
const string alterSp = """
|
||||
ALTER PROCEDURE dbo.usp_AddProductPrice
|
||||
@ProductId INT,
|
||||
@Price DECIMAL(12,2),
|
||||
@PriceValidFrom DATE,
|
||||
@NewId BIGINT OUTPUT,
|
||||
@ClosedId BIGINT OUTPUT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
|
||||
BEGIN TRY
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.Product WITH (NOLOCK) WHERE Id=@ProductId AND IsActive=1)
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
THROW 50404, 'Product not found or inactive', 1;
|
||||
END
|
||||
|
||||
DECLARE @ActiveId BIGINT, @ActivePVF DATE;
|
||||
SELECT TOP 1 @ActiveId = Id, @ActivePVF = PriceValidFrom
|
||||
FROM dbo.ProductPrices WITH (UPDLOCK, HOLDLOCK, ROWLOCK)
|
||||
WHERE ProductId = @ProductId AND PriceValidTo IS NULL;
|
||||
|
||||
IF @ActiveId IS NOT NULL AND @PriceValidFrom <= @ActivePVF
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
THROW 50409, 'ProductPriceForwardOnly: new PriceValidFrom must be > active.PriceValidFrom', 1;
|
||||
END
|
||||
|
||||
IF @ActiveId IS NOT NULL
|
||||
BEGIN
|
||||
UPDATE dbo.ProductPrices
|
||||
SET PriceValidTo = DATEADD(DAY, -1, @PriceValidFrom)
|
||||
WHERE Id = @ActiveId;
|
||||
SET @ClosedId = @ActiveId;
|
||||
END
|
||||
ELSE
|
||||
SET @ClosedId = NULL;
|
||||
|
||||
INSERT INTO dbo.ProductPrices (ProductId, Price, PriceValidFrom, PriceValidTo)
|
||||
VALUES (@ProductId, @Price, @PriceValidFrom, NULL);
|
||||
SET @NewId = SCOPE_IDENTITY();
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
|
||||
THROW;
|
||||
END CATCH
|
||||
END
|
||||
""";
|
||||
|
||||
await _connection.ExecuteAsync(createTable);
|
||||
await _connection.ExecuteAsync(addPeriod);
|
||||
await _connection.ExecuteAsync(setVersioning);
|
||||
await _connection.ExecuteAsync(createActiveIndex);
|
||||
await _connection.ExecuteAsync(createLookupIndex);
|
||||
await _connection.ExecuteAsync(createSp);
|
||||
await _connection.ExecuteAsync(alterSp);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user