feat(domain): ProductPrice entity + exceptions (PRD-003)

This commit is contained in:
2026-04-19 17:59:43 -03:00
parent 59f30cddfb
commit 54b0265994
6 changed files with 371 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
namespace SIGCM2.Domain.Entities;
/// <summary>
/// PRD-003 — Immutable record representing a price snapshot for a Product.
/// Business dates are Cat2 (civil Argentina dates, DateOnly). Forward-only — no mutations.
/// PriceValidTo = null means the row is the currently active price.
/// </summary>
public sealed record ProductPrice(
long Id,
int ProductId,
decimal Price,
DateOnly PriceValidFrom,
DateOnly? PriceValidTo,
DateTime FechaCreacion)
{
/// <summary>True if this row is the currently active price (PriceValidTo is null).</summary>
public bool IsActive => PriceValidTo is null;
/// <summary>
/// True if this price's window covers the given civil date (inclusive on both ends).
/// An active row (PriceValidTo = null) covers any date on or after PriceValidFrom.
/// </summary>
public bool CoversDate(DateOnly date)
=> PriceValidFrom <= date && (PriceValidTo is null || PriceValidTo >= date);
}

View File

@@ -0,0 +1,23 @@
namespace SIGCM2.Domain.Exceptions;
/// <summary>
/// Thrown when attempting to add a ProductPrice with a PriceValidFrom that is not strictly
/// greater than the currently active price's PriceValidFrom. → HTTP 409
/// </summary>
public sealed class ProductPriceForwardOnlyException : DomainException
{
public int ProductId { get; }
public DateOnly NewPriceValidFrom { get; }
public DateOnly ActivePriceValidFrom { get; }
public ProductPriceForwardOnlyException(
int productId,
DateOnly newPriceValidFrom,
DateOnly activePriceValidFrom)
: base($"El nuevo PriceValidFrom ({newPriceValidFrom:yyyy-MM-dd}) debe ser estrictamente mayor al PriceValidFrom del precio activo ({activePriceValidFrom:yyyy-MM-dd}).")
{
ProductId = productId;
NewPriceValidFrom = newPriceValidFrom;
ActivePriceValidFrom = activePriceValidFrom;
}
}

View File

@@ -0,0 +1,19 @@
namespace SIGCM2.Domain.Exceptions;
/// <summary>
/// Thrown when a ProductPrice value fails domain business validation
/// (e.g., Price &lt;= 0, PriceValidFrom in the past). → HTTP 400
/// Used as defense-in-depth alongside FluentValidation in the Application layer.
/// </summary>
public sealed class ProductPriceInvalidException : DomainException
{
public string Field { get; }
public string Reason { get; }
public ProductPriceInvalidException(string field, string reason)
: base($"Valor inválido para {field}: {reason}")
{
Field = field;
Reason = reason;
}
}

View File

@@ -0,0 +1,18 @@
namespace SIGCM2.Domain.Exceptions;
/// <summary>
/// Thrown when no ProductPrice row covers the requested date for the given product. → HTTP 404
/// Consumers of IProductPricingService may throw this when GetPriceAtAsync returns null.
/// </summary>
public sealed class ProductSinPrecioActivoException : DomainException
{
public int ProductId { get; }
public DateOnly Date { get; }
public ProductSinPrecioActivoException(int productId, DateOnly date)
: base($"No existe precio registrado para el producto {productId} aplicable a la fecha {date:yyyy-MM-dd}.")
{
ProductId = productId;
Date = date;
}
}