From 52ec8c9424bdfb137ea7d7abc0290c364dd12471 Mon Sep 17 00:00:00 2001 From: Jose Carlos Date: Sun, 22 Feb 2026 12:49:36 +0100 Subject: [PATCH] feat: add equals() to Price and Uuid Value Objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D2.2 from Phase 2 roadmap. Violation: Price and Uuid-based identities had no equals() method. Reference equality (===) is always false for two separate instances representing the same value. Source: Evans (Blue Book), p. 98 — "Value objects should be testable for equality. [...] Use the fields of the object to determine equality." Fix: - Price.equals(other: Price): compares amount and currency - Uuid.equals(other: Uuid): compares toString() values (TransactionId inherits this via extension) Tests: added equals() coverage to Price.test.ts and new TransactionId.test.ts (also adds negative amount, non-numeric amount rejection tests for completeness) --- src/Billing/Shared/Domain/ValueObject/Uuid.ts | 11 +++++++++ .../Transaction/Domain/ValueObject/Price.ts | 11 +++++++++ .../Transaction/ValueObject/Price.test.ts | 24 +++++++++++++++++++ .../ValueObject/TransactionId.test.ts | 24 +++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 tests/Billing/Transaction/Domain/Transaction/ValueObject/TransactionId.test.ts diff --git a/src/Billing/Shared/Domain/ValueObject/Uuid.ts b/src/Billing/Shared/Domain/ValueObject/Uuid.ts index 02ebcc9..6638204 100644 --- a/src/Billing/Shared/Domain/ValueObject/Uuid.ts +++ b/src/Billing/Shared/Domain/ValueObject/Uuid.ts @@ -22,4 +22,15 @@ export default abstract class Uuid { public toIdentity(): Domain.Identity { return this.identity; } + + /** + * Value Object equality — two UUID-based identities are equal when + * they represent the same UUID string value. + * + * Source: Evans (Blue Book), p. 98 — "Value objects should be + * testable for equality." + */ + public equals(other: Uuid): boolean { + return this.toString() === other.toString(); + } } diff --git a/src/Billing/Transaction/Domain/ValueObject/Price.ts b/src/Billing/Transaction/Domain/ValueObject/Price.ts index ab3e84d..b8283f0 100644 --- a/src/Billing/Transaction/Domain/ValueObject/Price.ts +++ b/src/Billing/Transaction/Domain/ValueObject/Price.ts @@ -39,4 +39,15 @@ export default class Price { this.amount = amount; this.currency = currency; } + + /** + * Value Object equality — two Price instances are equal when they + * represent the same amount in the same currency. + * + * Source: Evans (Blue Book), p. 98 — "Value objects should be + * testable for equality." + */ + public equals(other: Price): boolean { + return this.amount === other.amount && this.currency === other.currency; + } } diff --git a/tests/Billing/Transaction/Domain/Transaction/ValueObject/Price.test.ts b/tests/Billing/Transaction/Domain/Transaction/ValueObject/Price.test.ts index eb4f329..28469df 100644 --- a/tests/Billing/Transaction/Domain/Transaction/ValueObject/Price.test.ts +++ b/tests/Billing/Transaction/Domain/Transaction/ValueObject/Price.test.ts @@ -13,4 +13,28 @@ describe("Price", () => { const todo = () => new Price("12.5", "GBP"); expect(todo).toThrow(InvalidArgumentException); }); + + test("Negative amount is rejected", () => { + const todo = () => new Price("-1", "EUR"); + expect(todo).toThrow(InvalidArgumentException); + }); + + test("Non-numeric amount is rejected", () => { + const todo = () => new Price("abc", "EUR"); + expect(todo).toThrow(InvalidArgumentException); + }); + + describe("equals", () => { + test("two prices with the same amount and currency are equal", () => { + expect(new Price("10.00", "EUR").equals(new Price("10.00", "EUR"))).toBe(true); + }); + + test("prices with different amounts are not equal", () => { + expect(new Price("10.00", "EUR").equals(new Price("9.99", "EUR"))).toBe(false); + }); + + test("prices with different currencies are not equal", () => { + expect(new Price("10.00", "EUR").equals(new Price("10.00", "USD"))).toBe(false); + }); + }); }); diff --git a/tests/Billing/Transaction/Domain/Transaction/ValueObject/TransactionId.test.ts b/tests/Billing/Transaction/Domain/Transaction/ValueObject/TransactionId.test.ts new file mode 100644 index 0000000..0c18304 --- /dev/null +++ b/tests/Billing/Transaction/Domain/Transaction/ValueObject/TransactionId.test.ts @@ -0,0 +1,24 @@ +import TransactionId from "@Transaction/Domain/ValueObject/TransactionId"; + +const UUID_A = "ae081e7a-ec8c-4ff1-9de5-f70383fe03a7"; +const UUID_B = "f47ac10b-58cc-4372-a567-0e02b2c3d479"; + +describe("TransactionId", () => { + test("two ids with the same UUID are equal", () => { + expect(new TransactionId(UUID_A).equals(new TransactionId(UUID_A))).toBe(true); + }); + + test("two ids with different UUIDs are not equal", () => { + expect(new TransactionId(UUID_A).equals(new TransactionId(UUID_B))).toBe(false); + }); + + test("generates a unique id when constructed without value", () => { + const a = new TransactionId(); + const b = new TransactionId(); + expect(a.equals(b)).toBe(false); + }); + + test("Invalid UUID is rejected", () => { + expect(() => new TransactionId("not-a-uuid")).toThrow(); + }); +});