using FluentAssertions; using SIGCM2.Domain.Entities; namespace SIGCM2.Application.Tests.Domain; /// /// PRD-003 — Domain unit tests for ProductPrice record. /// Covers: §REQ-4.1 (IsActive), §REQ-4.2 (CoversDate inclusive bounds), /// §REQ-4.4 (active row covers any date on or after PriceValidFrom). /// public class ProductPriceTests { private static ProductPrice MakePrice( long id = 1, int productId = 10, decimal price = 1500.00m, DateOnly? from = null, DateOnly? to = null, DateTime? fechaCreacion = null) => new( Id: id, ProductId: productId, Price: price, PriceValidFrom: from ?? new DateOnly(2026, 4, 1), PriceValidTo: to, FechaCreacion: fechaCreacion ?? new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc)); // ── §REQ-4.1: IsActive ──────────────────────────────────────────────────── [Fact] public void IsActive_TrueWhenPriceValidToIsNull() { var price = MakePrice(to: null); price.IsActive.Should().BeTrue(); } [Fact] public void IsActive_FalseWhenPriceValidToHasValue() { var price = MakePrice(to: new DateOnly(2026, 4, 30)); price.IsActive.Should().BeFalse(); } // ── §REQ-4.2: CoversDate — inclusive lower bound ────────────────────────── [Fact] public void CoversDate_InclusiveLowerBound_ReturnsTrue() { // Row: 2026-04-01 → 2026-04-30. Query date = PriceValidFrom. var price = MakePrice(from: new DateOnly(2026, 4, 1), to: new DateOnly(2026, 4, 30)); price.CoversDate(new DateOnly(2026, 4, 1)).Should().BeTrue(); } // ── §REQ-4.2: CoversDate — inclusive upper bound ────────────────────────── [Fact] public void CoversDate_InclusiveUpperBound_ReturnsTrue() { // Row: 2026-04-01 → 2026-04-30. Query date = PriceValidTo. var price = MakePrice(from: new DateOnly(2026, 4, 1), to: new DateOnly(2026, 4, 30)); price.CoversDate(new DateOnly(2026, 4, 30)).Should().BeTrue(); } // ── §REQ-4.2: CoversDate — exclusive outside range ──────────────────────── [Fact] public void CoversDate_DateBeforePriceValidFrom_ReturnsFalse() { var price = MakePrice(from: new DateOnly(2026, 4, 10), to: new DateOnly(2026, 4, 20)); price.CoversDate(new DateOnly(2026, 4, 9)).Should().BeFalse(); } [Fact] public void CoversDate_DateAfterPriceValidTo_ReturnsFalse() { var price = MakePrice(from: new DateOnly(2026, 4, 10), to: new DateOnly(2026, 4, 20)); price.CoversDate(new DateOnly(2026, 4, 21)).Should().BeFalse(); } // ── §REQ-4.4: Active row (PriceValidTo = null) covers any date on or after PriceValidFrom ── [Fact] public void ActiveRow_CoversAnyDateOnOrAfterPvf() { // Active row: PriceValidFrom = 2026-04-01, PriceValidTo = null var price = MakePrice(from: new DateOnly(2026, 4, 1), to: null); // On PriceValidFrom itself price.CoversDate(new DateOnly(2026, 4, 1)).Should().BeTrue(); // Well in the future price.CoversDate(new DateOnly(2030, 12, 31)).Should().BeTrue(); } [Fact] public void ActiveRow_DoesNotCoverDateBeforePvf() { var price = MakePrice(from: new DateOnly(2026, 4, 1), to: null); price.CoversDate(new DateOnly(2026, 3, 31)).Should().BeFalse(); } // ── Record value equality (records compare by value) ───────────────────── [Fact] public void Equality_TwoIdenticalRecords_AreEqual() { var a = MakePrice(id: 1, productId: 10, price: 1500m, from: new DateOnly(2026, 4, 1), to: null, fechaCreacion: new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc)); var b = MakePrice(id: 1, productId: 10, price: 1500m, from: new DateOnly(2026, 4, 1), to: null, fechaCreacion: new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc)); a.Should().Be(b); (a == b).Should().BeTrue(); } [Fact] public void Equality_DifferentId_AreNotEqual() { var a = MakePrice(id: 1); var b = MakePrice(id: 2); a.Should().NotBe(b); } // ── Immutability: record positional props are init-only (no regular set) ─ [Fact] public void Record_PropertiesAreInitOnly_NotMutableSet() { // C# records expose init-only accessors, not regular set. // IsExternalInit attribute on the setter's return type distinguishes init from set. var type = typeof(ProductPrice); var mutableSetters = type.GetProperties() .Where(p => { var setter = p.GetSetMethod(nonPublic: false); if (setter is null) return false; // init-only setters have a modreq on IsExternalInit in their signature; // a simple way to detect them: they are NOT init-only when // ReturnParameter.GetRequiredCustomModifiers() does NOT contain IsExternalInit. var modifiers = setter.ReturnParameter.GetRequiredCustomModifiers(); bool isInitOnly = modifiers.Any(m => m.Name == "IsExternalInit"); return !isInitOnly; // only flag non-init setters as mutable }) .ToList(); mutableSetters.Should().BeEmpty("ProductPrice must be immutable — all setters should be init-only."); } }