diff --git a/src/Domain/Models/ValueObjects/Period.php b/src/Domain/Models/ValueObjects/Period.php index 9e75316..af6559f 100644 --- a/src/Domain/Models/ValueObjects/Period.php +++ b/src/Domain/Models/ValueObjects/Period.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Domain\Models\ValueObjects\RequestedCoupon; +namespace Domain\Models\ValueObjects; use Shared\ValueObjects\DateTime; diff --git a/src/Domain/Enums/Currency.php b/src/Shared/Enums/Currency.php similarity index 100% rename from src/Domain/Enums/Currency.php rename to src/Shared/Enums/Currency.php diff --git a/tests/Unit/Domain/Enums/CountryTest.php b/tests/Unit/Domain/Enums/CountryTest.php new file mode 100644 index 0000000..e9c49c6 --- /dev/null +++ b/tests/Unit/Domain/Enums/CountryTest.php @@ -0,0 +1,81 @@ +value())->toBe(1) + ->and(Country::BE->value())->toBe(2) + ->and(Country::LU->value())->toBe(3); + }); + + it('exposes its case name', function (): void { + // THEN + expect(Country::FR->name())->toBe('FR'); + }); + + it('returns the defined label', function (): void { + // THEN + expect(Country::FR->label())->toBe('FR') + ->and(Country::BE->label())->toBe('BE') + ->and(Country::LU->label())->toBe('LU'); + }); + + it('lists all labels keyed by case name', function (): void { + // THEN + expect(Country::toLabels())->toBe([ + 'FR' => 'FR', + 'BE' => 'BE', + 'LU' => 'LU', + ]); + }); + + it('lists all values keyed by case name', function (): void { + // THEN + expect(Country::toValues())->toBe([ + 'FR' => 1, + 'BE' => 2, + 'LU' => 3, + ]); + }); + + it('makes an instance from its backing value', function (): void { + // THEN + expect(Country::make(1))->toBe(Country::FR); + }); + + it('makes an instance from a label', function (): void { + // THEN + expect(Country::make('BE', 'label'))->toBe(Country::BE); + }); + + it('returns the same instance when already an enum case', function (): void { + // THEN + expect(Country::make(Country::LU))->toBe(Country::LU); + }); + + it('returns null for an unknown value', function (): void { + // THEN + expect(Country::make(99))->toBeNull(); + }); + + it('implements equals correctly', function (): void { + // THEN + expect(Country::FR->equals(Country::FR))->toBeTrue() + ->and(Country::FR->equals(Country::BE))->toBeFalse() + ->and(Country::FR->equals(null))->toBeFalse(); + }); + + it('serializes to json as its backing value', function (): void { + // THEN + expect(Country::FR->jsonSerialize())->toBe(1) + ->and(json_encode(Country::FR))->toBe('1'); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/MoneyAmountTest.php b/tests/Unit/Domain/Models/ValueObjects/MoneyAmountTest.php new file mode 100644 index 0000000..089ee70 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/MoneyAmountTest.php @@ -0,0 +1,45 @@ +toBeInstanceOf(MoneyAmount::class) + ->and($amount->value)->toBe($money); + }); + + it('implements equals by delegating to the wrapped Money', function (): void { + // GIVEN + $amount1 = new MoneyAmount(new Money(Currency::EUR, 300)); + $amount2 = new MoneyAmount(new Money(Currency::EUR, 300)); + $amount3 = new MoneyAmount(new Money(Currency::EUR, 500)); + + // THEN + expect($amount1->equals($amount2))->toBeTrue() + ->and($amount1->equals($amount3))->toBeFalse() + ->and($amount1->equals(null))->toBeFalse(); + }); + + it('converts to string by delegating to the wrapped Money', function (): void { + // GIVEN + $amount = new MoneyAmount(new Money(Currency::EUR, 300)); + + // THEN + expect((string) $amount)->toBe((string) $amount->value); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php new file mode 100644 index 0000000..20ff709 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php @@ -0,0 +1,26 @@ +toBeInstanceOf(Period::class) + ->and($period->start)->toBe($start) + ->and($period->end)->toBe($end); + }); +}); diff --git a/tests/Unit/Shared/Enums/CurrencyTest.php b/tests/Unit/Shared/Enums/CurrencyTest.php new file mode 100644 index 0000000..0f01dd3 --- /dev/null +++ b/tests/Unit/Shared/Enums/CurrencyTest.php @@ -0,0 +1,66 @@ +value())->toBe(1); + }); + + it('exposes its case name', function (): void { + // THEN + expect(Currency::EUR->name())->toBe('EUR'); + }); + + it('returns the defined label', function (): void { + // THEN + expect(Currency::EUR->label())->toBe('Euro'); + }); + + it('lists all labels keyed by case name', function (): void { + // THEN + expect(Currency::toLabels())->toBe([ + 'EUR' => 'Euro', + ]); + }); + + it('lists all values keyed by case name', function (): void { + // THEN + expect(Currency::toValues())->toBe([ + 'EUR' => 1, + ]); + }); + + it('makes an instance from its backing value', function (): void { + // THEN + expect(Currency::make(1))->toBe(Currency::EUR); + }); + + it('makes an instance from a label', function (): void { + // THEN + expect(Currency::make('Euro', 'label'))->toBe(Currency::EUR); + }); + + it('returns the same instance when already an enum case', function (): void { + // THEN + expect(Currency::make(Currency::EUR))->toBe(Currency::EUR); + }); + + it('returns null for an unknown value', function (): void { + // THEN + expect(Currency::make(99))->toBeNull(); + }); + + it('implements equals correctly', function (): void { + // THEN + expect(Currency::EUR->equals(Currency::EUR))->toBeTrue() + ->and(Currency::EUR->equals(null))->toBeFalse(); + }); +}); diff --git a/tests/Unit/Shared/ValueObjects/DateTimeTest.php b/tests/Unit/Shared/ValueObjects/DateTimeTest.php new file mode 100644 index 0000000..d028cdd --- /dev/null +++ b/tests/Unit/Shared/ValueObjects/DateTimeTest.php @@ -0,0 +1,86 @@ +value)->toBeInstanceOf(\DateTimeImmutable::class) + ->and($dateTime->value->getTimezone()->getName())->toBe('UTC') + ->and($dateTime->value->format('H:i:s'))->toBe('12:00:00'); + }); + + it('parses a valid date string', function (): void { + // WHEN + $dateTime = new DateTime('2026-01-15'); + + // THEN + expect($dateTime->value->format('Y-m-d'))->toBe('2026-01-15'); + }); + + it('rejects an invalid date string', function (): void { + // WHEN + $action = fn (): DateTime => new DateTime('not-a-date'); + + // THEN + expect($action)->toThrow(InvalidDateFormatException::class); + }); + + it('creates the current instant via now', function (): void { + // GIVEN + \Carbon\CarbonImmutable::setTestNow('2026-06-15T12:00:00Z'); + + // WHEN + $dateTime = DateTime::now(); + + // THEN + expect($dateTime)->toBeInstanceOf(DateTime::class) + ->and($dateTime->value->getTimezone()->getName())->toBe('UTC') + ->and($dateTime->value->format(\DateTimeInterface::ATOM))->toBe('2026-06-15T12:00:00+00:00'); + + // TEARDOWN + \Carbon\CarbonImmutable::setTestNow(); + }); + + it('implements equals correctly', function (): void { + // GIVEN + $a = new DateTime('2026-01-15T10:00:00Z'); + $b = new DateTime('2026-01-15T10:00:00Z'); + $c = new DateTime('2026-01-16T10:00:00Z'); + + // THEN + expect($a->equals($b))->toBeTrue() + ->and($a->equals($c))->toBeFalse() + ->and($a->equals(null))->toBeFalse(); + }); + + it('converts to a RFC3339 string', function (): void { + // GIVEN + $dateTime = new DateTime('2026-01-15T10:00:00Z'); + + // THEN + expect((string) $dateTime)->toBe(DatesHelper::toRfc3339($dateTime->value)); + }); + + it('serializes to json as a RFC3339 string', function (): void { + // GIVEN + $dateTime = new DateTime('2026-01-15T10:00:00Z'); + + // THEN + expect($dateTime->jsonSerialize())->toBe((string) $dateTime); + }); +}); diff --git a/tests/Unit/Shared/ValueObjects/MoneyTest.php b/tests/Unit/Shared/ValueObjects/MoneyTest.php new file mode 100644 index 0000000..e6bd56e --- /dev/null +++ b/tests/Unit/Shared/ValueObjects/MoneyTest.php @@ -0,0 +1,61 @@ +toBeInstanceOf(Money::class) + ->and($money->currency)->toBe(Currency::EUR) + ->and($money->value)->toBe($value); + }); + + it('accepts a zero value', function (): void { + // WHEN + $money = new Money(Currency::EUR, 0); + + // THEN + expect($money->value)->toBe(0); + }); + + it('rejects a negative value', function (): void { + // WHEN + $action = fn (): Money => new Money(Currency::EUR, -1); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Money value cannot be negative.'); + }); + + it('implements equals correctly', function (): void { + // GIVEN + $money1 = new Money(Currency::EUR, 300); + $money2 = new Money(Currency::EUR, 300); + $money3 = new Money(Currency::EUR, 500); + + // THEN + expect($money1->equals($money2))->toBeTrue() + ->and($money1->equals($money3))->toBeFalse() + ->and($money1->equals(null))->toBeFalse(); + }); + + it('converts to string', function (): void { + // GIVEN + $money = new Money(Currency::EUR, 300); + + // THEN + expect((string) $money)->toBe('300 EUR'); + }); +});