feat(domain): ProductPrice entity + exceptions (PRD-003)
This commit is contained in:
25
src/api/SIGCM2.Domain/Entities/ProductPrice.cs
Normal file
25
src/api/SIGCM2.Domain/Entities/ProductPrice.cs
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when a ProductPrice value fails domain business validation
|
||||
/// (e.g., Price <= 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user