From 59e4dbf2eecefd9b162e3ce53b42c134a54afbdd Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Fri, 25 Sep 2026 09:44:57 +0200 Subject: [PATCH] feat: :sparkles: add validations to entities and value objects --- src/Domain/Models/Entities/Campaign.php | 5 + src/Domain/Models/Entities/Coupon.php | 21 ++- src/Domain/Models/Entities/Stock.php | 5 + .../CampaignRequestFilters.php | 3 + .../CampaignRequestSortCriteria.php | 6 + .../Coupon/CouponAvailabilityCalendar.php | 6 + .../ValueObjects/Coupon/CouponDiffusion.php | 6 + src/Domain/Models/ValueObjects/PrintRules.php | 11 ++ .../Domain/Models/Entities/CampaignTest.php | 53 ++++++ .../Domain/Models/Entities/CouponTest.php | 169 +++++++++--------- .../Unit/Domain/Models/Entities/StockTest.php | 72 +++++++- .../CampaignRequestFiltersTest.php | 56 ++++++ .../CampaignRequestSortCriteriaTest.php | 38 +++- .../Coupon/CouponAvailabilityCalendarTest.php | 33 +++- .../Coupon/CouponDiffusionTest.php | 33 +++- .../Models/ValueObjects/PrintRulesTest.php | 32 +++- 16 files changed, 439 insertions(+), 110 deletions(-) create mode 100644 tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php index b4068c4..4030a0e 100644 --- a/src/Domain/Models/Entities/Campaign.php +++ b/src/Domain/Models/Entities/Campaign.php @@ -31,6 +31,11 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier public BrandCode $brand, public ?\DateTimeInterface $updatedAt = null, ) { + foreach ($this->coupons as $coupon) { + if (!$coupon instanceof CampaignCoupon) { + throw new \DomainException('Campaign coupons must be CampaignCoupon instances.'); + } + } } public function id(): CampaignId diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index 563b12e..33052b9 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -37,14 +37,19 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier public CouponDiffusion $diffusion, public CouponAvailabilityCalendar $availability, public CouponValidity $validity, - /** @var array $legal */ - public array $legal, + /** @var array $legalInformation */ + public array $legalInformation, public ProcessingCode $code, public BrandCode $brandCode, public ActorId $createdBy, public ?GS1 $gtin = null, public ?\DateTimeInterface $updatedAt = null, ) { + foreach ($this->legalInformation as $legal) { + if (!$legal instanceof CouponLegalInformation) { + throw new \DomainException('Coupon legal information must be CouponLegalInformation instances.'); + } + } } public function id(): CouponId @@ -63,7 +68,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier } /** - * @param array $legal + * @param array $legalInformation */ public static function create( CouponName $name, @@ -75,7 +80,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier CouponDiffusion $diffusion, CouponAvailabilityCalendar $availability, CouponValidity $validity, - array $legal, + array $legalInformation, ProcessingCode $code, BrandCode $brandCode, ActorId $createdBy, @@ -93,7 +98,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier diffusion: $diffusion, availability: $availability, validity: $validity, - legal: $legal, + legalInformation: $legalInformation, code: $code, brandCode: $brandCode, createdBy: $createdBy, @@ -103,7 +108,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier } /** - * @param array $legal + * @param array $legalInformation */ public static function hydrate( CouponId $id, @@ -116,7 +121,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier CouponDiffusion $diffusion, CouponAvailabilityCalendar $availability, CouponValidity $validity, - array $legal, + array $legalInformation, ProcessingCode $code, BrandCode $brandCode, ActorId $createdBy, @@ -134,7 +139,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier diffusion: $diffusion, availability: $availability, validity: $validity, - legal: $legal, + legalInformation: $legalInformation, code: $code, brandCode: $brandCode, createdBy: $createdBy, diff --git a/src/Domain/Models/Entities/Stock.php b/src/Domain/Models/Entities/Stock.php index 69693f1..06cd24d 100644 --- a/src/Domain/Models/Entities/Stock.php +++ b/src/Domain/Models/Entities/Stock.php @@ -26,6 +26,11 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier public array $stockAdjustment, public ?\DateTimeInterface $updatedAt = null, ) { + foreach ($this->stockAdjustment as $adjustment) { + if (!$adjustment instanceof StockAdjustment) { + throw new \DomainException('Stock adjustment must be StockAdjustment instances.'); + } + } } public function id(): StockId diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php index b6d6dc3..e60445d 100644 --- a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php +++ b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php @@ -13,5 +13,8 @@ final readonly class CampaignRequestFilters public ?BrandCode $brandCode = null, public ?CampaignStatus $status = null, ) { + if (null === $this->brandCode && null === $this->status) { + throw new \InvalidArgumentException('CampaignRequestFilters require at least a brand code or a status.'); + } } } diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteria.php b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteria.php index 896d793..e91f9a6 100644 --- a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteria.php +++ b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteria.php @@ -13,5 +13,11 @@ final readonly class CampaignRequestSortCriteria if ($this->clauses === []) { throw new \DomainException('Campaign Request Sort Criteria requires at least one clause.'); } + + foreach ($this->clauses as $clause) { + if (!$clause instanceof CampaignRequestSortClause) { + throw new \DomainException('CampaignRequestSortCriteria clauses must be CampaignRequestSortClause instances.'); + } + } } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php b/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php index 9dd9395..6121fe0 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php @@ -17,6 +17,12 @@ final readonly class CouponAvailabilityCalendar implements Equatable if ($this->periods === []) { throw new \DomainException('Diffusion requires at least one period.'); } + + foreach ($this->periods as $period) { + if (!$period instanceof Period) { + throw new \DomainException('CouponAvailabilityCalendar periods must be Period instances.'); + } + } } public function isActiveAt(DateTime $in): bool diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php b/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php index 1a0e4e0..2fef55a 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php @@ -15,5 +15,11 @@ final readonly class CouponDiffusion if ($this->periods === []) { throw new \DomainException('Diffusion requires at least one period.'); } + + foreach ($this->periods as $period) { + if (!$period instanceof Period) { + throw new \DomainException('CouponDiffusion periods must be Period instances.'); + } + } } } diff --git a/src/Domain/Models/ValueObjects/PrintRules.php b/src/Domain/Models/ValueObjects/PrintRules.php index 5f769d6..f915889 100644 --- a/src/Domain/Models/ValueObjects/PrintRules.php +++ b/src/Domain/Models/ValueObjects/PrintRules.php @@ -10,5 +10,16 @@ final readonly class PrintRules public ?int $maxPerCustomer = null, public ?int $cooldownDays = null, ) { + if (null === $this->maxPerCustomer && null === $this->cooldownDays) { + throw new \InvalidArgumentException('PrintRules require at least a max per customer or a cooldown days.'); + } + + if (null !== $this->maxPerCustomer && $this->maxPerCustomer <= 0) { + throw new \InvalidArgumentException('Max per customer must be greater than 0.'); + } + + if (null !== $this->cooldownDays && $this->cooldownDays <= 0) { + throw new \InvalidArgumentException('Cooldown days must be greater than 0.'); + } } } diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index 2132239..1eb807a 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -72,6 +72,34 @@ describe('campaign', function (): void { ->and($campaign->updatedAt())->toBeNull(); }); + it('creates a campaign without coupon', function (): void { + // GIVEN + $id = CampaignId::generate(); + $code = new CampaignCode('Camp100'); + $name = new CampaignName('Name'); + $publication = \boolval(0); + $printAllAtOnce = \boolval(1); + $availableIn = Country::FR; + $coupons = []; + $brand = new BrandCode('AAA01'); + + // WHEN + $campaign = Campaign::hydrate( + $id, + $code, + $this->tag, + $name, + $publication, + $printAllAtOnce, + $availableIn, + $coupons, + $brand, + ); + + // THEN + expect($campaign->coupons)->toBe([]); + }); + it('creates a coupon with an explicit updatedAt date', function (): void { // GIVEN $id = CampaignId::generate(); @@ -197,4 +225,29 @@ describe('campaign', function (): void { // THEN expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v')); }); + + it('rejects a campaign with a non-coupon element', function (): void { + // GIVEN + $id = CampaignId::generate(); + $code = new CampaignCode('Camp100'); + $name = new CampaignName('Name'); + $publication = \boolval(0); + $printAllAtOnce = \boolval(1); + $availableIn = Country::FR; + $coupons = ['PC1']; + $brand = new BrandCode('AAA01'); + + // WHEN / THEN + expect(fn (): Campaign => Campaign::hydrate( + $id, + $code, + $this->tag, + $name, + $publication, + $printAllAtOnce, + $availableIn, + $coupons, + $brand + ))->toThrow(\DomainException::class, 'Campaign coupons must be CampaignCoupon instances.'); + }); }); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index 8351b16..9b485a5 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -21,6 +21,7 @@ use Domain\Models\ValueObjects\CouponPicture; use Domain\Models\ValueObjects\CouponProductName; use Domain\Models\ValueObjects\GS1; use Domain\Models\ValueObjects\MoneyAmount; +use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\ProcessingCode; use Ramsey\Uuid\Uuid; @@ -37,21 +38,21 @@ describe('coupon', function (): void { it('creates a coupon', 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)]; - $code = new ProcessingCode('5425003944130'); - $brandCode = new BrandCode('AAA01'); - $createdBy = ActorId::generate(); - $gtin = new GS1('110101010', BarcodeFormat::EAN13); + $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([new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))]); + $availability = new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]); + $validity = new CouponValidity(30, new DateTime('2026-11-30')); + $legalInformation = [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR)]; + $code = new ProcessingCode('5425003944130'); + $brandCode = new BrandCode('AAA01'); + $createdBy = ActorId::generate(); + $gtin = new GS1('110101010', BarcodeFormat::EAN13); // WHEN $coupon = Coupon::hydrate( @@ -65,7 +66,7 @@ describe('coupon', function (): void { $diffusion, $availability, $validity, - $legal, + $legalInformation, $code, $brandCode, $createdBy, @@ -83,7 +84,7 @@ describe('coupon', function (): void { ->and($coupon->diffusion)->toBe($diffusion) ->and($coupon->availability)->toBe($availability) ->and($coupon->validity)->toBe($validity) - ->and($coupon->legal)->toBe($legal) + ->and($coupon->legalInformation)->toBe($legalInformation) ->and($coupon->code)->toBe($code) ->and($coupon->brandCode)->toBe($brandCode) ->and($coupon->createdBy)->toBe($createdBy) @@ -94,20 +95,20 @@ describe('coupon', function (): void { it('creates a coupon without gs1', 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)]; - $code = new ProcessingCode('5425003944130'); - $brandCode = new BrandCode('AAA01'); - $createdBy = ActorId::generate(); + $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([new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))]); + $availability = new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]); + $validity = new CouponValidity(30, new DateTime('2026-11-30')); + $legalInformation = [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR)]; + $code = new ProcessingCode('5425003944130'); + $brandCode = new BrandCode('AAA01'); + $createdBy = ActorId::generate(); // WHEN $coupon = Coupon::hydrate( @@ -121,7 +122,7 @@ describe('coupon', function (): void { $diffusion, $availability, $validity, - $legal, + $legalInformation, $code, $brandCode, $createdBy, @@ -138,7 +139,7 @@ describe('coupon', function (): void { ->and($coupon->diffusion)->toBe($diffusion) ->and($coupon->availability)->toBe($availability) ->and($coupon->validity)->toBe($validity) - ->and($coupon->legal)->toBe($legal) + ->and($coupon->legalInformation)->toBe($legalInformation) ->and($coupon->code)->toBe($code) ->and($coupon->brandCode)->toBe($brandCode) ->and($coupon->createdBy)->toBe($createdBy) @@ -148,22 +149,22 @@ describe('coupon', function (): void { 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)]; - $code = new ProcessingCode('5425003944130'); - $brandCode = new BrandCode('AAA01'); - $gtin = new GS1('110101010', BarcodeFormat::EAN13); - $createdBy = ActorId::generate(); - $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); + $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([new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))]); + $availability = new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]); + $validity = new CouponValidity(30, new DateTime('2026-11-30')); + $legalInformation = [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR)]; + $code = new ProcessingCode('5425003944130'); + $brandCode = new BrandCode('AAA01'); + $gtin = new GS1('110101010', BarcodeFormat::EAN13); + $createdBy = ActorId::generate(); + $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); // WHEN $coupon = Coupon::hydrate( @@ -177,7 +178,7 @@ describe('coupon', function (): void { $diffusion, $availability, $validity, - $legal, + $legalInformation, $code, $brandCode, $createdBy, @@ -192,20 +193,20 @@ describe('coupon', function (): void { 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)]; - $code = new ProcessingCode('5425003944130'); - $brandCode = new BrandCode('AAA01'); - $createdBy = ActorId::generate(); - $gtin = new GS1('110101010', BarcodeFormat::EAN13); + $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([new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))]); + $availability = new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]); + $validity = new CouponValidity(30, new DateTime('2026-11-30')); + $legalInformation = [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR)]; + $code = new ProcessingCode('5425003944130'); + $brandCode = new BrandCode('AAA01'); + $createdBy = ActorId::generate(); + $gtin = new GS1('110101010', BarcodeFormat::EAN13); // WHEN $coupon = Coupon::create( @@ -218,7 +219,7 @@ describe('coupon', function (): void { $diffusion, $availability, $validity, - $legal, + $legalInformation, $code, $brandCode, $createdBy, @@ -236,7 +237,7 @@ describe('coupon', function (): void { ->and($coupon->diffusion)->toBe($diffusion) ->and($coupon->availability)->toBe($availability) ->and($coupon->validity)->toBe($validity) - ->and($coupon->legal)->toBe($legal) + ->and($coupon->legalInformation)->toBe($legalInformation) ->and($coupon->code)->toBe($code) ->and($coupon->brandCode)->toBe($brandCode) ->and($coupon->createdBy)->toBe($createdBy) @@ -247,20 +248,20 @@ describe('coupon', function (): void { 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)]; - $code = new ProcessingCode('5425003944130'); - $brandCode = new BrandCode('AAA01'); - $createdBy = ActorId::generate(); - $gtin = new GS1('110101010', BarcodeFormat::EAN13); + $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([new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))]); + $availability = new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]); + $validity = new CouponValidity(30, new DateTime('2026-11-30')); + $legalInformation = [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR)]; + $code = new ProcessingCode('5425003944130'); + $brandCode = new BrandCode('AAA01'); + $createdBy = ActorId::generate(); + $gtin = new GS1('110101010', BarcodeFormat::EAN13); // WHEN $firstCoupon = Coupon::create( @@ -273,7 +274,7 @@ describe('coupon', function (): void { $diffusion, $availability, $validity, - $legal, + $legalInformation, $code, $brandCode, $createdBy, @@ -289,7 +290,7 @@ describe('coupon', function (): void { $diffusion, $availability, $validity, - $legal, + $legalInformation, $code, $brandCode, $createdBy, @@ -312,8 +313,8 @@ describe('coupon', function (): void { 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 CouponDiffusion([new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))]), + new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]), new CouponValidity(30, new DateTime('2026-11-30')), [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR)], new ProcessingCode('5425003944130'), diff --git a/tests/Unit/Domain/Models/Entities/StockTest.php b/tests/Unit/Domain/Models/Entities/StockTest.php index 637b966..15da8fc 100644 --- a/tests/Unit/Domain/Models/Entities/StockTest.php +++ b/tests/Unit/Domain/Models/Entities/StockTest.php @@ -20,9 +20,9 @@ describe('stock', function (): void { // GIVEN $id = StockId::generate(); $stockAmount = new StockAmount(10); - $stockAdjustment = [new StockAdjustment(5, 'Inventory correction', ActorId::generate())]; $campaignCode = new CampaignCode('AAA01'); $processingCode = new ProcessingCode('PC1'); + $stockAdjustment = [new StockAdjustment(5, 'Inventory correction', ActorId::generate())]; // WHEN $stock = Stock::hydrate($id, $stockAmount, $campaignCode, $processingCode, $stockAdjustment); @@ -30,13 +30,81 @@ describe('stock', function (): void { // THEN expect($stock->id())->toBe($id) ->and($stock->stockAmount)->toBe($stockAmount) - ->and($stock->stockAdjustment)->toBe($stockAdjustment) ->and($stock->campaignCode)->toBe($campaignCode) ->and($stock->processingCode)->toBe($processingCode) + ->and($stock->stockAdjustment)->toBe($stockAdjustment) ->and($stock->updatedAt)->toBeNull() ->and($stock->updatedAt())->toBeNull(); }); + it('hydrates a stock with several stock adjustments', function (): void { + // GIVEN + $id = StockId::generate(); + $stockAmount = new StockAmount(10); + $campaignCode = new CampaignCode('AAA01'); + $processingCode = new ProcessingCode('PC1'); + $stockAdjustment = [ + new StockAdjustment(5, 'Inventory correction', ActorId::generate()), + new StockAdjustment(10, 'Inventory correction', ActorId::generate()), + ]; + + // WHEN + $stock = Stock::hydrate( + $id, + $stockAmount, + $campaignCode, + $processingCode, + $stockAdjustment, + ); + + // THEN + expect($stock->id())->toBe($id) + ->and($stock->stockAmount)->toBe($stockAmount) + ->and($stock->campaignCode)->toBe($campaignCode) + ->and($stock->processingCode)->toBe($processingCode) + ->and($stock->stockAdjustment)->toBe($stockAdjustment) + ->and($stock->updatedAt)->toBeNull() + ->and($stock->updatedAt())->toBeNull(); + }); + + it('hydrates a stock without stock adjustement', function (): void { + // GIVEN + $id = StockId::generate(); + $stockAmount = new StockAmount(10); + $campaignCode = new CampaignCode('AAA01'); + $processingCode = new ProcessingCode('PC1'); + + // WHEN + $stock = Stock::hydrate($id, $stockAmount, $campaignCode, $processingCode); + + // THEN + expect($stock->id())->toBe($id) + ->and($stock->stockAmount)->toBe($stockAmount) + ->and($stock->campaignCode)->toBe($campaignCode) + ->and($stock->processingCode)->toBe($processingCode) + ->and($stock->stockAdjustment)->toBe([]) + ->and($stock->updatedAt)->toBeNull() + ->and($stock->updatedAt())->toBeNull(); + }); + + it('rejects a stock with a non-stock-adjustment element', function (): void { + // GIVEN + $id = StockId::generate(); + $stockAmount = new StockAmount(10); + $campaignCode = new CampaignCode('AAA01'); + $processingCode = new ProcessingCode('PC1'); + $stockAdjustment = [new StockAmount(5)]; + + // WHEN / THEN + expect(fn (): Stock => Stock::hydrate( + $id, + $stockAmount, + $campaignCode, + $processingCode, + $stockAdjustment, + ))->toThrow(\DomainException::class, 'Stock adjustment must be StockAdjustment instances.'); + }); + it('hydrates a stock with an explicit updatedAt date', function (): void { // GIVEN $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php new file mode 100644 index 0000000..4052f9f --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php @@ -0,0 +1,56 @@ +brandCode)->toBe($brandCode) + ->and($filters->status)->toBe($status); + }); + + it('creates filters with only brandCode set', function (): void { + // GIVEN + $brandCode = new BrandCode('AAA01'); + + // WHEN + $filters = new CampaignRequestFilters(brandCode: $brandCode); + + // THEN + expect($filters->brandCode)->toBe($brandCode) + ->and($filters->status)->toBeNull(); + }); + + it('creates filters with only status set', function (): void { + // GIVEN + $status = CampaignStatus::CREATED; + + // WHEN + $filters = new CampaignRequestFilters(status: $status); + + // THEN + expect($filters->brandCode)->toBeNull() + ->and($filters->status)->toBe($status); + }); + + it('rejects filters without any field set', function (): void { + // WHEN / THEN + expect(fn (): CampaignRequestFilters => new CampaignRequestFilters()) + ->toThrow(\InvalidArgumentException::class, 'CampaignRequestFilters require at least a brand code or a status.'); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php index 14f54e6..2f7b8b8 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php @@ -11,9 +11,11 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField; covers(CampaignRequestSortCriteria::class); describe('campaign request sort criteria', function (): void { - it('creates a valid campaign request sort criteria', function (): void { + it('creates a valid campaign request sort criteria with one clause', function (): void { // GIVEN - $clauses = [new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending)]; + $clauses = [ + new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending), + ]; // WHEN $campaignRequestSortCriteria = new CampaignRequestSortCriteria($clauses); @@ -22,11 +24,35 @@ describe('campaign request sort criteria', function (): void { expect($campaignRequestSortCriteria->clauses)->toBe($clauses); }); - it('rejects invalid campaign request sort criteria', function (): void { + it('creates a valid campaign request sort criteria with several clauses', function (): void { // GIVEN - $clauses = []; + $clauses = [ + new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending), + new CampaignRequestSortClause(field: CampaignRequestSortField::Status, direction: \SortDirection::Descending), + ]; - // GIVEN / THEN - expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($clauses))->toThrow(\DomainException::class); + // WHEN + $campaignRequestSortCriteria = new CampaignRequestSortCriteria($clauses); + + // THEN + expect($campaignRequestSortCriteria->clauses)->toBe($clauses); + }); + + it('rejects a campaign request sort criteria without clause', function (): void { + // GIVEN + $clauses = []; + + // WHEN / THEN + expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($clauses)) + ->toThrow(\DomainException::class, 'Campaign Request Sort Criteria requires at least one clause.'); + }); + + it('rejects a campaign request sort criteria with a non-clause element', function (): void { + // GIVEN + $clauses = [CampaignRequestSortField::BrandCode]; + + // WHEN / THEN + expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($clauses)) + ->toThrow(\DomainException::class, 'CampaignRequestSortCriteria clauses must be CampaignRequestSortClause instances.'); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php index 354ed37..0ab0282 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php @@ -5,13 +5,17 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\Coupon; use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar; +use Domain\Models\ValueObjects\Period; +use Shared\ValueObjects\DateTime; covers(CouponAvailabilityCalendar::class); describe('coupon availability calendar', function (): void { - it('creates a valid coupon availability calendar', function (): void { + it('creates a valid coupon availability calendar with one period', function (): void { // GIVEN - $periods = ['2026-08-25', '2026-10-31']; + $periods = [ + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + ]; // WHEN $couponAvailabilityCalendar = new CouponAvailabilityCalendar($periods); @@ -20,11 +24,34 @@ describe('coupon availability calendar', function (): void { expect($couponAvailabilityCalendar->periods)->toBe($periods); }); - it('rejects invalid coupon availability calendar', function (): void { + it('creates a valid coupon availability calendar with several periods', function (): void { + // GIVEN + $periods = [ + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + new Period(new DateTime('2026-03-01'), new DateTime('2026-04-01')), + ]; + + // WHEN + $couponAvailabilityCalendar = new CouponAvailabilityCalendar($periods); + + // THEN + expect($couponAvailabilityCalendar->periods)->toBe($periods); + }); + + it('rejects a coupon availability calendar without period', function (): void { // GIVEN $periods = []; // GIVEN / THEN expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar($periods))->toThrow(\DomainException::class); }); + + it('rejects a coupon availability calendar with a non-period element', function (): void { + // GIVEN + $periods = ['2026-08-25', '2026-10-31']; + + // WHEN / THEN + expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar($periods)) + ->toThrow(\DomainException::class, 'CouponAvailabilityCalendar periods must be Period instances.'); + }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php index 5b69a36..60d9196 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php @@ -5,13 +5,17 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\Coupon; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; +use Domain\Models\ValueObjects\Period; +use Shared\ValueObjects\DateTime; covers(CouponDiffusion::class); describe('coupon diffusion', function (): void { - it('creates a valid coupon diffusion', function (): void { + it('creates a valid coupon diffusion with one period', function (): void { // GIVEN - $periods = ['2026-08-25', '2026-10-31']; + $periods = [ + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + ]; // WHEN $couponDiffusion = new CouponDiffusion($periods); @@ -20,11 +24,34 @@ describe('coupon diffusion', function (): void { expect($couponDiffusion->periods)->toBe($periods); }); - it('rejects invalid coupon diffusion', function (): void { + it('creates a valid coupon diffusion with several periods', function (): void { + // GIVEN + $periods = [ + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + new Period(new DateTime('2026-03-01'), new DateTime('2026-04-01')), + ]; + + // WHEN + $couponDiffusion = new CouponDiffusion($periods); + + // THEN + expect($couponDiffusion->periods)->toBe($periods); + }); + + it('rejects a coupon diffusion without period', function (): void { // GIVEN $periods = []; // GIVEN / THEN expect(fn (): CouponDiffusion => new CouponDiffusion($periods))->toThrow(\DomainException::class); }); + + it('rejects a coupon diffusion with a non-period element', function (): void { + // GIVEN + $periods = ['2026-08-25', '2026-10-31']; + + // WHEN / THEN + expect(fn (): CouponDiffusion => new CouponDiffusion($periods)) + ->toThrow(\DomainException::class, 'CouponDiffusion periods must be Period instances.'); + }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php index 26092f5..10286c9 100644 --- a/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php @@ -37,12 +37,36 @@ describe('print rules', function (): void { ->and($printRules->cooldownDays)->toBe(45); }); - it('creates print rules with both fields null by default', function (): void { + it('rejects print rules without any field set', function (): void { + // WHEN / THEN + expect(fn (): PrintRules => new PrintRules()) + ->toThrow(\InvalidArgumentException::class, 'PrintRules require at least a max per customer or a cooldown days.'); + }); + + it('accepts the minimum value of 1 for both fields', function (): void { // WHEN - $printRules = new PrintRules(); + $printRules = new PrintRules(maxPerCustomer: 1, cooldownDays: 1); // THEN - expect($printRules->maxPerCustomer)->toBeNull() - ->and($printRules->cooldownDays)->toBeNull(); + expect($printRules->maxPerCustomer)->toBe(1) + ->and($printRules->cooldownDays)->toBe(1); + }); + + it('rejects a maxPerCustomer equal to zero', function (): void { + // GIVEN + $maxPerCustomer = 0; + + // WHEN / THEN + expect(fn (): PrintRules => new PrintRules(maxPerCustomer: $maxPerCustomer)) + ->toThrow(\InvalidArgumentException::class, 'Max per customer must be greater than 0.'); + }); + + it('rejects a cooldownDays equal to zero', function (): void { + // GIVEN + $cooldownDays = 0; + + // WHEN / THEN + expect(fn (): PrintRules => new PrintRules(cooldownDays: $cooldownDays)) + ->toThrow(\InvalidArgumentException::class, 'Cooldown days must be greater than 0.'); }); });