diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php index 5df5bfd..f270627 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php @@ -16,5 +16,10 @@ final readonly class CouponProduct /** @var CouponProductInfo[] */ public array $labels, ) { + $trimmed = trim($this->name); + + if ($trimmed === '') { + throw new \DomainException('Coupon Product name cannot be empty.'); + } } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php index d455e1a..f52ed95 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php @@ -15,11 +15,23 @@ final readonly class CouponProductInfo implements Equatable public string $validityText, public Country $forLocale ) { - $trimmed = trim($this->name); + $trimmedName = trim($this->name); - if ($trimmed === '') { + if ($trimmedName === '') { throw new \DomainException('Product Info name cannot be empty.'); } + + $trimmedRange = trim($this->range); + + if ($trimmedRange === '') { + throw new \DomainException('Product Info range cannot be empty.'); + } + + $trimmedValidityText = trim($this->validityText); + + if ($trimmedValidityText === '') { + throw new \DomainException('Product Info validity text cannot be empty.'); + } } #[\Override] diff --git a/src/Domain/Models/ValueObjects/ProcessingCode.php b/src/Domain/Models/ValueObjects/ProcessingCode.php index 8a55717..f5d293b 100644 --- a/src/Domain/Models/ValueObjects/ProcessingCode.php +++ b/src/Domain/Models/ValueObjects/ProcessingCode.php @@ -14,7 +14,7 @@ final readonly class ProcessingCode implements \Stringable, Equatable $trimmed = trim($this->value); if ($trimmed === '') { - throw new \DomainException('GS1 value cannot be empty.'); + throw new \DomainException('Processing Code value cannot be empty.'); } } diff --git a/tests/Unit/Domain/Enums/BarcodeFormatTest.php b/tests/Unit/Domain/Enums/BarcodeFormatTest.php new file mode 100644 index 0000000..80a157b --- /dev/null +++ b/tests/Unit/Domain/Enums/BarcodeFormatTest.php @@ -0,0 +1,81 @@ +value())->toBe(1) + ->and(BarcodeFormat::CODE128->value())->toBe(2) + ->and(BarcodeFormat::GS1_128->value())->toBe(3); + }); + + it('exposes its case name', function (): void { + // THEN + expect(BarcodeFormat::EAN13->name())->toBe('EAN13'); + }); + + it('returns the defined label', function (): void { + // THEN + expect(BarcodeFormat::EAN13->label())->toBe('EAN') + ->and(BarcodeFormat::CODE128->label())->toBe('CODE 128') + ->and(BarcodeFormat::GS1_128->label())->toBe('GS1_128'); + }); + + it('lists all labels keyed by case name', function (): void { + // THEN + expect(BarcodeFormat::toLabels())->toBe([ + 'EAN13' => 'EAN', + 'CODE128' => 'CODE 128', + 'GS1_128' => 'GS1_128', + ]); + }); + + it('lists all values keyed by case name', function (): void { + // THEN + expect(BarcodeFormat::toValues())->toBe([ + 'EAN13' => 1, + 'CODE128' => 2, + 'GS1_128' => 3, + ]); + }); + + it('makes an instance from its backing value', function (): void { + // THEN + expect(BarcodeFormat::make(1))->toBe(BarcodeFormat::EAN13); + }); + + it('makes an instance from a label', function (): void { + // THEN + expect(BarcodeFormat::make('CODE 128', 'label'))->toBe(BarcodeFormat::CODE128); + }); + + it('returns the same instance when already an enum case', function (): void { + // THEN + expect(BarcodeFormat::make(BarcodeFormat::GS1_128))->toBe(BarcodeFormat::GS1_128); + }); + + it('returns null for an unknown value', function (): void { + // THEN + expect(BarcodeFormat::make(99))->toBeNull(); + }); + + it('implements equals correctly', function (): void { + // THEN + expect(BarcodeFormat::EAN13->equals(BarcodeFormat::EAN13))->toBeTrue() + ->and(BarcodeFormat::EAN13->equals(BarcodeFormat::CODE128))->toBeFalse() + ->and(BarcodeFormat::EAN13->equals(null))->toBeFalse(); + }); + + it('serializes to json as its backing value', function (): void { + // THEN + expect(BarcodeFormat::EAN13->jsonSerialize())->toBe(1) + ->and(json_encode(BarcodeFormat::EAN13))->toBe('1'); + }); +}); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php new file mode 100644 index 0000000..6b06246 --- /dev/null +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -0,0 +1,264 @@ +id())->toBe($id) + ->and($coupon->name)->toBe($name) + ->and($coupon->status)->toBe($status) + ->and($coupon->productName)->toBe($productName) + ->and($coupon->amount)->toBe($amount) + ->and($coupon->picture)->toBe($picture) + ->and($coupon->product)->toBe($product) + ->and($coupon->diffusion)->toBe($diffusion) + ->and($coupon->availability)->toBe($availability) + ->and($coupon->validity)->toBe($validity) + ->and($coupon->legal)->toBe($legal) + ->and($coupon->gtin)->toBe($gtin) + ->and($coupon->code)->toBe($code) + ->and($coupon->brandCode)->toBe($brandCode) + ->and($coupon->updatedAt)->toBeNull() + ->and($coupon->updatedAt())->toBeNull(); + }); + + it('creates a coupon with an explicit updatedAt date', function (): void { + // GIVEN + $id = CouponId::generate(); + $name = new CouponName('Name'); + $status = \boolval(0); + $productName = new CouponProductName('Lorem ipsum'); + $amount = new MoneyAmount(new Money(Currency::EUR, 1)); + $picture = new CouponPicture(new PicturePath('files/product.jpg'), new PicturePath('files/thumbnail.jpg')); + $product = new CouponProduct('Lorem ipsum', [], []); + $diffusion = new CouponDiffusion(['2026-08-25', '2026-10-31']); + $availability = new CouponAvailabilityCalendar(['2026-08-25', '2026-10-31']); + $validity = new CouponValidity(30, new DateTime('2026-11-30')); + $legal = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR); + $gtin = new GS1('110101010', BarcodeFormat::EAN13); + $code = new ProcessingCode('5425003944130'); + $brandCode = new BrandCode('AAA01'); + $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); + + // WHEN + $coupon = Coupon::hydrate( + $id, + $name, + $status, + $productName, + $amount, + $picture, + $product, + $diffusion, + $availability, + $validity, + $legal, + $gtin, + $code, + $brandCode, + $updatedAt + ); + + // THEN + expect($coupon->updatedAt)->toBe($updatedAt) + ->and($coupon->updatedAt())->toBe($updatedAt); + }); + + it('creates a new coupon with a freshly generated id', function (): void { + // GIVEN + $name = new CouponName('Name'); + $status = \boolval(0); + $productName = new CouponProductName('Lorem ipsum'); + $amount = new MoneyAmount(new Money(Currency::EUR, 1)); + $picture = new CouponPicture(new PicturePath('files/product.jpg'), new PicturePath('files/thumbnail.jpg')); + $product = new CouponProduct('Lorem ipsum', [], []); + $diffusion = new CouponDiffusion(['2026-08-25', '2026-10-31']); + $availability = new CouponAvailabilityCalendar(['2026-08-25', '2026-10-31']); + $validity = new CouponValidity(30, new DateTime('2026-11-30')); + $legal = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR); + $gtin = new GS1('110101010', BarcodeFormat::EAN13); + $code = new ProcessingCode('5425003944130'); + $brandCode = new BrandCode('AAA01'); + + // WHEN + $coupon = Coupon::create( + $name, + $status, + $productName, + $amount, + $picture, + $product, + $diffusion, + $availability, + $validity, + $legal, + $gtin, + $code, + $brandCode + ); + + // THEN + expect($coupon->id())->toBeInstanceOf(CouponId::class) + ->and($coupon->name)->toBe($name) + ->and($coupon->status)->toBe($status) + ->and($coupon->productName)->toBe($productName) + ->and($coupon->amount)->toBe($amount) + ->and($coupon->picture)->toBe($picture) + ->and($coupon->product)->toBe($product) + ->and($coupon->diffusion)->toBe($diffusion) + ->and($coupon->availability)->toBe($availability) + ->and($coupon->validity)->toBe($validity) + ->and($coupon->legal)->toBe($legal) + ->and($coupon->gtin)->toBe($gtin) + ->and($coupon->code)->toBe($code) + ->and($coupon->brandCode)->toBe($brandCode) + ->and($coupon->updatedAt)->toBeNull() + ->and($coupon->updatedAt())->toBeNull(); + }); + + it('generates a different id for each created coupon', function (): void { + // GIVEN + $name = new CouponName('Name'); + $status = \boolval(0); + $productName = new CouponProductName('Lorem ipsum'); + $amount = new MoneyAmount(new Money(Currency::EUR, 1)); + $picture = new CouponPicture(new PicturePath('files/product.jpg'), new PicturePath('files/thumbnail.jpg')); + $product = new CouponProduct('Lorem ipsum', [], []); + $diffusion = new CouponDiffusion(['2026-08-25', '2026-10-31']); + $availability = new CouponAvailabilityCalendar(['2026-08-25', '2026-10-31']); + $validity = new CouponValidity(30, new DateTime('2026-11-30')); + $legal = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR); + $gtin = new GS1('110101010', BarcodeFormat::EAN13); + $code = new ProcessingCode('5425003944130'); + $brandCode = new BrandCode('AAA01'); + + // WHEN + $firstCoupon = Coupon::create( + $name, + $status, + $productName, + $amount, + $picture, + $product, + $diffusion, + $availability, + $validity, + $legal, + $gtin, + $code, + $brandCode + ); + $secondCoupon = Coupon::create( + $name, + $status, + $productName, + $amount, + $picture, + $product, + $diffusion, + $availability, + $validity, + $legal, + $gtin, + $code, + $brandCode + ); + + // THEN + expect($firstCoupon->id()->equals($secondCoupon->id()))->toBeFalse(); + }); + + it('returns the creation date extracted from the id', function (): void { + // GIVEN + $expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC')); + $id = new CouponId(Uuid::uuid7($expected)); + $coupon = Coupon::hydrate( + $id, + new CouponName('Name'), + \boolval(0), + new CouponProductName('Lorem ipsum'), + new MoneyAmount(new Money(Currency::EUR, 1)), + new CouponPicture(new PicturePath('files/product.jpg'), new PicturePath('files/thumbnail.jpg')), + new CouponProduct('Lorem ipsum', [], []), + new CouponDiffusion(['2026-08-25', '2026-10-31']), + new CouponAvailabilityCalendar(['2026-08-25', '2026-10-31']), + new CouponValidity(30, new DateTime('2026-11-30')), + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR), + new GS1('110101010', BarcodeFormat::EAN13), + new ProcessingCode('5425003944130'), + new BrandCode('AAA01') + ); + + // WHEN + $result = $coupon->createdAt(); + + // THEN + expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v')); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php new file mode 100644 index 0000000..354ed37 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php @@ -0,0 +1,30 @@ +periods)->toBe($periods); + }); + + it('rejects invalid coupon availability calendar', function (): void { + // GIVEN + $periods = []; + + // GIVEN / THEN + expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar($periods))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php new file mode 100644 index 0000000..5b69a36 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php @@ -0,0 +1,30 @@ +periods)->toBe($periods); + }); + + it('rejects invalid coupon diffusion', function (): void { + // GIVEN + $periods = []; + + // GIVEN / THEN + expect(fn (): CouponDiffusion => new CouponDiffusion($periods))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIdTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIdTest.php new file mode 100644 index 0000000..b4b1aee --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIdTest.php @@ -0,0 +1,28 @@ +toBeInstanceOf(CouponId::class); + }); + + it('generates different ids on each call', function (): void { + // WHEN + $first = CouponId::generate(); + $second = CouponId::generate(); + + // THEN + expect((string) $first)->not->toBe((string) $second); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php new file mode 100644 index 0000000..7f46ee8 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php @@ -0,0 +1,34 @@ +legalText)->toBe($legalText) + ->and($couponLegalInformation->forLocale)->toBe($forLocale); + }); + + it('rejects invalid coupon legal information', function (): void { + // GIVEN + $legalText = ''; + $forLocale = Country::FR; + + // GIVEN / THEN + expect(fn (): CouponLegalInformation => new CouponLegalInformation($legalText, $forLocale))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php new file mode 100644 index 0000000..473545f --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php @@ -0,0 +1,40 @@ +name)->toBe($name) + ->and($couponProductInfo->range)->toBe($range) + ->and($couponProductInfo->validityText)->toBe($validityText) + ->and($couponProductInfo->forLocale)->toBe($forLocale); + }); + + it('rejects invalid coupon product info', function (): void { + // GIVEN + $name = ''; + $range = ''; + $validityText = ''; + $forLocale = Country::FR; + + // GIVEN / THEN + expect(fn (): CouponProductInfo => new CouponProductInfo($name, $range, $validityText, $forLocale))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductTest.php new file mode 100644 index 0000000..9f593a6 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductTest.php @@ -0,0 +1,36 @@ +name)->toBe($name) + ->and($couponProduct->incoCodes)->toBe($incoCodes) + ->and($couponProduct->labels)->toBe($labels); + }); + + it('rejects invalid coupon product', function (): void { + // GIVEN + $name = ''; + $incoCodes = []; + $labels = []; + + // GIVEN / THEN + expect(fn (): CouponProduct => new CouponProduct($name, $incoCodes, $labels))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php new file mode 100644 index 0000000..005138e --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php @@ -0,0 +1,34 @@ +days)->toBe($days) + ->and($couponValidity->until)->toBe($until); + }); + + it('rejects invalid coupon validity', function (): void { + // GIVEN + $days = 0; + $until = new DateTime('2026-11-30'); + + // GIVEN / THEN + expect(fn (): CouponValidity => new CouponValidity($days, $until))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/CouponNameTest.php b/tests/Unit/Domain/Models/ValueObjects/CouponNameTest.php new file mode 100644 index 0000000..8125316 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CouponNameTest.php @@ -0,0 +1,30 @@ +value)->toBe($value); + }); + + it('rejects invalid coupon name', function (): void { + // GIVEN + $value = ''; + + // GIVEN / THEN + expect(fn (): CouponName => new CouponName($value))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/CouponProductNameTest.php b/tests/Unit/Domain/Models/ValueObjects/CouponProductNameTest.php new file mode 100644 index 0000000..0c96164 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CouponProductNameTest.php @@ -0,0 +1,30 @@ +value)->toBe($value); + }); + + it('rejects invalid coupon product name', function (): void { + // GIVEN + $value = ''; + + // GIVEN / THEN + expect(fn (): CouponProductName => new CouponProductName($value))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/GS1Test.php b/tests/Unit/Domain/Models/ValueObjects/GS1Test.php new file mode 100644 index 0000000..f6cf85c --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/GS1Test.php @@ -0,0 +1,34 @@ +value)->toBe($value) + ->and($GS1->format)->toBe($format); + }); + + it('rejects invalid gs1', function (): void { + // GIVEN + $value = ''; + $format = BarcodeFormat::EAN13; + + // GIVEN / THEN + expect(fn (): GS1 => new GS1($value, $format))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php new file mode 100644 index 0000000..c7fe5cb --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php @@ -0,0 +1,30 @@ +value)->toBe($value); + }); + + it('rejects invalid inco code', function (): void { + // GIVEN + $value = 0; + + // GIVEN / THEN + expect(fn (): IncoCode => new IncoCode($value))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/ProcessingCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/ProcessingCodeTest.php new file mode 100644 index 0000000..ae020bc --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/ProcessingCodeTest.php @@ -0,0 +1,30 @@ +value)->toBe($value); + }); + + it('rejects invalid processing code', function (): void { + // GIVEN + $value = ''; + + // GIVEN / THEN + expect(fn (): ProcessingCode => new ProcessingCode($value))->toThrow(\DomainException::class); + }); +});