From ab1923c05b8501fbdcf3cc2db28ffb7a12696c60 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 23 Sep 2026 13:13:49 +0200 Subject: [PATCH] feat: :sparkles: create Campaign entity and associated VOs --- src/Domain/Models/Entities/Campaign.php | 103 ++++++++++++++++++ .../ValueObjects/Campaign/CampaignCode.php | 18 +++ .../ValueObjects/Campaign/CampaignCoupon.php | 22 ++++ .../ValueObjects/Campaign/CampaignId.php | 16 +++ .../ValueObjects/Campaign/CampaignName.php | 18 +++ .../Campaign/CampaignCodeTest.php | 30 +++++ .../Campaign/CampaignCouponTest.php | 66 +++++++++++ .../ValueObjects/Campaign/CampaignIdTest.php | 28 +++++ .../Campaign/CampaignNameTest.php | 30 +++++ 9 files changed, 331 insertions(+) create mode 100644 src/Domain/Models/Entities/Campaign.php create mode 100644 src/Domain/Models/ValueObjects/Campaign/CampaignCode.php create mode 100644 src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php create mode 100644 src/Domain/Models/ValueObjects/Campaign/CampaignId.php create mode 100644 src/Domain/Models/ValueObjects/Campaign/CampaignName.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignIdTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php new file mode 100644 index 0000000..48fdbf1 --- /dev/null +++ b/src/Domain/Models/Entities/Campaign.php @@ -0,0 +1,103 @@ + + */ +final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier +{ + private function __construct( + public CampaignId $id, + public CampaignCode $code, + public Tag $tag, + public CampaignName $name, + public bool $publication, + public bool $printAllAtOnce, + public Country $availableIn, + /** @var CampaignCoupon[] */ + public array $coupons, + public BrandCode $brand, + public ?\DateTimeInterface $updatedAt = null, + ) { + } + + public function id(): CampaignId + { + return $this->id; + } + + public function createdAt(): \DateTimeInterface + { + return $this->id->toDateTime(); + } + + public function updatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + public static function create( + CampaignCode $code, + Tag $tag, + CampaignName $name, + bool $publication, + bool $printAllAtOnce, + Country $availableIn, + /** @var CampaignCoupon[] */ + array $coupons, + BrandCode $brand, + ?\DateTimeInterface $updatedAt = null, + ): Campaign { + return new self( + id: CampaignId::generate(), + code: $code, + tag: $tag, + name: $name, + publication: $publication, + printAllAtOnce: $printAllAtOnce, + availableIn: $availableIn, + coupons: $coupons, + brand: $brand, + updatedAt: $updatedAt, + ); + } + + public static function hydrate( + CampaignId $id, + CampaignCode $code, + Tag $tag, + CampaignName $name, + bool $publication, + bool $printAllAtOnce, + Country $availableIn, + /** @var CampaignCoupon[] */ + array $coupons, + BrandCode $brand, + ?\DateTimeInterface $updatedAt = null, + ): self { + return new self( + id: $id, + code: $code, + tag: $tag, + name: $name, + publication: $publication, + printAllAtOnce: $printAllAtOnce, + availableIn: $availableIn, + coupons: $coupons, + brand: $brand, + updatedAt: $updatedAt, + ); + } +} diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php new file mode 100644 index 0000000..b0eb68f --- /dev/null +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php @@ -0,0 +1,18 @@ +warningThresholdQty <= 0) { + throw new \DomainException('Campaign Coupon warningThresholdQty must be positive.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignId.php b/src/Domain/Models/ValueObjects/Campaign/CampaignId.php new file mode 100644 index 0000000..108d6c7 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignId.php @@ -0,0 +1,16 @@ +code)->toBe($code); + }); + + it('rejects invalid campaign code', function (): void { + // GIVEN + $code = ''; + + // GIVEN / THEN + expect(fn (): CampaignCode => new CampaignCode($code))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php new file mode 100644 index 0000000..1be0701 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php @@ -0,0 +1,66 @@ +code)->toBe($code) + ->and($campaignCoupon->qty)->toBe($qty) + ->and($campaignCoupon->printRules)->toBe($printRules) + ->and($campaignCoupon->warningThresholdQty)->toBe($warningThresholdQty); + }); + + it('creates a valid campaign coupon whitout print rules', function (): void { + // GIVEN + $code = new ProcessingCode('PC1'); + $qty = new StockId(); + $warningThresholdQty = 100; + + // WHEN + $campaignCoupon = new CampaignCoupon(code: $code, qty: $qty, warningThresholdQty: $warningThresholdQty); + + // THEN + expect($campaignCoupon->code)->toBe($code) + ->and($campaignCoupon->qty)->toBe($qty) + ->and($campaignCoupon->warningThresholdQty)->toBe($warningThresholdQty); + }); + + it('rejects invalid campaign coupon', function (): void { + // GIVEN + $code = new ProcessingCode('PC1'); + $qty = new StockId(); + $printRules = new PrintRules(maxPerCustomer: 3); + $warningThresholdQty = -100; + + // GIVEN / THEN + expect(fn (): CampaignCoupon => new CampaignCoupon(code: $code, qty: $qty, printRules: $printRules, warningThresholdQty: $warningThresholdQty))->toThrow(\DomainException::class); + }); + + it('rejects invalid campaign coupon whitout print rules', function (): void { + // GIVEN + $code = new ProcessingCode('PC1'); + $qty = new StockId(); + $warningThresholdQty = -100; + + // GIVEN / THEN + expect(fn (): CampaignCoupon => new CampaignCoupon(code: $code, qty: $qty, warningThresholdQty: $warningThresholdQty))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignIdTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignIdTest.php new file mode 100644 index 0000000..0e52797 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignIdTest.php @@ -0,0 +1,28 @@ +toBeInstanceOf(CampaignId::class); + }); + + it('generates different ids on each call', function (): void { + // WHEN + $first = CampaignId::generate(); + $second = CampaignId::generate(); + + // THEN + expect((string) $first)->not->toBe((string) $second); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php new file mode 100644 index 0000000..ffbf82f --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php @@ -0,0 +1,30 @@ +value)->toBe($value); + }); + + it('rejects invalid campaign name', function (): void { + // GIVEN + $value = ''; + + // GIVEN / THEN + expect(fn (): CampaignName => new CampaignName($value))->toThrow(\DomainException::class); + }); +});