From 0f39559ff9760d2561c788d46103d91cdba4fdba Mon Sep 17 00:00:00 2001 From: K Muller Date: Mon, 21 Sep 2026 14:24:37 +0200 Subject: [PATCH 01/52] feat: :sparkles: add VO and Enums to works on Coupon/CampaignRequest --- src/Domain/Enums/Country.php | 28 +++ src/Domain/Enums/Currency.php | 22 +++ .../Models/ValueObjects/MoneyAmount.php | 30 ++++ .../ValueObjects/RequestedCoupon/Period.php | 16 ++ src/Shared/Enums/BaseEnum.php | 160 ++++++++++++++++++ src/Shared/ValueObjects/DateTime.php | 51 ++++++ src/Shared/ValueObjects/Money.php | 42 +++++ 7 files changed, 349 insertions(+) create mode 100644 src/Domain/Enums/Country.php create mode 100644 src/Domain/Enums/Currency.php create mode 100644 src/Domain/Models/ValueObjects/MoneyAmount.php create mode 100644 src/Domain/Models/ValueObjects/RequestedCoupon/Period.php create mode 100644 src/Shared/Enums/BaseEnum.php create mode 100644 src/Shared/ValueObjects/DateTime.php create mode 100644 src/Shared/ValueObjects/Money.php diff --git a/src/Domain/Enums/Country.php b/src/Domain/Enums/Country.php new file mode 100644 index 0000000..a75b0e4 --- /dev/null +++ b/src/Domain/Enums/Country.php @@ -0,0 +1,28 @@ + */ + use BaseEnum; + + case FR = 1; + case BE = 2; + case LU = 3; + + /** + * Optional labels definition + * + * @phpstan-ignore-next-line + */ + private const LABELS = [ + self::FR->name => 'FR', + self::BE->name => 'BE', + self::LU->name => 'LU', + ]; +} diff --git a/src/Domain/Enums/Currency.php b/src/Domain/Enums/Currency.php new file mode 100644 index 0000000..ac68c2d --- /dev/null +++ b/src/Domain/Enums/Currency.php @@ -0,0 +1,22 @@ + */ + use BaseEnum; + + case EUR = 1; + + /** + * Optional labels definition + * + * @phpstan-ignore-next-line + */ + private const LABELS = [ + self::EUR->name => 'Euro', + ]; +} diff --git a/src/Domain/Models/ValueObjects/MoneyAmount.php b/src/Domain/Models/ValueObjects/MoneyAmount.php new file mode 100644 index 0000000..cb0b753 --- /dev/null +++ b/src/Domain/Models/ValueObjects/MoneyAmount.php @@ -0,0 +1,30 @@ +value->equals($other->value); + } + + public function __toString(): string + { + return (string) $this->value; + } +} diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/Period.php b/src/Domain/Models/ValueObjects/RequestedCoupon/Period.php new file mode 100644 index 0000000..9e75316 --- /dev/null +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/Period.php @@ -0,0 +1,16 @@ +value; + } + + public function label(): string + { + return self::toLabels()[$this->name] ?? ucfirst(strtolower($this->name)); + } + + public function name(): string + { + return $this->name; + } + + /** + * @return array + */ + public static function toLabels(): array + { + static $cache = null; + + if ($cache !== null) { + return $cache; + } + + $labels = defined(self::class . '::' . self::LABELS_CONSTANT_NAME) + ? constant(self::class . '::' . self::LABELS_CONSTANT_NAME) + : null; + + $result = []; + foreach (self::cases() as $enum) { + if ($labels !== null && ! isset($labels[$enum->name])) { + throw new \Error('Missing definition label ' . self::class . " for '{$enum->name}'"); + } + $result[$enum->name] = $labels[$enum->name] ?? ucfirst(strtolower($enum->name)); + } + + return $cache = $result; + } + + /** + * @return array + */ + public static function toValues(): array + { + static $cache = null; + + if ($cache !== null) { + return $cache; + } + + return $cache = array_column( + self::cases(), + 'value', + 'name' + ); + } + + /** + * @param self|V $value + */ + public static function make(self | int | string $value, string $field = 'value'): ?self + { + if ($value instanceof self) { + return $value; + } + + if ($field === 'value') { + $tryValue = null; + try { + $tryValue = self::tryFrom(intval($value)); + } catch (\ValueError) { + $tryValue = self::tryFrom(strval($value)); + } finally { + if ($tryValue !== null) { + return $tryValue; + } + } + } + + if ($field === 'label') { + $normalizedSearch = preg_replace('/[\W_]+/u', '', mb_strtolower((string) $value)); + + foreach (self::cases() as $enum) { + $normalizedLabel = preg_replace('/[\W_]+/u', '', mb_strtolower($enum->label())); + if ($normalizedLabel === $normalizedSearch) { + return $enum; + } + } + + return null; + } + + foreach (self::cases() as $enum) { + if ($enum->{$field} === $value) { + return $enum; + } + } + + return null; + } + + public function equals(mixed $other): bool + { + return $this === $other; + } + + public function jsonSerialize(): mixed + { + return $this->value; + } + + // * PRIVATE + + /** + * @return array> + */ + private static function getCustomDefinitions(): array + { + static $cache = null; + + if ($cache !== null) { + return $cache; + } + + $names = array_column(self::cases(), 'name', 'name'); + $filteredConstants = []; + + foreach ((new \ReflectionClass(self::class))->getConstants() as $key => $constant) { + if ($key === self::LABELS_CONSTANT_NAME || ! \is_array($constant)) { + continue; + } + + if (! empty(array_intersect_key($constant, $names))) { + $filteredConstants[$key] = $constant; + } + } + + return $cache = $filteredConstants; + } +} diff --git a/src/Shared/ValueObjects/DateTime.php b/src/Shared/ValueObjects/DateTime.php new file mode 100644 index 0000000..d8007d1 --- /dev/null +++ b/src/Shared/ValueObjects/DateTime.php @@ -0,0 +1,51 @@ +value = \DateTimeImmutable::createFromInterface($dateTime) + ->setTimezone(new \DateTimeZone('UTC')); + } + + public static function now(): self + { + return new self(DatesHelper::now()); + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self + && $this->value->format(\DateTimeInterface::RFC3339_EXTENDED) === $other->value->format(\DateTimeInterface::RFC3339_EXTENDED); + } + + public function __toString(): string + { + return DatesHelper::toRfc3339($this->value); + } + + public function jsonSerialize(): string + { + return (string) $this; + } +} diff --git a/src/Shared/ValueObjects/Money.php b/src/Shared/ValueObjects/Money.php new file mode 100644 index 0000000..08529cb --- /dev/null +++ b/src/Shared/ValueObjects/Money.php @@ -0,0 +1,42 @@ +value < 0) { + throw new \DomainException('Money value cannot be negative.'); + } + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self + && $this->currency->equals($other->currency) + && $this->value === $other->value; + } + + public function __toString(): string + { + return sprintf('%d %s', $this->value, $this->currency->name()); + } +} From 680bc59d91b4d7e12d4c5ca770979b78d1f4dd03 Mon Sep 17 00:00:00 2001 From: K Muller Date: Mon, 21 Sep 2026 14:57:08 +0200 Subject: [PATCH 02/52] fix: :bug: period changed namespace --- src/Domain/Models/ValueObjects/{RequestedCoupon => }/Period.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Domain/Models/ValueObjects/{RequestedCoupon => }/Period.php (100%) diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/Period.php b/src/Domain/Models/ValueObjects/Period.php similarity index 100% rename from src/Domain/Models/ValueObjects/RequestedCoupon/Period.php rename to src/Domain/Models/ValueObjects/Period.php From cdec541f92b09016900bff4deaa86b721a4b05e0 Mon Sep 17 00:00:00 2001 From: K Muller Date: Mon, 21 Sep 2026 16:46:24 +0200 Subject: [PATCH 03/52] fix: :bug: change some file place + add tests --- src/Domain/Models/ValueObjects/Period.php | 2 +- src/{Domain => Shared}/Enums/Currency.php | 0 tests/Unit/Domain/Enums/CountryTest.php | 81 +++++++++++++++++ .../Models/ValueObjects/MoneyAmountTest.php | 45 ++++++++++ .../RequestedCoupon/PeriodTest.php | 26 ++++++ tests/Unit/Shared/Enums/CurrencyTest.php | 66 ++++++++++++++ .../Unit/Shared/ValueObjects/DateTimeTest.php | 86 +++++++++++++++++++ tests/Unit/Shared/ValueObjects/MoneyTest.php | 61 +++++++++++++ 8 files changed, 366 insertions(+), 1 deletion(-) rename src/{Domain => Shared}/Enums/Currency.php (100%) create mode 100644 tests/Unit/Domain/Enums/CountryTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/MoneyAmountTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php create mode 100644 tests/Unit/Shared/Enums/CurrencyTest.php create mode 100644 tests/Unit/Shared/ValueObjects/DateTimeTest.php create mode 100644 tests/Unit/Shared/ValueObjects/MoneyTest.php 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'); + }); +}); From 5767011375417633172faa29eb22f9c7e15765df Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Mon, 21 Sep 2026 16:59:26 +0200 Subject: [PATCH 04/52] feat: :sparkles: create Coupon entity and associated VOs --- src/Domain/Enums/BarcodeFormat.php | 28 ++++ src/Domain/Models/Entities/Coupon.php | 132 ++++++++++++++++++ .../Coupon/CouponAvailabilityCalendar.php | 48 +++++++ .../ValueObjects/Coupon/CouponDiffusion.php | 19 +++ .../Models/ValueObjects/Coupon/CouponId.php | 16 +++ .../Coupon/CouponLegalInformation.php | 28 ++++ .../ValueObjects/Coupon/CouponProduct.php | 20 +++ .../ValueObjects/Coupon/CouponProductInfo.php | 34 +++++ .../ValueObjects/Coupon/CouponValidity.php | 26 ++++ src/Domain/Models/ValueObjects/CouponName.php | 18 +++ .../Models/ValueObjects/CouponPicture.php | 23 +++ .../Models/ValueObjects/CouponProductName.php | 18 +++ src/Domain/Models/ValueObjects/GS1.php | 33 +++++ src/Domain/Models/ValueObjects/IncoCode.php | 27 ++++ .../Models/ValueObjects/ProcessingCode.php | 31 ++++ 15 files changed, 501 insertions(+) create mode 100644 src/Domain/Enums/BarcodeFormat.php create mode 100644 src/Domain/Models/Entities/Coupon.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponId.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponProduct.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponValidity.php create mode 100644 src/Domain/Models/ValueObjects/CouponName.php create mode 100644 src/Domain/Models/ValueObjects/CouponPicture.php create mode 100644 src/Domain/Models/ValueObjects/CouponProductName.php create mode 100644 src/Domain/Models/ValueObjects/GS1.php create mode 100644 src/Domain/Models/ValueObjects/IncoCode.php create mode 100644 src/Domain/Models/ValueObjects/ProcessingCode.php diff --git a/src/Domain/Enums/BarcodeFormat.php b/src/Domain/Enums/BarcodeFormat.php new file mode 100644 index 0000000..db2dc18 --- /dev/null +++ b/src/Domain/Enums/BarcodeFormat.php @@ -0,0 +1,28 @@ + */ + use BaseEnum; + + case EAN13 = 1; + case CODE128 = 2; + case GS1_128 = 3; + + /** + * Optional labels definition + * + * @phpstan-ignore-next-line + */ + private const LABELS = [ + self::EAN13->name => 'EAN', + self::CODE128->name => 'CODE 128', + self::GS1_128->name => 'GS1_128', + ]; +} diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php new file mode 100644 index 0000000..fdd285b --- /dev/null +++ b/src/Domain/Models/Entities/Coupon.php @@ -0,0 +1,132 @@ + + */ +final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier +{ + private function __construct( + public CouponId $id, + public CouponName $name, + public bool $status, + public CouponProductName $productName, + public MoneyAmount $amount, + public CouponPicture $picture, + public CouponProduct $product, + public CouponDiffusion $diffusion, + public CouponAvailabilityCalendar $availability, + public CouponValidity $validity, + public CouponLegalInformation $legal, + public GS1 $gtin, + public ProcessingCode $code, + public BrandCode $brandCode, + public ?\DateTimeInterface $updatedAt = null, + ) { + } + + public function id(): CouponId + { + return $this->id; + } + + public function createdAt(): \DateTimeInterface + { + return $this->id->toDateTime(); + } + + public function updatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + public static function create( + CouponName $name, + bool $status, + CouponProductName $productName, + MoneyAmount $amount, + CouponPicture $picture, + CouponProduct $product, + CouponDiffusion $diffusion, + CouponAvailabilityCalendar $availability, + CouponValidity $validity, + CouponLegalInformation $legal, + GS1 $gtin, + ProcessingCode $code, + BrandCode $brandCode, + ?\DateTimeInterface $updatedAt = null, + ): Coupon { + return new self( + id: CouponId::generate(), + name: $name, + status: $status, + productName: $productName, + amount: $amount, + picture: $picture, + product: $product, + diffusion: $diffusion, + availability: $availability, + validity: $validity, + legal: $legal, + gtin: $gtin, + code: $code, + brandCode: $brandCode, + updatedAt: $updatedAt, + ); + } + + public static function hydrate( + CouponId $id, + CouponName $name, + bool $status, + CouponProductName $productName, + MoneyAmount $amount, + CouponPicture $picture, + CouponProduct $product, + CouponDiffusion $diffusion, + CouponAvailabilityCalendar $availability, + CouponValidity $validity, + CouponLegalInformation $legal, + GS1 $gtin, + ProcessingCode $code, + BrandCode $brandCode, + ?\DateTimeInterface $updatedAt = null, + ): self { + return new self( + id: $id, + name: $name, + status: $status, + productName: $productName, + amount: $amount, + picture: $picture, + product: $product, + diffusion: $diffusion, + availability: $availability, + validity: $validity, + legal: $legal, + gtin: $gtin, + code: $code, + brandCode: $brandCode, + updatedAt: $updatedAt, + ); + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php b/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php new file mode 100644 index 0000000..9dd9395 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php @@ -0,0 +1,48 @@ +periods === []) { + throw new \DomainException('Diffusion requires at least one period.'); + } + } + + public function isActiveAt(DateTime $in): bool + { + foreach ($this->periods as $period) { + if ($in >= $period->start && $in <= $period->end) { + return true; + } + } + + return false; + } + + public function beginsAt(): DateTime + { + return min(array_map(fn (Period $period) => $period->start, $this->periods)); + } + + public function endsAt(): DateTime + { + return max(array_map(fn (Period $period) => $period->end, $this->periods)); + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self && $this->periods == $other->periods; + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php b/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php new file mode 100644 index 0000000..1a0e4e0 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php @@ -0,0 +1,19 @@ +periods === []) { + throw new \DomainException('Diffusion requires at least one period.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponId.php b/src/Domain/Models/ValueObjects/Coupon/CouponId.php new file mode 100644 index 0000000..f8b9734 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponId.php @@ -0,0 +1,16 @@ +legalText); + + if ($trimmed === '') { + throw new \DomainException('Legal text cannot be empty.'); + } + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self && $this->legalText === $other->legalText && $this->forLocale->equals($other->forLocale); + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php new file mode 100644 index 0000000..5df5bfd --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php @@ -0,0 +1,20 @@ +name); + + if ($trimmed === '') { + throw new \DomainException('Product Info name cannot be empty.'); + } + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self + && $this->name === $other->name + && $this->range === $other->range + && $this->validityText === $other->validityText + && $this->forLocale->equals($other->forLocale); + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponValidity.php b/src/Domain/Models/ValueObjects/Coupon/CouponValidity.php new file mode 100644 index 0000000..a5304c9 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponValidity.php @@ -0,0 +1,26 @@ +days <= 0) { + throw new \DomainException('Coupon Validity days must be positive.'); + } + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self && $this->days === $other->days && $this->until == $other->until; + } +} diff --git a/src/Domain/Models/ValueObjects/CouponName.php b/src/Domain/Models/ValueObjects/CouponName.php new file mode 100644 index 0000000..764da54 --- /dev/null +++ b/src/Domain/Models/ValueObjects/CouponName.php @@ -0,0 +1,18 @@ +product->equals($other->product) && $this->thumbnail->equals($other->thumbnail); + } +} diff --git a/src/Domain/Models/ValueObjects/CouponProductName.php b/src/Domain/Models/ValueObjects/CouponProductName.php new file mode 100644 index 0000000..3812226 --- /dev/null +++ b/src/Domain/Models/ValueObjects/CouponProductName.php @@ -0,0 +1,18 @@ +value); + + if ($trimmed === '') { + throw new \DomainException('GS1 value cannot be empty.'); + } + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self && $this->value === $other->value && $this->format === $other->format; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Domain/Models/ValueObjects/IncoCode.php b/src/Domain/Models/ValueObjects/IncoCode.php new file mode 100644 index 0000000..e830ad6 --- /dev/null +++ b/src/Domain/Models/ValueObjects/IncoCode.php @@ -0,0 +1,27 @@ +value <= 0) { + throw new \DomainException('Inco Code must be a positive integer.'); + } + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self && $this->value === $other->value; + } +} diff --git a/src/Domain/Models/ValueObjects/ProcessingCode.php b/src/Domain/Models/ValueObjects/ProcessingCode.php new file mode 100644 index 0000000..8a55717 --- /dev/null +++ b/src/Domain/Models/ValueObjects/ProcessingCode.php @@ -0,0 +1,31 @@ +value); + + if ($trimmed === '') { + throw new \DomainException('GS1 value cannot be empty.'); + } + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self && $this->value === $other->value; + } + + public function __toString(): string + { + return $this->value; + } +} From dbcaa60dcedba616adb4561f4982812a54bc039b Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 22 Sep 2026 12:45:08 +0200 Subject: [PATCH 05/52] feat: :sparkles: add unit tests for Coupon entity and associated value objects --- .../ValueObjects/Coupon/CouponProduct.php | 5 + .../ValueObjects/Coupon/CouponProductInfo.php | 16 +- .../Models/ValueObjects/ProcessingCode.php | 2 +- tests/Unit/Domain/Enums/BarcodeFormatTest.php | 81 ++++++ .../Domain/Models/Entities/CouponTest.php | 264 ++++++++++++++++++ .../Coupon/CouponAvailabilityCalendarTest.php | 30 ++ .../Coupon/CouponDiffusionTest.php | 30 ++ .../ValueObjects/Coupon/CouponIdTest.php | 28 ++ .../Coupon/CouponLegalInformationTest.php | 34 +++ .../Coupon/CouponProductInfoTest.php | 40 +++ .../ValueObjects/Coupon/CouponProductTest.php | 36 +++ .../Coupon/CouponValidityTest.php | 34 +++ .../Models/ValueObjects/CouponNameTest.php | 30 ++ .../ValueObjects/CouponProductNameTest.php | 30 ++ .../Domain/Models/ValueObjects/GS1Test.php | 34 +++ .../Models/ValueObjects/IncoCodeTest.php | 30 ++ .../ValueObjects/ProcessingCodeTest.php | 30 ++ 17 files changed, 751 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Domain/Enums/BarcodeFormatTest.php create mode 100644 tests/Unit/Domain/Models/Entities/CouponTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIdTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/CouponNameTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/CouponProductNameTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/GS1Test.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/ProcessingCodeTest.php 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); + }); +}); From b4d45ec716ca43047793fee1df24cd5733979d36 Mon Sep 17 00:00:00 2001 From: K Muller Date: Tue, 22 Sep 2026 13:01:30 +0200 Subject: [PATCH 06/52] feat: :hammer: on the campaign entity/type --- src/Domain/Enums/Campaign/CampaignStatus.php | 28 ++++++ src/Domain/Enums/Campaign/CampaignType.php | 26 +++++ .../Models/Entities/CampaignRequest.php | 73 ++++++++++++++ .../Models/Entities/RequestedCoupon.php | 70 ++++++++++++++ .../CampaignRequest/Applicant.php | 36 +++++++ .../CampaignRequest/CampaignRequestId.php | 16 ++++ src/Domain/Models/ValueObjects/PrintRules.php | 14 +++ .../RequestedCoupon/RequestedCouponId.php | 16 ++++ .../Enums/Campaign/CampaignStatusTest.php | 81 ++++++++++++++++ .../Enums/Campaign/CampaignTypeTest.php | 77 +++++++++++++++ .../CampaignRequest/ApplicantTest.php | 94 +++++++++++++++++++ .../CampaignRequest/CampaignRequestIdTest.php | 28 ++++++ .../Models/ValueObjects/PrintRulesTest.php | 48 ++++++++++ .../RequestedCoupon/RequestedCouponIdTest.php | 28 ++++++ 14 files changed, 635 insertions(+) create mode 100644 src/Domain/Enums/Campaign/CampaignStatus.php create mode 100644 src/Domain/Enums/Campaign/CampaignType.php create mode 100644 src/Domain/Models/Entities/CampaignRequest.php create mode 100644 src/Domain/Models/Entities/RequestedCoupon.php create mode 100644 src/Domain/Models/ValueObjects/CampaignRequest/Applicant.php create mode 100644 src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestId.php create mode 100644 src/Domain/Models/ValueObjects/PrintRules.php create mode 100644 src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponId.php create mode 100644 tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php create mode 100644 tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/CampaignRequest/ApplicantTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestIdTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponIdTest.php diff --git a/src/Domain/Enums/Campaign/CampaignStatus.php b/src/Domain/Enums/Campaign/CampaignStatus.php new file mode 100644 index 0000000..96142de --- /dev/null +++ b/src/Domain/Enums/Campaign/CampaignStatus.php @@ -0,0 +1,28 @@ + */ + use BaseEnum; + + case CREATED = 1; + case TRANSFORMED = 2; + case REJECTED = 3; + + /** + * Optional labels definition + * + * @phpstan-ignore-next-line + */ + private const LABELS = [ + self::CREATED->name => 'Crée', + self::TRANSFORMED->name => 'Transformé', + self::REJECTED->name => 'Rejeté' + ]; +} diff --git a/src/Domain/Enums/Campaign/CampaignType.php b/src/Domain/Enums/Campaign/CampaignType.php new file mode 100644 index 0000000..5728cb3 --- /dev/null +++ b/src/Domain/Enums/Campaign/CampaignType.php @@ -0,0 +1,26 @@ + */ + use BaseEnum; + + case CLUB = 1; + case GAME = 2; + + /** + * Optional labels definition + * + * @phpstan-ignore-next-line + */ + private const LABELS = [ + self::CLUB->name => 'CLUB', + self::GAME->name => 'GAME', + ]; +} diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php new file mode 100644 index 0000000..6512dff --- /dev/null +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -0,0 +1,73 @@ + + */ +final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdentifier +{ + private function __construct( + public CampaignRequestId $id, + public BrandCode $code, + public Applicant $applicant, + public Country $country, + public CampaignType $campaignType, + public ?PrintRules $printRules, + public RequestedCoupon $requestedCoupon, + public CampaignStatus $status, + public ?\DateTimeInterface $updatedAt = null, + ) {} + + public function id(): CampaignRequestId + { + return $this->id; + } + + public function createdAt(): \DateTimeInterface + { + return $this->id->toDateTime(); + } + + public function updatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + + public static function hydrate( + CampaignRequestId $id, + BrandCode $code, + Applicant $applicant, + Country $country, + CampaignType $campaignType, + ?PrintRules $printRules, + RequestedCoupon $requestedCoupon, + CampaignStatus $status, + ?\DateTimeInterface $updatedAt = null, + ): self { + return new self( + id: $id, + code: $code, + applicant: $applicant, + country: $country, + campaignType: $campaignType, + printRules: $printRules, + requestedCoupon: $requestedCoupon, + status: $status, + updatedAt: $updatedAt, + ); + } +} diff --git a/src/Domain/Models/Entities/RequestedCoupon.php b/src/Domain/Models/Entities/RequestedCoupon.php new file mode 100644 index 0000000..5d2beb0 --- /dev/null +++ b/src/Domain/Models/Entities/RequestedCoupon.php @@ -0,0 +1,70 @@ + + */ +final readonly class RequestedCoupon implements AggregateRoot, HasTimestampIdentifier +{ + private function __construct( + public RequestedCouponId $id, + public string $name, + public string $productName, + public int $quantity, + public int $warningThresholdQuantity, + public Period $period, + public MoneyAmount $moneyAmount, + + // FROM COUPONS + public $processingCode, + public $couponPicture, + public $gs1, + public $validity, + public ?\DateTimeInterface $updatedAt = null, + ) { + } + + public function id(): RequestedCouponId + { + return $this->id; + } + + public function createdAt(): \DateTimeInterface + { + return $this->id->toDateTime(); + } + + public function updatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + public static function hydrate( + RequestedCouponId $id, + string $name, + string $productName, + int $quantity, + int $warningThresholdQuantity, + Period $period, + ?\DateTimeInterface $updatedAt = null, + ): self { + return new self( + id: $id, + name: $name, + productName: $productName, + quantity: $quantity, + warningThresholdQuantity: $warningThresholdQuantity, + period: $period, + updatedAt: $updatedAt, + ); + } +} diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/Applicant.php b/src/Domain/Models/ValueObjects/CampaignRequest/Applicant.php new file mode 100644 index 0000000..7375c56 --- /dev/null +++ b/src/Domain/Models/ValueObjects/CampaignRequest/Applicant.php @@ -0,0 +1,36 @@ + 200) { + throw new \DomainException('Applicant firstname exceeds the maximum allowed length.'); + } + + $trimmedLastname = trim($lastname); + + if ($trimmedLastname === '') { + throw new \DomainException('Applicant lastname cannot be empty.'); + } + + if (mb_strlen($trimmedLastname) > 200) { + throw new \DomainException('Applicant lastname exceeds the maximum allowed length.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestId.php b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestId.php new file mode 100644 index 0000000..459d131 --- /dev/null +++ b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestId.php @@ -0,0 +1,16 @@ +value())->toBe(1) + ->and(CampaignStatus::TRANSFORMED->value())->toBe(2) + ->and(CampaignStatus::REJECTED->value())->toBe(3); + }); + + it('exposes its case name', function (): void { + // THEN + expect(CampaignStatus::CREATED->name())->toBe('CREATED'); + }); + + it('returns the defined label', function (): void { + // THEN + expect(CampaignStatus::CREATED->label())->toBe('Crée') + ->and(CampaignStatus::TRANSFORMED->label())->toBe('Transformé') + ->and(CampaignStatus::REJECTED->label())->toBe('Rejeté'); + }); + + it('lists all labels keyed by case name', function (): void { + // THEN + expect(CampaignStatus::toLabels())->toBe([ + 'CREATED' => 'Crée', + 'TRANSFORMED' => 'Transformé', + 'REJECTED' => 'Rejeté', + ]); + }); + + it('lists all values keyed by case name', function (): void { + // THEN + expect(CampaignStatus::toValues())->toBe([ + 'CREATED' => 1, + 'TRANSFORMED' => 2, + 'REJECTED' => 3, + ]); + }); + + it('makes an instance from its backing value', function (): void { + // THEN + expect(CampaignStatus::make(1))->toBe(CampaignStatus::CREATED); + }); + + it('makes an instance from a label', function (): void { + // THEN + expect(CampaignStatus::make('Transformé', 'label'))->toBe(CampaignStatus::TRANSFORMED); + }); + + it('returns the same instance when already an enum case', function (): void { + // THEN + expect(CampaignStatus::make(CampaignStatus::REJECTED))->toBe(CampaignStatus::REJECTED); + }); + + it('returns null for an unknown value', function (): void { + // THEN + expect(CampaignStatus::make(99))->toBeNull(); + }); + + it('implements equals correctly', function (): void { + // THEN + expect(CampaignStatus::CREATED->equals(CampaignStatus::CREATED))->toBeTrue() + ->and(CampaignStatus::CREATED->equals(CampaignStatus::REJECTED))->toBeFalse() + ->and(CampaignStatus::CREATED->equals(null))->toBeFalse(); + }); + + it('serializes to json as its backing value', function (): void { + // THEN + expect(CampaignStatus::CREATED->jsonSerialize())->toBe(1) + ->and(json_encode(CampaignStatus::CREATED))->toBe('1'); + }); +}); diff --git a/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php b/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php new file mode 100644 index 0000000..2504339 --- /dev/null +++ b/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php @@ -0,0 +1,77 @@ +value())->toBe(1) + ->and(CampaignType::GAME->value())->toBe(2); + }); + + it('exposes its case name', function (): void { + // THEN + expect(CampaignType::CLUB->name())->toBe('CLUB'); + }); + + it('returns the defined label', function (): void { + // THEN + expect(CampaignType::CLUB->label())->toBe('CLUB') + ->and(CampaignType::GAME->label())->toBe('GAME'); + }); + + it('lists all labels keyed by case name', function (): void { + // THEN + expect(CampaignType::toLabels())->toBe([ + 'CLUB' => 'CLUB', + 'GAME' => 'GAME', + ]); + }); + + it('lists all values keyed by case name', function (): void { + // THEN + expect(CampaignType::toValues())->toBe([ + 'CLUB' => 1, + 'GAME' => 2, + ]); + }); + + it('makes an instance from its backing value', function (): void { + // THEN + expect(CampaignType::make(1))->toBe(CampaignType::CLUB); + }); + + it('makes an instance from a label', function (): void { + // THEN + expect(CampaignType::make('GAME', 'label'))->toBe(CampaignType::GAME); + }); + + it('returns the same instance when already an enum case', function (): void { + // THEN + expect(CampaignType::make(CampaignType::CLUB))->toBe(CampaignType::CLUB); + }); + + it('returns null for an unknown value', function (): void { + // THEN + expect(CampaignType::make(99))->toBeNull(); + }); + + it('implements equals correctly', function (): void { + // THEN + expect(CampaignType::CLUB->equals(CampaignType::CLUB))->toBeTrue() + ->and(CampaignType::CLUB->equals(CampaignType::GAME))->toBeFalse() + ->and(CampaignType::CLUB->equals(null))->toBeFalse(); + }); + + it('serializes to json as its backing value', function (): void { + // THEN + expect(CampaignType::CLUB->jsonSerialize())->toBe(1) + ->and(json_encode(CampaignType::CLUB))->toBe('1'); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/ApplicantTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/ApplicantTest.php new file mode 100644 index 0000000..e46349c --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/ApplicantTest.php @@ -0,0 +1,94 @@ +toBeInstanceOf(Applicant::class) + ->and($applicant->firstname)->toBe($firstname) + ->and($applicant->lastname)->toBe($lastname) + ->and($applicant->email)->toBe($email); + }); + + it('rejects an empty or whitespace-only firstname', function (string $firstname): void { + // WHEN + $action = fn (): Applicant => new Applicant($firstname, 'Doe', new Email('john.doe@example.com')); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Applicant firstname cannot be empty.'); + })->with([ + 'empty' => '', + 'spaces' => ' ', + ]); + + it('rejects a firstname that exceeds the maximum allowed length', function (): void { + // GIVEN + $firstname = str_repeat('a', 201); + + // WHEN + $action = fn (): Applicant => new Applicant($firstname, 'Doe', new Email('john.doe@example.com')); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Applicant firstname exceeds the maximum allowed length.'); + }); + + it('accepts a firstname of exactly the maximum length', function (): void { + // GIVEN + $firstname = str_repeat('a', 200); + + // WHEN + $applicant = new Applicant($firstname, 'Doe', new Email('john.doe@example.com')); + + // THEN + expect(mb_strlen($applicant->firstname))->toBe(200); + }); + + it('rejects an empty or whitespace-only lastname', function (string $lastname): void { + // WHEN + $action = fn (): Applicant => new Applicant('John', $lastname, new Email('john.doe@example.com')); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Applicant lastname cannot be empty.'); + })->with([ + 'empty' => '', + 'spaces' => ' ', + ]); + + it('rejects a lastname that exceeds the maximum allowed length', function (): void { + // GIVEN + $lastname = str_repeat('a', 201); + + // WHEN + $action = fn (): Applicant => new Applicant('John', $lastname, new Email('john.doe@example.com')); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Applicant lastname exceeds the maximum allowed length.'); + }); + + it('accepts a lastname of exactly the maximum length', function (): void { + // GIVEN + $lastname = str_repeat('a', 200); + + // WHEN + $applicant = new Applicant('John', $lastname, new Email('john.doe@example.com')); + + // THEN + expect(mb_strlen($applicant->lastname))->toBe(200); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestIdTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestIdTest.php new file mode 100644 index 0000000..5de8df4 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestIdTest.php @@ -0,0 +1,28 @@ +toBeInstanceOf(CampaignRequestId::class); + }); + + it('generates different ids on each call', function (): void { + // WHEN + $first = CampaignRequestId::generate(); + $second = CampaignRequestId::generate(); + + // THEN + expect((string) $first)->not->toBe((string) $second); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php new file mode 100644 index 0000000..26092f5 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php @@ -0,0 +1,48 @@ +toBeInstanceOf(PrintRules::class) + ->and($printRules->maxPerCustomer)->toBe(3) + ->and($printRules->cooldownDays)->toBe(30); + }); + + it('creates print rules with only maxPerCustomer set', function (): void { + // WHEN + $printRules = new PrintRules(maxPerCustomer: 5); + + // THEN + expect($printRules->maxPerCustomer)->toBe(5) + ->and($printRules->cooldownDays)->toBeNull(); + }); + + it('creates print rules with only cooldownDays set', function (): void { + // WHEN + $printRules = new PrintRules(cooldownDays: 45); + + // THEN + expect($printRules->maxPerCustomer)->toBeNull() + ->and($printRules->cooldownDays)->toBe(45); + }); + + it('creates print rules with both fields null by default', function (): void { + // WHEN + $printRules = new PrintRules(); + + // THEN + expect($printRules->maxPerCustomer)->toBeNull() + ->and($printRules->cooldownDays)->toBeNull(); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponIdTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponIdTest.php new file mode 100644 index 0000000..017f4ac --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponIdTest.php @@ -0,0 +1,28 @@ +toBeInstanceOf(RequestedCouponId::class); + }); + + it('generates different ids on each call', function (): void { + // WHEN + $first = RequestedCouponId::generate(); + $second = RequestedCouponId::generate(); + + // THEN + expect((string) $first)->not->toBe((string) $second); + }); +}); From f71a39547fe13923ed58d338e6f61cd803cf0e46 Mon Sep 17 00:00:00 2001 From: K Muller Date: Tue, 22 Sep 2026 13:55:30 +0200 Subject: [PATCH 07/52] feat: :sparkles: add campaign / coupon entity + vo --- .../Models/Entities/RequestedCoupon.php | 34 ++-- .../Models/Entities/CampaignRequestTest.php | 154 ++++++++++++++++++ .../Models/Entities/RequestedCouponTest.php | 126 ++++++++++++++ 3 files changed, 304 insertions(+), 10 deletions(-) create mode 100644 tests/Unit/Domain/Models/Entities/CampaignRequestTest.php create mode 100644 tests/Unit/Domain/Models/Entities/RequestedCouponTest.php diff --git a/src/Domain/Models/Entities/RequestedCoupon.php b/src/Domain/Models/Entities/RequestedCoupon.php index 5d2beb0..3ef925f 100644 --- a/src/Domain/Models/Entities/RequestedCoupon.php +++ b/src/Domain/Models/Entities/RequestedCoupon.php @@ -4,8 +4,14 @@ declare(strict_types=1); namespace Domain\Models\Entities; +use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\CouponName; +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\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; @@ -17,18 +23,16 @@ final readonly class RequestedCoupon implements AggregateRoot, HasTimestampIdent { private function __construct( public RequestedCouponId $id, - public string $name, - public string $productName, + public CouponName $name, + public CouponProductName $productName, public int $quantity, public int $warningThresholdQuantity, public Period $period, public MoneyAmount $moneyAmount, - - // FROM COUPONS - public $processingCode, - public $couponPicture, - public $gs1, - public $validity, + public ProcessingCode $processingCode, + public CouponPicture $couponPicture, + public GS1 $gs1, + public CouponValidity $couponValidity, public ?\DateTimeInterface $updatedAt = null, ) { } @@ -50,11 +54,16 @@ final readonly class RequestedCoupon implements AggregateRoot, HasTimestampIdent public static function hydrate( RequestedCouponId $id, - string $name, - string $productName, + CouponName $name, + CouponProductName $productName, int $quantity, int $warningThresholdQuantity, Period $period, + MoneyAmount $moneyAmount, + ProcessingCode $processingCode, + CouponPicture $couponPicture, + GS1 $gs1, + CouponValidity $couponValidity, ?\DateTimeInterface $updatedAt = null, ): self { return new self( @@ -64,6 +73,11 @@ final readonly class RequestedCoupon implements AggregateRoot, HasTimestampIdent quantity: $quantity, warningThresholdQuantity: $warningThresholdQuantity, period: $period, + moneyAmount: $moneyAmount, + processingCode: $processingCode, + couponPicture: $couponPicture, + gs1: $gs1, + couponValidity: $couponValidity, updatedAt: $updatedAt, ); } diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php new file mode 100644 index 0000000..64000d9 --- /dev/null +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -0,0 +1,154 @@ +id())->toBe($id) + ->and($campaignRequest->code)->toBe($code) + ->and($campaignRequest->applicant)->toBe($applicant) + ->and($campaignRequest->country)->toBe($country) + ->and($campaignRequest->campaignType)->toBe($campaignType) + ->and($campaignRequest->printRules)->toBe($printRules) + ->and($campaignRequest->requestedCoupon)->toBe($requestedCoupon) + ->and($campaignRequest->status)->toBe($status) + ->and($campaignRequest->updatedAt)->toBeNull() + ->and($campaignRequest->updatedAt())->toBeNull(); + }); + + it('hydrates a campaign request with a null printRules', function (): void { + // WHEN + $campaignRequest = CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + null, + aRequestedCoupon(), + CampaignStatus::CREATED, + ); + + // THEN + expect($campaignRequest->printRules)->toBeNull(); + }); + + it('hydrates a campaign request with an explicit updatedAt date', function (): void { + // GIVEN + $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); + + // WHEN + $campaignRequest = CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + aRequestedCoupon(), + CampaignStatus::CREATED, + $updatedAt, + ); + + // THEN + expect($campaignRequest->updatedAt)->toBe($updatedAt) + ->and($campaignRequest->updatedAt())->toBe($updatedAt); + }); + + 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 CampaignRequestId(Uuid::uuid7($expected)); + $campaignRequest = CampaignRequest::hydrate( + $id, + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + aRequestedCoupon(), + CampaignStatus::CREATED, + ); + + // WHEN + $result = $campaignRequest->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/Entities/RequestedCouponTest.php b/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php new file mode 100644 index 0000000..b69de01 --- /dev/null +++ b/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php @@ -0,0 +1,126 @@ +id())->toBe($id) + ->and($coupon->name)->toBe($name) + ->and($coupon->productName)->toBe($productName) + ->and($coupon->quantity)->toBe($quantity) + ->and($coupon->warningThresholdQuantity)->toBe($warningThresholdQuantity) + ->and($coupon->period)->toBe($period) + ->and($coupon->moneyAmount)->toBe($moneyAmount) + ->and($coupon->processingCode)->toBe($processingCode) + ->and($coupon->couponPicture)->toBe($couponPicture) + ->and($coupon->gs1)->toBe($gs1) + ->and($coupon->couponValidity)->toBe($couponValidity) + ->and($coupon->updatedAt)->toBeNull() + ->and($coupon->updatedAt())->toBeNull(); + }); + + it('hydrates a requested coupon with an explicit updatedAt date', function (): void { + // GIVEN + $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); + + // WHEN + $coupon = RequestedCoupon::hydrate( + RequestedCouponId::generate(), + new CouponName('Coupon A'), + new CouponProductName('Product A'), + 100, + 10, + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + new MoneyAmount(new Money(Currency::EUR, 300)), + new ProcessingCode('PC1'), + new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new GS1('01234567890128', BarcodeFormat::EAN13), + new CouponValidity(30, DateTime::now()), + $updatedAt, + ); + + // THEN + expect($coupon->updatedAt)->toBe($updatedAt) + ->and($coupon->updatedAt())->toBe($updatedAt); + }); + + 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 RequestedCouponId(Uuid::uuid7($expected)); + $coupon = RequestedCoupon::hydrate( + $id, + new CouponName('Coupon A'), + new CouponProductName('Product A'), + 100, + 10, + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + new MoneyAmount(new Money(Currency::EUR, 300)), + new ProcessingCode('PC1'), + new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new GS1('01234567890128', BarcodeFormat::EAN13), + new CouponValidity(30, DateTime::now()), + ); + + // 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')); + }); +}); From 00a33f1b7ec02763db6d3f689ca50e35a89f7e2e Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 22 Sep 2026 14:08:50 +0200 Subject: [PATCH 08/52] feat: :sparkles: create campaign request repository --- .../Ports/Repositories/CampaignRequestRepository.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/Domain/Ports/Repositories/CampaignRequestRepository.php diff --git a/src/Domain/Ports/Repositories/CampaignRequestRepository.php b/src/Domain/Ports/Repositories/CampaignRequestRepository.php new file mode 100644 index 0000000..638b460 --- /dev/null +++ b/src/Domain/Ports/Repositories/CampaignRequestRepository.php @@ -0,0 +1,12 @@ + + */ +interface CampaignRequestRepository extends Repository {} From 269ff25f0ffaf685478e1a50cb8b8432fd0926c8 Mon Sep 17 00:00:00 2001 From: K Muller Date: Tue, 22 Sep 2026 16:29:34 +0200 Subject: [PATCH 09/52] fix: :bug: add missing rejection field in campaignRequest --- .../Models/Entities/CampaignRequest.php | 14 +++- .../CampaignRequest/RejectionReason.php | 18 +++++ .../Models/Entities/CampaignRequestTest.php | 70 +++++++++++++++++-- .../CampaignRequest/RejectionReasonTest.php | 34 +++++++++ 4 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/CampaignRequest/RejectionReasonTest.php diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index 6512dff..127885b 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -10,6 +10,7 @@ use Domain\Enums\Country; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\Applicant; +use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Models\ValueObjects\PrintRules; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; @@ -28,8 +29,17 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent public ?PrintRules $printRules, public RequestedCoupon $requestedCoupon, public CampaignStatus $status, + public ?RejectionReason $rejectionReason = null, public ?\DateTimeInterface $updatedAt = null, - ) {} + ) { + if ($this->status === CampaignStatus::REJECTED && ! $this->rejectionReason instanceof RejectionReason) { + throw new \DomainException('A rejection reason is required when the campaign request is rejected.'); + } + + if ($this->status !== CampaignStatus::REJECTED && $this->rejectionReason instanceof RejectionReason) { + throw new \DomainException('A rejection reason can only be set when the campaign request is rejected.'); + } + } public function id(): CampaignRequestId { @@ -56,6 +66,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent ?PrintRules $printRules, RequestedCoupon $requestedCoupon, CampaignStatus $status, + ?RejectionReason $rejectionReason = null, ?\DateTimeInterface $updatedAt = null, ): self { return new self( @@ -67,6 +78,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent printRules: $printRules, requestedCoupon: $requestedCoupon, status: $status, + rejectionReason: $rejectionReason, updatedAt: $updatedAt, ); } diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php b/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php new file mode 100644 index 0000000..73e4658 --- /dev/null +++ b/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php @@ -0,0 +1,18 @@ +and($campaignRequest->updatedAt())->toBe($updatedAt); }); + it('hydrates a rejected campaign request with a rejection reason', function (): void { + // GIVEN + $rejectionReason = new RejectionReason('Missing legal documents.'); + + // WHEN + $campaignRequest = CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + aRequestedCoupon(), + CampaignStatus::REJECTED, + $rejectionReason, + ); + + // THEN + expect($campaignRequest->status)->toBe(CampaignStatus::REJECTED) + ->and($campaignRequest->rejectionReason)->toBe($rejectionReason); + }); + + it('rejects a rejected campaign request without a rejection reason', function (): void { + // WHEN + $action = fn (): CampaignRequest => CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + aRequestedCoupon(), + CampaignStatus::REJECTED, + ); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'A rejection reason is required when the campaign request is rejected.' + ); + }); + + it('rejects a non-rejected campaign request with a rejection reason', function (): void { + // WHEN + $action = fn (): CampaignRequest => CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + aRequestedCoupon(), + CampaignStatus::CREATED, + new RejectionReason('Missing legal documents.'), + ); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'A rejection reason can only be set when the campaign request is rejected.' + ); + }); + 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')); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/RejectionReasonTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/RejectionReasonTest.php new file mode 100644 index 0000000..817d894 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/RejectionReasonTest.php @@ -0,0 +1,34 @@ +toBeInstanceOf(RejectionReason::class) + ->and($reason->value)->toBe($value); + }); + + it('rejects an empty or whitespace-only reason', function (string $value): void { + // WHEN + $action = fn (): RejectionReason => new RejectionReason($value); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Rejection reason cannot be empty.'); + })->with([ + 'empty' => '', + 'spaces' => ' ', + ]); +}); From 46ae192d88d0867a0805e96e79140f9fe57c799c Mon Sep 17 00:00:00 2001 From: K Muller Date: Wed, 23 Sep 2026 09:24:18 +0200 Subject: [PATCH 10/52] fix: :rotating_light: linters --- .../Models/Entities/CampaignRequest.php | 36 +++++++------- .../Models/Entities/RequestedCoupon.php | 2 +- .../ValueObjects/Coupon/CouponProduct.php | 1 - .../ValueObjects/Coupon/CouponProductInfo.php | 4 +- .../Models/ValueObjects/CouponPicture.php | 1 - .../CampaignRequestRepository.php | 4 +- src/Shared/Enums/BaseEnum.php | 31 +----------- tests/Unit/Domain/Enums/BarcodeFormatTest.php | 4 +- .../Models/Entities/CampaignRequestTest.php | 47 +++++++++---------- .../Domain/Models/Entities/CouponTest.php | 6 +-- .../Coupon/CouponProductInfoTest.php | 12 ++--- .../ValueObjects/Coupon/CouponProductTest.php | 8 ++-- .../Coupon/CouponValidityTest.php | 4 +- .../Domain/Models/ValueObjects/GS1Test.php | 4 +- .../Unit/Shared/ValueObjects/DateTimeTest.php | 5 +- 15 files changed, 71 insertions(+), 98 deletions(-) diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index 127885b..a007125 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -21,15 +21,15 @@ use Shared\DDD\Models\HasTimestampIdentifier; final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdentifier { private function __construct( - public CampaignRequestId $id, - public BrandCode $code, - public Applicant $applicant, - public Country $country, - public CampaignType $campaignType, - public ?PrintRules $printRules, - public RequestedCoupon $requestedCoupon, - public CampaignStatus $status, - public ?RejectionReason $rejectionReason = null, + public CampaignRequestId $id, + public BrandCode $code, + public Applicant $applicant, + public Country $country, + public CampaignType $campaignType, + public ?PrintRules $printRules, + public RequestedCoupon $requestedCoupon, + public CampaignStatus $status, + public ?RejectionReason $rejectionReason = null, public ?\DateTimeInterface $updatedAt = null, ) { if ($this->status === CampaignStatus::REJECTED && ! $this->rejectionReason instanceof RejectionReason) { @@ -58,15 +58,15 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent public static function hydrate( - CampaignRequestId $id, - BrandCode $code, - Applicant $applicant, - Country $country, - CampaignType $campaignType, - ?PrintRules $printRules, - RequestedCoupon $requestedCoupon, - CampaignStatus $status, - ?RejectionReason $rejectionReason = null, + CampaignRequestId $id, + BrandCode $code, + Applicant $applicant, + Country $country, + CampaignType $campaignType, + ?PrintRules $printRules, + RequestedCoupon $requestedCoupon, + CampaignStatus $status, + ?RejectionReason $rejectionReason = null, ?\DateTimeInterface $updatedAt = null, ): self { return new self( diff --git a/src/Domain/Models/Entities/RequestedCoupon.php b/src/Domain/Models/Entities/RequestedCoupon.php index 3ef925f..3ea6715 100644 --- a/src/Domain/Models/Entities/RequestedCoupon.php +++ b/src/Domain/Models/Entities/RequestedCoupon.php @@ -42,7 +42,7 @@ final readonly class RequestedCoupon implements AggregateRoot, HasTimestampIdent return $this->id; } - public function createdAt(): \DateTimeInterface + public function createdAt(): \DateTimeInterface { return $this->id->toDateTime(); } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php index f270627..d0bcdaf 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Coupon; -use Domain\Models\ValueObjects\Coupon\CouponProductInfo; use Domain\Models\ValueObjects\IncoCode; final readonly class CouponProduct diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php index f52ed95..97928fc 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php @@ -38,8 +38,8 @@ final readonly class CouponProductInfo implements Equatable public function equals(mixed $other): bool { return $other instanceof self - && $this->name === $other->name - && $this->range === $other->range + && $this->name === $other->name + && $this->range === $other->range && $this->validityText === $other->validityText && $this->forLocale->equals($other->forLocale); } diff --git a/src/Domain/Models/ValueObjects/CouponPicture.php b/src/Domain/Models/ValueObjects/CouponPicture.php index f59ad6d..ec29916 100644 --- a/src/Domain/Models/ValueObjects/CouponPicture.php +++ b/src/Domain/Models/ValueObjects/CouponPicture.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects; -use Domain\Models\ValueObjects\PicturePath; use Shared\DDD\Models\Equatable; final readonly class CouponPicture implements Equatable diff --git a/src/Domain/Ports/Repositories/CampaignRequestRepository.php b/src/Domain/Ports/Repositories/CampaignRequestRepository.php index 638b460..7ba64c2 100644 --- a/src/Domain/Ports/Repositories/CampaignRequestRepository.php +++ b/src/Domain/Ports/Repositories/CampaignRequestRepository.php @@ -9,4 +9,6 @@ use Shared\DDD\Ports\Repository; /** * @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\Brand\BrandId, \Domain\Models\Entities\Brand> */ -interface CampaignRequestRepository extends Repository {} +interface CampaignRequestRepository extends Repository +{ +} diff --git a/src/Shared/Enums/BaseEnum.php b/src/Shared/Enums/BaseEnum.php index e524e25..cb4bee0 100644 --- a/src/Shared/Enums/BaseEnum.php +++ b/src/Shared/Enums/BaseEnum.php @@ -36,6 +36,7 @@ trait BaseEnum */ public static function toLabels(): array { + /** @var array|null */ static $cache = null; if ($cache !== null) { @@ -62,6 +63,7 @@ trait BaseEnum */ public static function toValues(): array { + /** @var array|null */ static $cache = null; if ($cache !== null) { @@ -128,33 +130,4 @@ trait BaseEnum { return $this->value; } - - // * PRIVATE - - /** - * @return array> - */ - private static function getCustomDefinitions(): array - { - static $cache = null; - - if ($cache !== null) { - return $cache; - } - - $names = array_column(self::cases(), 'name', 'name'); - $filteredConstants = []; - - foreach ((new \ReflectionClass(self::class))->getConstants() as $key => $constant) { - if ($key === self::LABELS_CONSTANT_NAME || ! \is_array($constant)) { - continue; - } - - if (! empty(array_intersect_key($constant, $names))) { - $filteredConstants[$key] = $constant; - } - } - - return $cache = $filteredConstants; - } } diff --git a/tests/Unit/Domain/Enums/BarcodeFormatTest.php b/tests/Unit/Domain/Enums/BarcodeFormatTest.php index 80a157b..b9c9882 100644 --- a/tests/Unit/Domain/Enums/BarcodeFormatTest.php +++ b/tests/Unit/Domain/Enums/BarcodeFormatTest.php @@ -31,7 +31,7 @@ describe('barcode format', function (): void { it('lists all labels keyed by case name', function (): void { // THEN expect(BarcodeFormat::toLabels())->toBe([ - 'EAN13' => 'EAN', + 'EAN13' => 'EAN', 'CODE128' => 'CODE 128', 'GS1_128' => 'GS1_128', ]); @@ -40,7 +40,7 @@ describe('barcode format', function (): void { it('lists all values keyed by case name', function (): void { // THEN expect(BarcodeFormat::toValues())->toBe([ - 'EAN13' => 1, + 'EAN13' => 1, 'CODE128' => 2, 'GS1_128' => 3, ]); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 35f7af7..07d213d 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -33,24 +33,23 @@ use Shared\ValueObjects\Money; covers(CampaignRequest::class); -function aRequestedCoupon(): RequestedCoupon -{ - return RequestedCoupon::hydrate( - RequestedCouponId::generate(), - new CouponName('Coupon A'), - new CouponProductName('Product A'), - 100, - 10, - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC1'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), - new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), - ); -} - describe('campaign request', function (): void { + beforeEach(function (): void { + $this->requestedCoupon = RequestedCoupon::hydrate( + RequestedCouponId::generate(), + new CouponName('Coupon A'), + new CouponProductName('Product A'), + 100, + 10, + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + new MoneyAmount(new Money(Currency::EUR, 300)), + new ProcessingCode('PC1'), + new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new GS1('01234567890128', BarcodeFormat::EAN13), + new CouponValidity(30, DateTime::now()), + ); + }); + it('hydrates a campaign request with all its fields', function (): void { // GIVEN $id = CampaignRequestId::generate(); @@ -59,7 +58,7 @@ describe('campaign request', function (): void { $country = Country::FR; $campaignType = CampaignType::CLUB; $printRules = new PrintRules(maxPerCustomer: 3); - $requestedCoupon = aRequestedCoupon(); + $requestedCoupon = $this->requestedCoupon; $status = CampaignStatus::CREATED; // WHEN @@ -96,7 +95,7 @@ describe('campaign request', function (): void { Country::FR, CampaignType::CLUB, null, - aRequestedCoupon(), + $this->requestedCoupon, CampaignStatus::CREATED, ); @@ -116,7 +115,7 @@ describe('campaign request', function (): void { Country::FR, CampaignType::CLUB, new PrintRules(maxPerCustomer: 3), - aRequestedCoupon(), + $this->requestedCoupon, CampaignStatus::CREATED, null, $updatedAt, @@ -139,7 +138,7 @@ describe('campaign request', function (): void { Country::FR, CampaignType::CLUB, new PrintRules(maxPerCustomer: 3), - aRequestedCoupon(), + $this->requestedCoupon, CampaignStatus::REJECTED, $rejectionReason, ); @@ -158,7 +157,7 @@ describe('campaign request', function (): void { Country::FR, CampaignType::CLUB, new PrintRules(maxPerCustomer: 3), - aRequestedCoupon(), + $this->requestedCoupon, CampaignStatus::REJECTED, ); @@ -178,7 +177,7 @@ describe('campaign request', function (): void { Country::FR, CampaignType::CLUB, new PrintRules(maxPerCustomer: 3), - aRequestedCoupon(), + $this->requestedCoupon, CampaignStatus::CREATED, new RejectionReason('Missing legal documents.'), ); @@ -201,7 +200,7 @@ describe('campaign request', function (): void { Country::FR, CampaignType::CLUB, new PrintRules(maxPerCustomer: 3), - aRequestedCoupon(), + $this->requestedCoupon, CampaignStatus::CREATED, ); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index 6b06246..ca5be82 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -104,7 +104,7 @@ describe('coupon', function (): void { $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')); + $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); // WHEN $coupon = Coupon::hydrate( @@ -236,8 +236,8 @@ describe('coupon', function (): void { 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)); + $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'), diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php index 473545f..bef8718 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php @@ -12,10 +12,10 @@ covers(CouponProductInfo::class); describe('coupon product info', function (): void { it('creates a valid coupon product info', function (): void { // GIVEN - $name = 'Lorem ipsum'; - $range = 'Lorem'; + $name = 'Lorem ipsum'; + $range = 'Lorem'; $validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; - $forLocale = Country::FR; + $forLocale = Country::FR; // WHEN $couponProductInfo = new CouponProductInfo($name, $range, $validityText, $forLocale); @@ -29,10 +29,10 @@ describe('coupon product info', function (): void { it('rejects invalid coupon product info', function (): void { // GIVEN - $name = ''; - $range = ''; + $name = ''; + $range = ''; $validityText = ''; - $forLocale = Country::FR; + $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 index 9f593a6..16e2459 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductTest.php @@ -11,9 +11,9 @@ covers(CouponProduct::class); describe('coupon product', function (): void { it('creates a valid coupon product', function (): void { // GIVEN - $name = 'Lorem'; + $name = 'Lorem'; $incoCodes = []; - $labels = []; + $labels = []; // WHEN $couponProduct = new CouponProduct($name, $incoCodes, $labels); @@ -26,9 +26,9 @@ describe('coupon product', function (): void { it('rejects invalid coupon product', function (): void { // GIVEN - $name = ''; + $name = ''; $incoCodes = []; - $labels = []; + $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 index 005138e..a4bb9e1 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php @@ -12,7 +12,7 @@ covers(CouponValidity::class); describe('coupon validity', function (): void { it('creates a valid coupon validity', function (): void { // GIVEN - $days = 30; + $days = 30; $until = new DateTime('2026-11-30'); // WHEN @@ -25,7 +25,7 @@ describe('coupon validity', function (): void { it('rejects invalid coupon validity', function (): void { // GIVEN - $days = 0; + $days = 0; $until = new DateTime('2026-11-30'); // GIVEN / THEN diff --git a/tests/Unit/Domain/Models/ValueObjects/GS1Test.php b/tests/Unit/Domain/Models/ValueObjects/GS1Test.php index f6cf85c..84e8d16 100644 --- a/tests/Unit/Domain/Models/ValueObjects/GS1Test.php +++ b/tests/Unit/Domain/Models/ValueObjects/GS1Test.php @@ -12,7 +12,7 @@ covers(GS1::class); describe('gs1', function (): void { it('creates a valid gs1', function (): void { // GIVEN - $value = '1010100101'; + $value = '1010100101'; $format = BarcodeFormat::EAN13; // WHEN @@ -25,7 +25,7 @@ describe('gs1', function (): void { it('rejects invalid gs1', function (): void { // GIVEN - $value = ''; + $value = ''; $format = BarcodeFormat::EAN13; // GIVEN / THEN diff --git a/tests/Unit/Shared/ValueObjects/DateTimeTest.php b/tests/Unit/Shared/ValueObjects/DateTimeTest.php index d028cdd..d7f081c 100644 --- a/tests/Unit/Shared/ValueObjects/DateTimeTest.php +++ b/tests/Unit/Shared/ValueObjects/DateTimeTest.php @@ -7,6 +7,7 @@ namespace Tests\Unit\Shared\ValueObjects; use Shared\Helpers\DatesHelper; use Shared\Helpers\Exceptions\InvalidDateFormatException; use Shared\ValueObjects\DateTime; +use Carbon\CarbonImmutable; covers(DateTime::class); @@ -42,7 +43,7 @@ describe('date time', function (): void { it('creates the current instant via now', function (): void { // GIVEN - \Carbon\CarbonImmutable::setTestNow('2026-06-15T12:00:00Z'); + CarbonImmutable::setTestNow('2026-06-15T12:00:00Z'); // WHEN $dateTime = DateTime::now(); @@ -53,7 +54,7 @@ describe('date time', function (): void { ->and($dateTime->value->format(\DateTimeInterface::ATOM))->toBe('2026-06-15T12:00:00+00:00'); // TEARDOWN - \Carbon\CarbonImmutable::setTestNow(); + CarbonImmutable::setTestNow(); }); it('implements equals correctly', function (): void { From c0801988fc70f2f81905422aefb8d2967bccc3c1 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 23 Sep 2026 12:48:19 +0200 Subject: [PATCH 11/52] feat: :sparkles: create Tag entity and associated VOs --- src/Domain/Models/Entities/Tag.php | 60 ++++++++++ src/Domain/Models/ValueObjects/Tag/TagId.php | 16 +++ .../Models/ValueObjects/Tag/TagName.php | 18 +++ tests/Unit/Domain/Models/Entities/TagTest.php | 103 ++++++++++++++++++ .../Models/ValueObjects/Tag/TagIdTest.php | 28 +++++ .../Models/ValueObjects/Tag/TagNameTest.php | 30 +++++ 6 files changed, 255 insertions(+) create mode 100644 src/Domain/Models/Entities/Tag.php create mode 100644 src/Domain/Models/ValueObjects/Tag/TagId.php create mode 100644 src/Domain/Models/ValueObjects/Tag/TagName.php create mode 100644 tests/Unit/Domain/Models/Entities/TagTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Tag/TagIdTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php diff --git a/src/Domain/Models/Entities/Tag.php b/src/Domain/Models/Entities/Tag.php new file mode 100644 index 0000000..449c8df --- /dev/null +++ b/src/Domain/Models/Entities/Tag.php @@ -0,0 +1,60 @@ + + */ +final readonly class Tag implements AggregateRoot, HasTimestampIdentifier +{ + private function __construct( + public TagId $id, + public TagName $value, + public ?\DateTimeInterface $updatedAt = null, + ) { + } + + public function id(): TagId + { + return $this->id; + } + + public function createdAt(): \DateTimeInterface + { + return $this->id->toDateTime(); + } + + public function updatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + public static function create( + TagName $value, + ?\DateTimeInterface $updatedAt = null, + ): Tag { + return new self( + id: TagId::generate(), + value: $value, + updatedAt: $updatedAt, + ); + } + + public static function hydrate( + TagId $id, + TagName $value, + ?\DateTimeInterface $updatedAt = null, + ): self { + return new self( + id: $id, + value: $value, + updatedAt: $updatedAt, + ); + } +} diff --git a/src/Domain/Models/ValueObjects/Tag/TagId.php b/src/Domain/Models/ValueObjects/Tag/TagId.php new file mode 100644 index 0000000..9ae28e2 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Tag/TagId.php @@ -0,0 +1,16 @@ +id())->toBe($id) + ->and($tag->value)->toBe($value) + ->and($tag->updatedAt)->toBeNull() + ->and($tag->updatedAt())->toBeNull(); + }); + + it('creates a tag with an explicit updatedAt date', function (): void { + // GIVEN + $id = TagId::generate(); + $name = new TagName('Name'); + $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); + + // WHEN + $tag = Tag::hydrate( + $id, + $name, + $updatedAt + ); + + // THEN + expect($tag->updatedAt)->toBe($updatedAt) + ->and($tag->updatedAt())->toBe($updatedAt); + }); + + it('creates a new tag with a freshly generated id', function (): void { + // GIVEN + $value = new TagName('Name'); + + // WHEN + $tag = Tag::create( + $value, + ); + + // THEN + expect($tag->id())->toBeInstanceOf(TagId::class) + ->and($tag->value)->toBe($value) + ->and($tag->updatedAt)->toBeNull() + ->and($tag->updatedAt())->toBeNull(); + }); + + it('generates a different id for each created tag', function (): void { + // GIVEN + $value = new TagName('Name'); + + // WHEN + $firstTag = Tag::create( + $value + ); + $secondTag = Tag::create( + $value + ); + + // THEN + expect($firstTag->id()->equals($secondTag->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 TagId(Uuid::uuid7($expected)); + $tag = Tag::hydrate( + $id, + new TagName('Name'), + ); + + // WHEN + $result = $tag->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/Tag/TagIdTest.php b/tests/Unit/Domain/Models/ValueObjects/Tag/TagIdTest.php new file mode 100644 index 0000000..298d233 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Tag/TagIdTest.php @@ -0,0 +1,28 @@ +toBeInstanceOf(TagId::class); + }); + + it('generates different ids on each call', function (): void { + // WHEN + $first = TagId::generate(); + $second = TagId::generate(); + + // THEN + expect((string) $first)->not->toBe((string) $second); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php new file mode 100644 index 0000000..be3f6b4 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php @@ -0,0 +1,30 @@ +value)->toBe($value); + }); + + it('rejects invalid tag name', function (): void { + // GIVEN + $value = ''; + + // GIVEN / THEN + expect(fn (): TagName => new TagName($value))->toThrow(\DomainException::class); + }); +}); From 9c5c3eaf64842f8bbd9cc320d64825ecda3933f9 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 23 Sep 2026 13:13:49 +0200 Subject: [PATCH 12/52] 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 ++ .../Domain/Models/Entities/CampaignTest.php | 216 ++++++++++++++++++ .../Campaign/CampaignCodeTest.php | 30 +++ .../Campaign/CampaignCouponTest.php | 66 ++++++ .../ValueObjects/Campaign/CampaignIdTest.php | 28 +++ .../Campaign/CampaignNameTest.php | 30 +++ 10 files changed, 547 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/Entities/CampaignTest.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 @@ +tag = Tag::hydrate( + TagId::generate(), + new TagName('Tag A'), + ); + }); + + afterEach(function (): void { + CarbonImmutable::setTestNow(); + }); + + it('creates a campaign', function (): void { + // GIVEN + $id = CampaignId::generate(); + $code = new CampaignCode('Camp100'); + $name = new CampaignName('Name'); + $publication = \boolval(0); + $printAllAtOnce = \boolval(1); + $availableIn = Country::FR; + $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), new StockId(1), new PrintRules(maxPerCustomer: 3), 100)]; + $brand = new BrandCode('AAA01'); + + // WHEN + $campaign = Campaign::hydrate( + $id, + $code, + $this->tag, + $name, + $publication, + $printAllAtOnce, + $availableIn, + $coupons, + $brand, + ); + + // THEN + expect($campaign->id())->toBe($id) + ->and($campaign->code)->toBe($code) + ->and($campaign->tag)->toBe($this->tag) + ->and($campaign->name)->toBe($name) + ->and($campaign->publication)->toBe($publication) + ->and($campaign->printAllAtOnce)->toBe($printAllAtOnce) + ->and($campaign->availableIn)->toBe($availableIn) + ->and($campaign->coupons)->toBe($coupons) + ->and($campaign->brand)->toBe($brand) + ->and($campaign->updatedAt)->toBeNull() + ->and($campaign->updatedAt())->toBeNull(); + }); + + it('creates a coupon with an explicit updatedAt date', function (): void { + // GIVEN + $id = CampaignId::generate(); + $code = new CampaignCode('Camp100'); + $name = new CampaignName('Name'); + $publication = \boolval(0); + $printAllAtOnce = \boolval(1); + $availableIn = Country::FR; + $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), new StockId(1), new PrintRules(maxPerCustomer: 3), 100)]; + $brand = new BrandCode('AAA01'); + $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); + + // WHEN + $campaign = Campaign::hydrate( + $id, + $code, + $this->tag, + $name, + $publication, + $printAllAtOnce, + $availableIn, + $coupons, + $brand, + $updatedAt + ); + + // THEN + expect($campaign->updatedAt)->toBe($updatedAt) + ->and($campaign->updatedAt())->toBe($updatedAt); + }); + + it('creates a new campaign with a freshly generated id', function (): void { + // GIVEN + $code = new CampaignCode('Camp100'); + $name = new CampaignName('Name'); + $publication = \boolval(0); + $printAllAtOnce = \boolval(1); + $availableIn = Country::FR; + $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), new StockId(1), new PrintRules(maxPerCustomer: 3), 100)]; + $brand = new BrandCode('AAA01'); + + // WHEN + $campaign = Campaign::create( + $code, + $this->tag, + $name, + $publication, + $printAllAtOnce, + $availableIn, + $coupons, + $brand + ); + + // THEN + expect($campaign->id())->toBeInstanceOf(CampaignId::class) + ->and($campaign->code)->toBe($code) + ->and($campaign->tag)->toBe($this->tag) + ->and($campaign->name)->toBe($name) + ->and($campaign->publication)->toBe($publication) + ->and($campaign->printAllAtOnce)->toBe($printAllAtOnce) + ->and($campaign->availableIn)->toBe($availableIn) + ->and($campaign->coupons)->toBe($coupons) + ->and($campaign->brand)->toBe($brand) + ->and($campaign->updatedAt)->toBeNull() + ->and($campaign->updatedAt())->toBeNull(); + }); + + it('generates a different id for each created campaign', function (): void { + // GIVEN + $code = new CampaignCode('Camp100'); + $name = new CampaignName('Name'); + $publication = \boolval(0); + $printAllAtOnce = \boolval(1); + $availableIn = Country::FR; + $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), new StockId(1), new PrintRules(maxPerCustomer: 3), 100)]; + $brand = new BrandCode('AAA01'); + + // WHEN + $firstCampaign = Campaign::create( + $code, + $this->tag, + $name, + $publication, + $printAllAtOnce, + $availableIn, + $coupons, + $brand + ); + $secondCampaign = Campaign::create( + $code, + $this->tag, + $name, + $publication, + $printAllAtOnce, + $availableIn, + $coupons, + $brand + ); + + // THEN + expect($firstCampaign->id()->equals($secondCampaign->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 CampaignId(Uuid::uuid7($expected)); + $campaign = Campaign::hydrate( + $id, + new CampaignCode('Camp100'), + $this->tag, + new CampaignName('Name'), + \boolval(0), + \boolval(1), + Country::FR, + new GS1('110101010', BarcodeFormat::EAN13), + [new CampaignCoupon(new ProcessingCode('PC1'), new StockId(1), new PrintRules(maxPerCustomer: 3), 100)], + new BrandCode('AAA01') + ); + + // WHEN + $result = $campaign->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/Campaign/CampaignCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php new file mode 100644 index 0000000..84b229c --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php @@ -0,0 +1,30 @@ +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); + }); +}); From 6a24dd7e8a7b49b73d6a0b0d60451207116927b2 Mon Sep 17 00:00:00 2001 From: K Muller Date: Wed, 23 Sep 2026 15:05:27 +0200 Subject: [PATCH 13/52] feat: :hammer: working on Stock and associated VO/Entities --- src/Domain/Models/Entities/Actor.php | 48 +++++++++++++++ src/Domain/Models/Entities/Stock.php | 61 +++++++++++++++++++ .../Models/ValueObjects/Actor/ActorId.php | 16 +++++ .../ValueObjects/Stock/StockAdjustment.php | 21 +++++++ .../Models/ValueObjects/Stock/StockAmount.php | 16 +++++ .../Models/ValueObjects/Stock/StockId.php | 16 +++++ .../Unit/Domain/Models/Entities/ActorTest.php | 53 ++++++++++++++++ .../Models/ValueObjects/Actor/ActorIdTest.php | 28 +++++++++ .../Stock/StockAdjustmentTest.php | 56 +++++++++++++++++ .../ValueObjects/Stock/StockAmountTest.php | 39 ++++++++++++ .../Models/ValueObjects/Stock/StockIdTest.php | 28 +++++++++ 11 files changed, 382 insertions(+) create mode 100644 src/Domain/Models/Entities/Actor.php create mode 100644 src/Domain/Models/Entities/Stock.php create mode 100644 src/Domain/Models/ValueObjects/Actor/ActorId.php create mode 100644 src/Domain/Models/ValueObjects/Stock/StockAdjustment.php create mode 100644 src/Domain/Models/ValueObjects/Stock/StockAmount.php create mode 100644 src/Domain/Models/ValueObjects/Stock/StockId.php create mode 100644 tests/Unit/Domain/Models/Entities/ActorTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Actor/ActorIdTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Stock/StockIdTest.php diff --git a/src/Domain/Models/Entities/Actor.php b/src/Domain/Models/Entities/Actor.php new file mode 100644 index 0000000..d5f2ec8 --- /dev/null +++ b/src/Domain/Models/Entities/Actor.php @@ -0,0 +1,48 @@ + + */ +final readonly class Actor implements AggregateRoot, HasTimestampIdentifier +{ + private function __construct( + public ActorId $id, + public string $displayName, + public ?\DateTimeInterface $updatedAt = null, + ) {} + + public function id(): ActorId + { + return $this->id; + } + + public function createdAt(): \DateTimeInterface + { + return $this->id->toDateTime(); + } + + public function updatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + public static function hydrate( + ActorId $id, + string $displayName, + ?\DateTimeInterface $updatedAt = null, + ): self { + return new self( + id: $id, + displayName: $displayName, + updatedAt: $updatedAt, + ); + } +} diff --git a/src/Domain/Models/Entities/Stock.php b/src/Domain/Models/Entities/Stock.php new file mode 100644 index 0000000..c289d20 --- /dev/null +++ b/src/Domain/Models/Entities/Stock.php @@ -0,0 +1,61 @@ + + */ +final readonly class Stock implements AggregateRoot, HasTimestampIdentifier +{ + private function __construct( + public StockId $id, + public StockAmount $stockAmount, + public StockAdjustment $stockAdjustment, + public CampaignCode $campaignCode, + public ProcessingCode $processingCode, + public ?\DateTimeInterface $updatedAt = null, + ) { + } + + public function id(): StockId + { + return $this->id; + } + + public function createdAt(): \DateTimeInterface + { + return $this->id->toDateTime(); + } + + public function updatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + public static function hydrate( + StockId $id, + StockAmount $stockAmount, + StockAdjustment $stockAdjustment, + CampaignCode $campaignCode, + ProcessingCode $processingCode, + ?\DateTimeInterface $updatedAt = null, + ): self { + return new self( + id: $id, + stockAmount: $stockAmount, + stockAdjustment: $stockAdjustment, + campaignCode: $campaignCode, + processingCode: $processingCode, + updatedAt: $updatedAt, + ); + } +} diff --git a/src/Domain/Models/ValueObjects/Actor/ActorId.php b/src/Domain/Models/ValueObjects/Actor/ActorId.php new file mode 100644 index 0000000..48e6ee1 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Actor/ActorId.php @@ -0,0 +1,16 @@ +quantity <= 0 ) { + throw new \DomainException('Stock Amount should be more than 0.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/Stock/StockAmount.php b/src/Domain/Models/ValueObjects/Stock/StockAmount.php new file mode 100644 index 0000000..3195bbd --- /dev/null +++ b/src/Domain/Models/ValueObjects/Stock/StockAmount.php @@ -0,0 +1,16 @@ +value < 0 ) { + throw new \DomainException('Stock Amount should be positive.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/Stock/StockId.php b/src/Domain/Models/ValueObjects/Stock/StockId.php new file mode 100644 index 0000000..000e873 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Stock/StockId.php @@ -0,0 +1,16 @@ +id())->toBe($id) + ->and($actor->displayName)->toBe($displayName) + ->and($actor->updatedAt)->toBeNull() + ->and($actor->updatedAt())->toBeNull(); + }); + + it('hydrates an actor with an explicit updatedAt date', function (): void { + // GIVEN + $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); + + // WHEN + $actor = Actor::hydrate(ActorId::generate(), 'John Doe', $updatedAt); + + // THEN + expect($actor->updatedAt)->toBe($updatedAt) + ->and($actor->updatedAt())->toBe($updatedAt); + }); + + 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 ActorId(Uuid::uuid7($expected)); + $actor = Actor::hydrate($id, 'John Doe'); + + // WHEN + $result = $actor->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/Actor/ActorIdTest.php b/tests/Unit/Domain/Models/ValueObjects/Actor/ActorIdTest.php new file mode 100644 index 0000000..166ba24 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Actor/ActorIdTest.php @@ -0,0 +1,28 @@ +toBeInstanceOf(ActorId::class); + }); + + it('generates different ids on each call', function (): void { + // WHEN + $first = ActorId::generate(); + $second = ActorId::generate(); + + // THEN + expect((string) $first)->not->toBe((string) $second); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php new file mode 100644 index 0000000..30d60d7 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php @@ -0,0 +1,56 @@ +toBeInstanceOf(StockAdjustment::class) + ->and($adjustment->quantity)->toBe($quantity) + ->and($adjustment->reason)->toBe($reason) + ->and($adjustment->actorId)->toBe($actorId) + ->and($adjustment->updatedAt)->toBeNull(); + }); + + it('creates a stock adjustment with an explicit updatedAt date', function (): void { + // GIVEN + $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); + + // WHEN + $adjustment = new StockAdjustment(10, 'Inventory correction', ActorId::generate(), $updatedAt); + + // THEN + expect($adjustment->updatedAt)->toBe($updatedAt); + }); + + it('rejects a quantity of zero', function (): void { + // WHEN + $action = fn (): StockAdjustment => new StockAdjustment(0, 'Inventory correction', ActorId::generate()); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Stock Amount should be more than 0.'); + }); + + it('rejects a negative quantity', function (): void { + // WHEN + $action = fn (): StockAdjustment => new StockAdjustment(-5, 'Inventory correction', ActorId::generate()); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Stock Amount should be more than 0.'); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php new file mode 100644 index 0000000..ae988fb --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php @@ -0,0 +1,39 @@ +toBeInstanceOf(StockAmount::class) + ->and($amount->value)->toBe($value); + }); + + it('accepts a zero value', function (): void { + // WHEN + $amount = new StockAmount(0); + + // THEN + expect($amount->value)->toBe(0); + }); + + it('rejects a negative value', function (): void { + // WHEN + $action = fn (): StockAmount => new StockAmount(-1); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Stock Amount should be positive.'); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Stock/StockIdTest.php b/tests/Unit/Domain/Models/ValueObjects/Stock/StockIdTest.php new file mode 100644 index 0000000..65e6c85 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Stock/StockIdTest.php @@ -0,0 +1,28 @@ +toBeInstanceOf(StockId::class); + }); + + it('generates different ids on each call', function (): void { + // WHEN + $first = StockId::generate(); + $second = StockId::generate(); + + // THEN + expect((string) $first)->not->toBe((string) $second); + }); +}); From 1891359e65c814623be9a2287684ae2f5840a9cb Mon Sep 17 00:00:00 2001 From: K Muller Date: Wed, 23 Sep 2026 15:31:51 +0200 Subject: [PATCH 14/52] feat: :sparkles: add stock entity --- src/Domain/Models/Entities/Actor.php | 8 +- src/Domain/Models/Entities/Stock.php | 14 ++-- .../ValueObjects/Actor/ActorDisplayName.php | 18 +++++ .../ValueObjects/Stock/StockAdjustment.php | 10 ++- .../Models/ValueObjects/Stock/StockAmount.php | 2 +- .../Unit/Domain/Models/Entities/ActorTest.php | 7 +- .../Unit/Domain/Models/Entities/StockTest.php | 77 +++++++++++++++++++ .../Actor/ActorDisplayNameTest.php | 30 ++++++++ .../Stock/StockAdjustmentTest.php | 10 +-- 9 files changed, 157 insertions(+), 19 deletions(-) create mode 100644 src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php create mode 100644 tests/Unit/Domain/Models/Entities/StockTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php diff --git a/src/Domain/Models/Entities/Actor.php b/src/Domain/Models/Entities/Actor.php index d5f2ec8..fecbafa 100644 --- a/src/Domain/Models/Entities/Actor.php +++ b/src/Domain/Models/Entities/Actor.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Domain\Models\Entities; +use Domain\Models\ValueObjects\Actor\ActorDisplayName; use Domain\Models\ValueObjects\Actor\ActorId; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; @@ -15,9 +16,10 @@ final readonly class Actor implements AggregateRoot, HasTimestampIdentifier { private function __construct( public ActorId $id, - public string $displayName, + public ActorDisplayName $displayName, public ?\DateTimeInterface $updatedAt = null, - ) {} + ) { + } public function id(): ActorId { @@ -36,7 +38,7 @@ final readonly class Actor implements AggregateRoot, HasTimestampIdentifier public static function hydrate( ActorId $id, - string $displayName, + ActorDisplayName $displayName, ?\DateTimeInterface $updatedAt = null, ): self { return new self( diff --git a/src/Domain/Models/Entities/Stock.php b/src/Domain/Models/Entities/Stock.php index c289d20..4d707dc 100644 --- a/src/Domain/Models/Entities/Stock.php +++ b/src/Domain/Models/Entities/Stock.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Domain\Models\Entities; +use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\Stock\StockAdjustment; use Domain\Models\ValueObjects\Stock\StockAmount; @@ -19,12 +20,12 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier private function __construct( public StockId $id, public StockAmount $stockAmount, - public StockAdjustment $stockAdjustment, public CampaignCode $campaignCode, public ProcessingCode $processingCode, + /** @var array $stockAdjustment */ + public array $stockAdjustment, public ?\DateTimeInterface $updatedAt = null, - ) { - } + ) {} public function id(): StockId { @@ -41,20 +42,23 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier return $this->updatedAt; } + /** + * @param array $stockAdjustment + */ public static function hydrate( StockId $id, StockAmount $stockAmount, - StockAdjustment $stockAdjustment, CampaignCode $campaignCode, ProcessingCode $processingCode, + array $stockAdjustment = [], ?\DateTimeInterface $updatedAt = null, ): self { return new self( id: $id, stockAmount: $stockAmount, - stockAdjustment: $stockAdjustment, campaignCode: $campaignCode, processingCode: $processingCode, + stockAdjustment: $stockAdjustment, updatedAt: $updatedAt, ); } diff --git a/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php b/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php new file mode 100644 index 0000000..483148f --- /dev/null +++ b/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php @@ -0,0 +1,18 @@ +quantity <= 0 ) { + if ($this->quantity <= 0) { throw new \DomainException('Stock Amount should be more than 0.'); } + + $trimmed = trim($reason); + + if ($trimmed === '') { + throw new \DomainException('Reason cannot be empty.'); + } } } diff --git a/src/Domain/Models/ValueObjects/Stock/StockAmount.php b/src/Domain/Models/ValueObjects/Stock/StockAmount.php index 3195bbd..8fbac0c 100644 --- a/src/Domain/Models/ValueObjects/Stock/StockAmount.php +++ b/src/Domain/Models/ValueObjects/Stock/StockAmount.php @@ -9,7 +9,7 @@ final readonly class StockAmount public function __construct( public int $value, ) { - if ($this->value < 0 ) { + if ($this->value < 0) { throw new \DomainException('Stock Amount should be positive.'); } } diff --git a/tests/Unit/Domain/Models/Entities/ActorTest.php b/tests/Unit/Domain/Models/Entities/ActorTest.php index 6ac741d..3d81e58 100644 --- a/tests/Unit/Domain/Models/Entities/ActorTest.php +++ b/tests/Unit/Domain/Models/Entities/ActorTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; use Domain\Models\Entities\Actor; +use Domain\Models\ValueObjects\Actor\ActorDisplayName; use Domain\Models\ValueObjects\Actor\ActorId; use Ramsey\Uuid\Uuid; @@ -14,7 +15,7 @@ describe('actor', function (): void { it('hydrates an actor', function (): void { // GIVEN $id = ActorId::generate(); - $displayName = 'John Doe'; + $displayName = new ActorDisplayName('John Doe'); // WHEN $actor = Actor::hydrate($id, $displayName); @@ -31,7 +32,7 @@ describe('actor', function (): void { $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); // WHEN - $actor = Actor::hydrate(ActorId::generate(), 'John Doe', $updatedAt); + $actor = Actor::hydrate(ActorId::generate(), new ActorDisplayName('John Doe'), $updatedAt); // THEN expect($actor->updatedAt)->toBe($updatedAt) @@ -42,7 +43,7 @@ describe('actor', function (): void { // GIVEN $expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC')); $id = new ActorId(Uuid::uuid7($expected)); - $actor = Actor::hydrate($id, 'John Doe'); + $actor = Actor::hydrate($id, new ActorDisplayName('John Doe')); // WHEN $result = $actor->createdAt(); diff --git a/tests/Unit/Domain/Models/Entities/StockTest.php b/tests/Unit/Domain/Models/Entities/StockTest.php new file mode 100644 index 0000000..637b966 --- /dev/null +++ b/tests/Unit/Domain/Models/Entities/StockTest.php @@ -0,0 +1,77 @@ +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->updatedAt)->toBeNull() + ->and($stock->updatedAt())->toBeNull(); + }); + + it('hydrates a stock with an explicit updatedAt date', function (): void { + // GIVEN + $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); + + // WHEN + $stock = Stock::hydrate( + StockId::generate(), + new StockAmount(10), + new CampaignCode('AAA01'), + new ProcessingCode('PC1'), + [new StockAdjustment(5, 'Inventory correction', ActorId::generate())], + $updatedAt, + ); + + // THEN + expect($stock->updatedAt)->toBe($updatedAt) + ->and($stock->updatedAt())->toBe($updatedAt); + }); + + 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 StockId(Uuid::uuid7($expected)); + $stock = Stock::hydrate( + $id, + new StockAmount(10), + new CampaignCode('AAA01'), + new ProcessingCode('PC1'), + [new StockAdjustment(5, 'Inventory correction', ActorId::generate())], + ); + + // WHEN + $result = $stock->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/Actor/ActorDisplayNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php new file mode 100644 index 0000000..8ea9a6f --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php @@ -0,0 +1,30 @@ +value)->toBe($value); + }); + + it('rejects invalid actor displayname', function (): void { + // GIVEN + $value = ''; + + // GIVEN / THEN + expect(fn (): ActorDisplayName => new ActorDisplayName($value))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php index 30d60d7..02515f0 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php @@ -24,18 +24,18 @@ describe('stock adjustment', function (): void { ->and($adjustment->quantity)->toBe($quantity) ->and($adjustment->reason)->toBe($reason) ->and($adjustment->actorId)->toBe($actorId) - ->and($adjustment->updatedAt)->toBeNull(); + ->and($adjustment->createdAt)->toBeNull(); }); - it('creates a stock adjustment with an explicit updatedAt date', function (): void { + it('creates a stock adjustment with an explicit createdAt date', function (): void { // GIVEN - $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); + $createdAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); // WHEN - $adjustment = new StockAdjustment(10, 'Inventory correction', ActorId::generate(), $updatedAt); + $adjustment = new StockAdjustment(10, 'Inventory correction', ActorId::generate(), $createdAt); // THEN - expect($adjustment->updatedAt)->toBe($updatedAt); + expect($adjustment->createdAt)->toBe($createdAt); }); it('rejects a quantity of zero', function (): void { From 2f17cd6a9a603e0ec330a63171705176c9d0b52a Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 23 Sep 2026 17:33:02 +0200 Subject: [PATCH 15/52] fix: :bug: resolve linter errors and tests --- src/Domain/Models/Entities/Campaign.php | 12 ++++++-- src/Domain/Models/Entities/Stock.php | 3 +- src/Domain/Models/Entities/Tag.php | 1 + .../ValueObjects/Campaign/CampaignCoupon.php | 5 ++-- .../Domain/Models/Entities/CampaignTest.php | 28 ++++--------------- .../Campaign/CampaignCouponTest.php | 17 +++++------ 6 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php index 48fdbf1..7ed5804 100644 --- a/src/Domain/Models/Entities/Campaign.php +++ b/src/Domain/Models/Entities/Campaign.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace Domain\Models\Entities; + use Domain\Enums\Country; use Domain\Models\Entities\Tag; use Domain\Models\ValueObjects\Brand\BrandCode; @@ -26,7 +27,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier public bool $publication, public bool $printAllAtOnce, public Country $availableIn, - /** @var CampaignCoupon[] */ + /** @var array $coupons */ public array $coupons, public BrandCode $brand, public ?\DateTimeInterface $updatedAt = null, @@ -48,6 +49,9 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier return $this->updatedAt; } + /** + * @param array $coupons + */ public static function create( CampaignCode $code, Tag $tag, @@ -55,7 +59,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier bool $publication, bool $printAllAtOnce, Country $availableIn, - /** @var CampaignCoupon[] */ + /** @var array $coupons */ array $coupons, BrandCode $brand, ?\DateTimeInterface $updatedAt = null, @@ -74,6 +78,9 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier ); } + /** + * @param array $coupons + */ public static function hydrate( CampaignId $id, CampaignCode $code, @@ -82,7 +89,6 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier bool $publication, bool $printAllAtOnce, Country $availableIn, - /** @var CampaignCoupon[] */ array $coupons, BrandCode $brand, ?\DateTimeInterface $updatedAt = null, diff --git a/src/Domain/Models/Entities/Stock.php b/src/Domain/Models/Entities/Stock.php index 4d707dc..69693f1 100644 --- a/src/Domain/Models/Entities/Stock.php +++ b/src/Domain/Models/Entities/Stock.php @@ -25,7 +25,8 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier /** @var array $stockAdjustment */ public array $stockAdjustment, public ?\DateTimeInterface $updatedAt = null, - ) {} + ) { + } public function id(): StockId { diff --git a/src/Domain/Models/Entities/Tag.php b/src/Domain/Models/Entities/Tag.php index 449c8df..bb8e084 100644 --- a/src/Domain/Models/Entities/Tag.php +++ b/src/Domain/Models/Entities/Tag.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace Domain\Models\Entities; + use Domain\Models\ValueObjects\Tag\TagId; use Domain\Models\ValueObjects\Tag\TagName; use Shared\DDD\Models\AggregateRoot; diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php index ced1d7f..2e7ca71 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php @@ -6,14 +6,15 @@ namespace Domain\Models\ValueObjects\Campaign; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\Stock\StockId; final readonly class CampaignCoupon { public function __construct( public ProcessingCode $code, public StockId $qty, - public ?PrintRules $printRules, - public int $warningThresholdQty + public int $warningThresholdQty, + public ?PrintRules $printRules = null, ) { if ($this->warningThresholdQty <= 0) { throw new \DomainException('Campaign Coupon warningThresholdQty must be positive.'); diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index 500b649..f2c0f92 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; -use Domain\Enums\BarcodeFormat; use Domain\Enums\Country; use Domain\Models\Entities\Campaign; use Domain\Models\Entities\Tag; @@ -14,26 +13,12 @@ use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; -use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar; -use Domain\Models\ValueObjects\Coupon\CouponDiffusion; -use Domain\Models\ValueObjects\Coupon\CouponId; -use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; -use Domain\Models\ValueObjects\Coupon\CouponProduct; -use Domain\Models\ValueObjects\Coupon\CouponValidity; -use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\CouponPicture; -use Domain\Models\ValueObjects\CouponProductName; -use Domain\Models\ValueObjects\GS1; -use Domain\Models\ValueObjects\MoneyAmount; -use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\Stock\StockId; use Domain\Models\ValueObjects\Tag\TagId; use Domain\Models\ValueObjects\Tag\TagName; use Ramsey\Uuid\Uuid; -use Shared\Enums\Currency; -use Shared\ValueObjects\DateTime; -use Shared\ValueObjects\Money; covers(Campaign::class); @@ -57,7 +42,7 @@ describe('campaign', function (): void { $publication = \boolval(0); $printAllAtOnce = \boolval(1); $availableIn = Country::FR; - $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), new StockId(1), new PrintRules(maxPerCustomer: 3), 100)]; + $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; $brand = new BrandCode('AAA01'); // WHEN @@ -95,7 +80,7 @@ describe('campaign', function (): void { $publication = \boolval(0); $printAllAtOnce = \boolval(1); $availableIn = Country::FR; - $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), new StockId(1), new PrintRules(maxPerCustomer: 3), 100)]; + $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; $brand = new BrandCode('AAA01'); $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); @@ -125,7 +110,7 @@ describe('campaign', function (): void { $publication = \boolval(0); $printAllAtOnce = \boolval(1); $availableIn = Country::FR; - $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), new StockId(1), new PrintRules(maxPerCustomer: 3), 100)]; + $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; $brand = new BrandCode('AAA01'); // WHEN @@ -161,7 +146,7 @@ describe('campaign', function (): void { $publication = \boolval(0); $printAllAtOnce = \boolval(1); $availableIn = Country::FR; - $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), new StockId(1), new PrintRules(maxPerCustomer: 3), 100)]; + $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; $brand = new BrandCode('AAA01'); // WHEN @@ -202,8 +187,7 @@ describe('campaign', function (): void { \boolval(0), \boolval(1), Country::FR, - new GS1('110101010', BarcodeFormat::EAN13), - [new CampaignCoupon(new ProcessingCode('PC1'), new StockId(1), new PrintRules(maxPerCustomer: 3), 100)], + [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))], new BrandCode('AAA01') ); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php index 1be0701..9d1b6ba 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php @@ -7,6 +7,7 @@ namespace Tests\Unit\Domain\Models\ValueObjects\Campaign; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\Stock\StockId; covers(CampaignCoupon::class); @@ -14,12 +15,12 @@ describe('campaign coupon', function (): void { it('creates a valid campaign coupon', function (): void { // GIVEN $code = new ProcessingCode('PC1'); - $qty = new StockId(); + $qty = StockId::generate(); $printRules = new PrintRules(maxPerCustomer: 3); $warningThresholdQty = 100; // WHEN - $campaignCoupon = new CampaignCoupon(code: $code, qty: $qty, printRules: $printRules, warningThresholdQty: $warningThresholdQty); + $campaignCoupon = new CampaignCoupon($code, $qty, $warningThresholdQty, $printRules); // THEN expect($campaignCoupon->code)->toBe($code) @@ -31,11 +32,11 @@ describe('campaign coupon', function (): void { it('creates a valid campaign coupon whitout print rules', function (): void { // GIVEN $code = new ProcessingCode('PC1'); - $qty = new StockId(); + $qty = StockId::generate(); $warningThresholdQty = 100; // WHEN - $campaignCoupon = new CampaignCoupon(code: $code, qty: $qty, warningThresholdQty: $warningThresholdQty); + $campaignCoupon = new CampaignCoupon($code, $qty, $warningThresholdQty); // THEN expect($campaignCoupon->code)->toBe($code) @@ -46,21 +47,21 @@ describe('campaign coupon', function (): void { it('rejects invalid campaign coupon', function (): void { // GIVEN $code = new ProcessingCode('PC1'); - $qty = new StockId(); + $qty = StockId::generate(); $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); + expect(fn (): CampaignCoupon => new CampaignCoupon($code, $qty, $warningThresholdQty, $printRules))->toThrow(\DomainException::class); }); it('rejects invalid campaign coupon whitout print rules', function (): void { // GIVEN $code = new ProcessingCode('PC1'); - $qty = new StockId(); + $qty = StockId::generate(); $warningThresholdQty = -100; // GIVEN / THEN - expect(fn (): CampaignCoupon => new CampaignCoupon(code: $code, qty: $qty, warningThresholdQty: $warningThresholdQty))->toThrow(\DomainException::class); + expect(fn (): CampaignCoupon => new CampaignCoupon($code, $qty, $warningThresholdQty))->toThrow(\DomainException::class); }); }); From 063e3b79fbb2a33875d770fd092ee51c2b9a85d0 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Thu, 24 Sep 2026 09:48:01 +0200 Subject: [PATCH 16/52] fix: :rotating_light: resolve pint errors --- src/Domain/Models/Entities/Campaign.php | 1 - tests/Unit/Domain/Models/Entities/CampaignTest.php | 4 ++-- tests/Unit/Domain/Models/Entities/TagTest.php | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php index 7ed5804..b4068c4 100644 --- a/src/Domain/Models/Entities/Campaign.php +++ b/src/Domain/Models/Entities/Campaign.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Domain\Models\Entities; use Domain\Enums\Country; -use Domain\Models\Entities\Tag; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index f2c0f92..2132239 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -177,8 +177,8 @@ describe('campaign', function (): void { 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 CampaignId(Uuid::uuid7($expected)); + $expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC')); + $id = new CampaignId(Uuid::uuid7($expected)); $campaign = Campaign::hydrate( $id, new CampaignCode('Camp100'), diff --git a/tests/Unit/Domain/Models/Entities/TagTest.php b/tests/Unit/Domain/Models/Entities/TagTest.php index 95f4149..1ade199 100644 --- a/tests/Unit/Domain/Models/Entities/TagTest.php +++ b/tests/Unit/Domain/Models/Entities/TagTest.php @@ -89,7 +89,7 @@ describe('tag', function (): void { // GIVEN $expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC')); $id = new TagId(Uuid::uuid7($expected)); - $tag = Tag::hydrate( + $tag = Tag::hydrate( $id, new TagName('Name'), ); From 5b872045c5cb9a122b6497134cddc883ea9e8091 Mon Sep 17 00:00:00 2001 From: K Muller Date: Wed, 23 Sep 2026 09:29:22 +0200 Subject: [PATCH 17/52] fix: :rotating_light: linters --- .../Find/FindCampaignRequest.php | 58 +++++ .../UseCases/CampaignRequest/Find/Input.php | 16 ++ .../Find/Output/CampaignRequestFound.php | 31 +++ .../Find/Output/CampaignRequestNotFound.php | 9 + .../CampaignRequest/Find/Output/Output.php | 11 + .../Exceptions/BrandNotFoundException.php | 19 ++ .../CampaignRequestRepository.php | 6 +- .../Find/FindCampaignRequestTest.php | 222 ++++++++++++++++++ 8 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php create mode 100644 src/Application/UseCases/CampaignRequest/Find/Input.php create mode 100644 src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php create mode 100644 src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestNotFound.php create mode 100644 src/Application/UseCases/CampaignRequest/Find/Output/Output.php create mode 100644 src/Domain/Exceptions/BrandNotFoundException.php create mode 100644 tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php diff --git a/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php new file mode 100644 index 0000000..7e28121 --- /dev/null +++ b/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php @@ -0,0 +1,58 @@ + */ +final readonly class FindCampaignRequest implements UseCase +{ + public function __construct( + private CampaignRequestRepository $repository, + private BrandRepository $brandRepository, + ) { + } + + #[\Override] + public function execute(BaseInput $input): Output + { + Assert::isInstanceOf($input, Input::class); + + $campaignRequest = $this->repository->findById($input->id); + + if (! $campaignRequest instanceof CampaignRequest) { + return new CampaignRequestNotFound(); + } + + $brand = $this->brandRepository->findByCode($campaignRequest->code); + + if (! $brand instanceof Brand) { + throw new BrandNotFoundException($campaignRequest->code); + } + + return new CampaignRequestFound( + brand: $brand, + applicant: $campaignRequest->applicant, + country: $campaignRequest->country, + campaignType: $campaignRequest->campaignType, + printRules: $campaignRequest->printRules, + requestedCoupon: $campaignRequest->requestedCoupon, + status: $campaignRequest->status, + rejectionReason: $campaignRequest->rejectionReason, + createdAt: $campaignRequest->createdAt(), + updatedAt: $campaignRequest->updatedAt(), + ); + } +} diff --git a/src/Application/UseCases/CampaignRequest/Find/Input.php b/src/Application/UseCases/CampaignRequest/Find/Input.php new file mode 100644 index 0000000..29221ad --- /dev/null +++ b/src/Application/UseCases/CampaignRequest/Find/Input.php @@ -0,0 +1,16 @@ +value)); + } +} diff --git a/src/Domain/Ports/Repositories/CampaignRequestRepository.php b/src/Domain/Ports/Repositories/CampaignRequestRepository.php index 7ba64c2..6c2c086 100644 --- a/src/Domain/Ports/Repositories/CampaignRequestRepository.php +++ b/src/Domain/Ports/Repositories/CampaignRequestRepository.php @@ -4,11 +4,13 @@ declare(strict_types=1); namespace Domain\Ports\Repositories; +use Domain\Models\Entities\CampaignRequest; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Shared\DDD\Ports\Repository; /** * @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\Brand\BrandId, \Domain\Models\Entities\Brand> */ -interface CampaignRequestRepository extends Repository -{ +interface CampaignRequestRepository extends Repository { + public function findById(CampaignRequestId $id): ?CampaignRequest; } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php new file mode 100644 index 0000000..3a91a39 --- /dev/null +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -0,0 +1,222 @@ + static), out of scope for now. + // + // it('finds a campaign request by its id, along with its brand', function (): void { + // // GIVEN + // $id = CampaignRequestId::generate(); + // $code = new BrandCode('AAA01'); + // $campaignRequest = CampaignRequest::hydrate( + // $id, + // $code, + // new Applicant('John', 'Doe', new Email('john.doe@example.com')), + // Country::FR, + // CampaignType::CLUB, + // new PrintRules(maxPerCustomer: 3), + // aFindableRequestedCoupon(), + // CampaignStatus::CREATED, + // ); + // $brand = aFindableBrand($code); + // + // $repository = \Mockery::mock(CampaignRequestRepository::class); + // $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); + // + // $brandRepository = \Mockery::mock(BrandRepository::class); + // $brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn($brand); + // + // $useCase = new FindCampaignRequest($repository, $brandRepository); + // + // // WHEN + // $result = $useCase->execute(new Input(id: $id)); + // + // // THEN + // expect($result)->toBeInstanceOf(CampaignRequestFound::class) + // ->and($result->brand)->toBe($brand) + // ->and($result->applicant)->toBe($campaignRequest->applicant) + // ->and($result->country)->toBe($campaignRequest->country) + // ->and($result->campaignType)->toBe($campaignRequest->campaignType) + // ->and($result->printRules)->toBe($campaignRequest->printRules) + // ->and($result->requestedCoupon)->toBe($campaignRequest->requestedCoupon) + // ->and($result->status)->toBe($campaignRequest->status) + // ->and($result->rejectionReason)->toBeNull(); + // }); + // + // it('finds a rejected campaign request with its rejection reason', function (): void { + // // GIVEN + // $id = CampaignRequestId::generate(); + // $code = new BrandCode('AAA01'); + // $rejectionReason = new RejectionReason('Missing legal documents.'); + // $campaignRequest = CampaignRequest::hydrate( + // $id, + // $code, + // new Applicant('John', 'Doe', new Email('john.doe@example.com')), + // Country::FR, + // CampaignType::CLUB, + // new PrintRules(maxPerCustomer: 3), + // aFindableRequestedCoupon(), + // CampaignStatus::REJECTED, + // $rejectionReason, + // ); + // + // $repository = \Mockery::mock(CampaignRequestRepository::class); + // $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); + // + // $brandRepository = \Mockery::mock(BrandRepository::class); + // $brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(aFindableBrand($code)); + // + // $useCase = new FindCampaignRequest($repository, $brandRepository); + // + // // WHEN + // $result = $useCase->execute(new Input(id: $id)); + // + // // THEN + // expect($result)->toBeInstanceOf(CampaignRequestFound::class) + // ->and($result->status)->toBe(CampaignStatus::REJECTED) + // ->and($result->rejectionReason)->toBe($rejectionReason); + // }); + + it('returns CampaignRequestNotFound when the id is not found', function (): void { + // GIVEN + $id = CampaignRequestId::generate(); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findById')->once()->with($id)->andReturn(null); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldNotReceive('findByCode'); + + $useCase = new FindCampaignRequest($repository, $brandRepository); + + // WHEN + $result = $useCase->execute(new Input(id: $id)); + + // THEN + expect($result)->toBeInstanceOf(CampaignRequestNotFound::class); + }); + + it('throws when the campaign request references a brand that no longer exists', function (): void { + // GIVEN + $id = CampaignRequestId::generate(); + $code = new BrandCode('AAA01'); + $campaignRequest = CampaignRequest::hydrate( + $id, + $code, + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + aFindableRequestedCoupon(), + CampaignStatus::CREATED, + ); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(null); + + $useCase = new FindCampaignRequest($repository, $brandRepository); + + // WHEN + $action = fn (): Output => $useCase->execute(new Input(id: $id)); + + // THEN + expect($action)->toThrow(BrandNotFoundException::class); + }); +}); + +describe('find campaign request use case assertions', function (): void { + it('rejects a wrong type input', function (): void { + // GIVEN + $repository = \Mockery::mock(CampaignRequestRepository::class); + $brandRepository = \Mockery::mock(BrandRepository::class); + $useCase = new FindCampaignRequest($repository, $brandRepository); + $invalidInput = new class () implements BaseInput + { + }; + + // WHEN + $action = fn (): Output => $useCase->execute($invalidInput); + + // THEN + expect($action)->toThrow(\InvalidArgumentException::class); + }); +}); From 41a73edd97733054bd97cb044c6be983bd191862 Mon Sep 17 00:00:00 2001 From: K Muller Date: Wed, 23 Sep 2026 09:40:47 +0200 Subject: [PATCH 18/52] fix: :rotating_light: linters --- .../Find/Output/CampaignRequestFound.php | 4 +- .../CampaignRequestRepository.php | 5 +- .../Find/FindCampaignRequestTest.php | 214 +++++++++--------- 3 files changed, 111 insertions(+), 112 deletions(-) diff --git a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php index 732a2e4..ecb675e 100644 --- a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php +++ b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php @@ -7,8 +7,8 @@ namespace Application\UseCases\CampaignRequest\Find\Output; use Domain\Enums\Campaign\CampaignStatus; use Domain\Enums\Campaign\CampaignType; use Domain\Enums\Country; +use Domain\Models\Entities\Brand; use Domain\Models\Entities\RequestedCoupon; -use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Models\ValueObjects\PrintRules; @@ -16,7 +16,7 @@ use Domain\Models\ValueObjects\PrintRules; final readonly class CampaignRequestFound implements Output { public function __construct( - public BrandCode $code, + public Brand $brand, public Applicant $applicant, public Country $country, public CampaignType $campaignType, diff --git a/src/Domain/Ports/Repositories/CampaignRequestRepository.php b/src/Domain/Ports/Repositories/CampaignRequestRepository.php index 6c2c086..b421c0c 100644 --- a/src/Domain/Ports/Repositories/CampaignRequestRepository.php +++ b/src/Domain/Ports/Repositories/CampaignRequestRepository.php @@ -11,6 +11,7 @@ use Shared\DDD\Ports\Repository; /** * @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\Brand\BrandId, \Domain\Models\Entities\Brand> */ -interface CampaignRequestRepository extends Repository { - public function findById(CampaignRequestId $id): ?CampaignRequest; +interface CampaignRequestRepository extends Repository +{ + public function findById(CampaignRequestId $id): ?CampaignRequest; } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 3a91a39..62dc9b8 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -45,113 +45,111 @@ use Shared\ValueObjects\Rcs; covers(FindCampaignRequest::class, Input::class, CampaignRequestFound::class, CampaignRequestNotFound::class); -function aFindableRequestedCoupon(): RequestedCoupon -{ - return RequestedCoupon::hydrate( - RequestedCouponId::generate(), - new CouponName('Coupon A'), - new CouponProductName('Product A'), - 100, - 10, - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC1'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), - new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), - ); -} - -function aFindableBrand(BrandCode $code): Brand -{ - return Brand::hydrate( - BrandId::generate(), - $code, - new BrandName('Brand A'), - new PicturePath('brands/logo.jpg'), - new Rcs('RCS PARIS 111 222 333'), - new Email('contact@example.com'), - ); -} -#FIXME describe('find campaign request use case', function (): void { - // Disabled: these two tests build a real Brand via aFindableBrand(), which calls - // BrandId::generate(). BrandId::generate(): self is not covariant with the parent - // Uuidv7EntityId::generate(): static, which fatals PHP as soon as BrandId.php is - // loaded. Re-enable once BrandId is fixed (self -> static), out of scope for now. - // - // it('finds a campaign request by its id, along with its brand', function (): void { - // // GIVEN - // $id = CampaignRequestId::generate(); - // $code = new BrandCode('AAA01'); - // $campaignRequest = CampaignRequest::hydrate( - // $id, - // $code, - // new Applicant('John', 'Doe', new Email('john.doe@example.com')), - // Country::FR, - // CampaignType::CLUB, - // new PrintRules(maxPerCustomer: 3), - // aFindableRequestedCoupon(), - // CampaignStatus::CREATED, - // ); - // $brand = aFindableBrand($code); - // - // $repository = \Mockery::mock(CampaignRequestRepository::class); - // $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); - // - // $brandRepository = \Mockery::mock(BrandRepository::class); - // $brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn($brand); - // - // $useCase = new FindCampaignRequest($repository, $brandRepository); - // - // // WHEN - // $result = $useCase->execute(new Input(id: $id)); - // - // // THEN - // expect($result)->toBeInstanceOf(CampaignRequestFound::class) - // ->and($result->brand)->toBe($brand) - // ->and($result->applicant)->toBe($campaignRequest->applicant) - // ->and($result->country)->toBe($campaignRequest->country) - // ->and($result->campaignType)->toBe($campaignRequest->campaignType) - // ->and($result->printRules)->toBe($campaignRequest->printRules) - // ->and($result->requestedCoupon)->toBe($campaignRequest->requestedCoupon) - // ->and($result->status)->toBe($campaignRequest->status) - // ->and($result->rejectionReason)->toBeNull(); - // }); - // - // it('finds a rejected campaign request with its rejection reason', function (): void { - // // GIVEN - // $id = CampaignRequestId::generate(); - // $code = new BrandCode('AAA01'); - // $rejectionReason = new RejectionReason('Missing legal documents.'); - // $campaignRequest = CampaignRequest::hydrate( - // $id, - // $code, - // new Applicant('John', 'Doe', new Email('john.doe@example.com')), - // Country::FR, - // CampaignType::CLUB, - // new PrintRules(maxPerCustomer: 3), - // aFindableRequestedCoupon(), - // CampaignStatus::REJECTED, - // $rejectionReason, - // ); - // - // $repository = \Mockery::mock(CampaignRequestRepository::class); - // $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); - // - // $brandRepository = \Mockery::mock(BrandRepository::class); - // $brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(aFindableBrand($code)); - // - // $useCase = new FindCampaignRequest($repository, $brandRepository); - // - // // WHEN - // $result = $useCase->execute(new Input(id: $id)); - // - // // THEN - // expect($result)->toBeInstanceOf(CampaignRequestFound::class) - // ->and($result->status)->toBe(CampaignStatus::REJECTED) - // ->and($result->rejectionReason)->toBe($rejectionReason); - // }); + beforeEach(function (): void { + $this->requestedCoupon = RequestedCoupon::hydrate( + RequestedCouponId::generate(), + new CouponName('Coupon A'), + new CouponProductName('Product A'), + 100, + 10, + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + new MoneyAmount(new Money(Currency::EUR, 300)), + new ProcessingCode('PC1'), + new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new GS1('01234567890128', BarcodeFormat::EAN13), + new CouponValidity(30, DateTime::now()), + ); + }); + + it('finds a campaign request by its id, along with its brand', function (): void { + // GIVEN + $id = CampaignRequestId::generate(); + $code = new BrandCode('AAA01'); + $campaignRequest = CampaignRequest::hydrate( + $id, + $code, + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + $this->requestedCoupon, + CampaignStatus::CREATED, + ); + $brand = Brand::hydrate( + BrandId::generate(), + $code, + new BrandName('Brand A'), + new PicturePath('brands/logo.jpg'), + new Rcs('RCS PARIS 111 222 333'), + new Email('contact@example.com'), + ); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn($brand); + + $useCase = new FindCampaignRequest($repository, $brandRepository); + + // WHEN + $result = $useCase->execute(new Input(id: $id)); + + // THEN + expect($result)->toBeInstanceOf(CampaignRequestFound::class) + ->and($result->brand)->toBe($brand) + ->and($result->applicant)->toBe($campaignRequest->applicant) + ->and($result->country)->toBe($campaignRequest->country) + ->and($result->campaignType)->toBe($campaignRequest->campaignType) + ->and($result->printRules)->toBe($campaignRequest->printRules) + ->and($result->requestedCoupon)->toBe($campaignRequest->requestedCoupon) + ->and($result->status)->toBe($campaignRequest->status) + ->and($result->rejectionReason)->toBeNull(); + }); + + it('finds a rejected campaign request with its rejection reason', function (): void { + // GIVEN + $id = CampaignRequestId::generate(); + $code = new BrandCode('AAA01'); + $rejectionReason = new RejectionReason('Missing legal documents.'); + $campaignRequest = CampaignRequest::hydrate( + $id, + $code, + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + $this->requestedCoupon, + CampaignStatus::REJECTED, + $rejectionReason, + ); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn( + Brand::hydrate( + BrandId::generate(), + $code, + new BrandName('Brand A'), + new PicturePath('brands/logo.jpg'), + new Rcs('RCS PARIS 111 222 333'), + new Email('contact@example.com'), + ) + ); + + $useCase = new FindCampaignRequest($repository, $brandRepository); + + // WHEN + $result = $useCase->execute(new Input(id: $id)); + + // THEN + expect($result)->toBeInstanceOf(CampaignRequestFound::class) + ->and($result->status)->toBe(CampaignStatus::REJECTED) + ->and($result->rejectionReason)->toBe($rejectionReason); + }); it('returns CampaignRequestNotFound when the id is not found', function (): void { // GIVEN @@ -183,7 +181,7 @@ describe('find campaign request use case', function (): void { Country::FR, CampaignType::CLUB, new PrintRules(maxPerCustomer: 3), - aFindableRequestedCoupon(), + $this->requestedCoupon, CampaignStatus::CREATED, ); @@ -204,7 +202,7 @@ describe('find campaign request use case', function (): void { }); describe('find campaign request use case assertions', function (): void { - it('rejects a wrong type input', function (): void { + it('rejects an input of the wrong type', function (): void { // GIVEN $repository = \Mockery::mock(CampaignRequestRepository::class); $brandRepository = \Mockery::mock(BrandRepository::class); From 895b535f76bcc5fae903277c05c04bc4acd7336d Mon Sep 17 00:00:00 2001 From: K Muller Date: Wed, 23 Sep 2026 11:34:44 +0200 Subject: [PATCH 19/52] feat: :sparkles: add the rejection method for campaign request --- .../UseCases/CampaignRequest/Reject/Input.php | 18 ++ .../Reject/Output/CampaignRequestNotFound.php | 9 + .../Reject/Output/CampaignRequestRejected.php | 20 +++ .../CampaignRequest/Reject/Output/Output.php | 11 ++ .../Reject/RejectCampaignRequest.php | 51 ++++++ .../Models/Entities/CampaignRequest.php | 21 +++ .../CampaignRequestRepository.php | 2 + .../Reject/RejectCampaignRequestTest.php | 163 ++++++++++++++++++ .../Models/Entities/CampaignRequestTest.php | 78 +++++++++ 9 files changed, 373 insertions(+) create mode 100644 src/Application/UseCases/CampaignRequest/Reject/Input.php create mode 100644 src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestNotFound.php create mode 100644 src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php create mode 100644 src/Application/UseCases/CampaignRequest/Reject/Output/Output.php create mode 100644 src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php create mode 100644 tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php diff --git a/src/Application/UseCases/CampaignRequest/Reject/Input.php b/src/Application/UseCases/CampaignRequest/Reject/Input.php new file mode 100644 index 0000000..d1783ff --- /dev/null +++ b/src/Application/UseCases/CampaignRequest/Reject/Input.php @@ -0,0 +1,18 @@ + */ +final readonly class RejectCampaignRequest implements UseCase, Transactional +{ + public function __construct( + private CampaignRequestRepository $repository, + ) { + } + + #[\Override] + public function execute(BaseInput $input): Output + { + Assert::isInstanceOf($input, Input::class); + + $campaignRequest = $this->repository->findById($input->id); + + if (! $campaignRequest instanceof CampaignRequest) { + return new CampaignRequestNotFound(); + } + + $campaignRequest = $campaignRequest->reject($input->reason); + + $this->repository->save($campaignRequest); + + Assert::isInstanceOf($campaignRequest->rejectionReason, RejectionReason::class); + Assert::notNull($campaignRequest->updatedAt()); + + return new CampaignRequestRejected( + id: $campaignRequest->id(), + status: $campaignRequest->status, + rejectionReason: $campaignRequest->rejectionReason, + updatedAt: $campaignRequest->updatedAt(), + ); + } +} diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index a007125..bdf1de0 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -14,6 +14,7 @@ use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Models\ValueObjects\PrintRules; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; +use Shared\Helpers\DatesHelper; /** * @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId> @@ -82,4 +83,24 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent updatedAt: $updatedAt, ); } + + public function reject(RejectionReason $reason): self + { + if ($this->status !== CampaignStatus::CREATED) { + throw new \DomainException('Only a created campaign request can be rejected.'); + } + + return new self( + id: $this->id, + code: $this->code, + applicant: $this->applicant, + country: $this->country, + campaignType: $this->campaignType, + printRules: $this->printRules, + requestedCoupon: $this->requestedCoupon, + status: CampaignStatus::REJECTED, + rejectionReason: $reason, + updatedAt: DatesHelper::now(), + ); + } } diff --git a/src/Domain/Ports/Repositories/CampaignRequestRepository.php b/src/Domain/Ports/Repositories/CampaignRequestRepository.php index b421c0c..a218598 100644 --- a/src/Domain/Ports/Repositories/CampaignRequestRepository.php +++ b/src/Domain/Ports/Repositories/CampaignRequestRepository.php @@ -14,4 +14,6 @@ use Shared\DDD\Ports\Repository; interface CampaignRequestRepository extends Repository { public function findById(CampaignRequestId $id): ?CampaignRequest; + + public function save(CampaignRequest $campaignRequest): void; } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php new file mode 100644 index 0000000..23c3b30 --- /dev/null +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -0,0 +1,163 @@ +requestedCoupon = RequestedCoupon::hydrate( + RequestedCouponId::generate(), + new CouponName('Coupon A'), + new CouponProductName('Product A'), + 100, + 10, + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + new MoneyAmount(new Money(Currency::EUR, 300)), + new ProcessingCode('PC1'), + new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new GS1('01234567890128', BarcodeFormat::EAN13), + new CouponValidity(30, DateTime::now()), + ); + }); + + it('rejects a campaign request and persists it', function (): void { + // GIVEN + CarbonImmutable::setTestNow('2026-06-15 12:00:00'); + $id = CampaignRequestId::generate(); + $campaignRequest = CampaignRequest::hydrate( + $id, + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + $this->requestedCoupon, + CampaignStatus::CREATED, + ); + $reason = new RejectionReason('Missing legal documents.'); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); + $repository->shouldReceive('save')->once()->with(\Mockery::on( + fn (CampaignRequest $saved): bool => $saved->status === CampaignStatus::REJECTED + && $saved->rejectionReason === $reason + && $saved->updatedAt !== null, + )); + + $useCase = new RejectCampaignRequest($repository); + + // WHEN + $result = $useCase->execute(new Input(id: $id, reason: $reason)); + + // THEN + expect($result)->toBeInstanceOf(CampaignRequestRejected::class) + ->and($result->id)->toBe($id) + ->and($result->status)->toBe(CampaignStatus::REJECTED) + ->and($result->rejectionReason)->toBe($reason) + ->and($result->updatedAt->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00'); + }); + + it('returns CampaignRequestNotFound when the id is not found', function (): void { + // GIVEN + $id = CampaignRequestId::generate(); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findById')->once()->with($id)->andReturn(null); + $repository->shouldNotReceive('save'); + + $useCase = new RejectCampaignRequest($repository); + + // WHEN + $result = $useCase->execute(new Input(id: $id, reason: new RejectionReason('Missing legal documents.'))); + + // THEN + expect($result)->toBeInstanceOf(CampaignRequestNotFound::class); + }); + + it('propagates the domain exception when the campaign request cannot be rejected', function (): void { + // GIVEN + $id = CampaignRequestId::generate(); + $campaignRequest = CampaignRequest::hydrate( + $id, + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + $this->requestedCoupon, + CampaignStatus::TRANSFORMED, + ); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); + $repository->shouldNotReceive('save'); + + $useCase = new RejectCampaignRequest($repository); + + // WHEN + $action = fn (): Output => $useCase->execute( + new Input(id: $id, reason: new RejectionReason('Missing legal documents.')) + ); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.'); + }); +}); + +describe('reject campaign request use case assertions', function (): void { + it('rejects an input of the wrong type', function (): void { + // GIVEN + $repository = \Mockery::mock(CampaignRequestRepository::class); + $useCase = new RejectCampaignRequest($repository); + $invalidInput = new class () implements BaseInput + { + }; + + // WHEN + $action = fn (): Output => $useCase->execute($invalidInput); + + // THEN + expect($action)->toThrow(\InvalidArgumentException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 07d213d..94e6262 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -30,10 +30,15 @@ use Shared\Enums\Currency; use Shared\ValueObjects\DateTime; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; +use Carbon\CarbonImmutable; covers(CampaignRequest::class); describe('campaign request', function (): void { + afterEach(function (): void { + CarbonImmutable::setTestNow(); + }); + beforeEach(function (): void { $this->requestedCoupon = RequestedCoupon::hydrate( RequestedCouponId::generate(), @@ -189,6 +194,79 @@ describe('campaign request', function (): void { ); }); + it('rejects a campaign request and sets the rejection reason and updatedAt', function (): void { + // GIVEN + CarbonImmutable::setTestNow('2026-06-15 12:00:00'); + $campaignRequest = CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + $this->requestedCoupon, + CampaignStatus::CREATED, + ); + $reason = new RejectionReason('Missing legal documents.'); + + // WHEN + $rejected = $campaignRequest->reject($reason); + + // THEN + expect($rejected->status)->toBe(CampaignStatus::REJECTED) + ->and($rejected->rejectionReason)->toBe($reason) + ->and($rejected->id())->toBe($campaignRequest->id()) + ->and($rejected->code)->toBe($campaignRequest->code) + ->and($rejected->applicant)->toBe($campaignRequest->applicant) + ->and($rejected->country)->toBe($campaignRequest->country) + ->and($rejected->campaignType)->toBe($campaignRequest->campaignType) + ->and($rejected->printRules)->toBe($campaignRequest->printRules) + ->and($rejected->requestedCoupon)->toBe($campaignRequest->requestedCoupon) + ->and($rejected->updatedAt)->not->toBeNull() + ->and($rejected->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00'); + }); + + it('rejects rejecting a transformed campaign request', function (): void { + // GIVEN + $campaignRequest = CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + $this->requestedCoupon, + CampaignStatus::TRANSFORMED, + ); + + // WHEN + $action = fn (): CampaignRequest => $campaignRequest->reject(new RejectionReason('Missing legal documents.')); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.'); + }); + + it('rejects rejecting an already rejected campaign request', function (): void { + // GIVEN + $campaignRequest = CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + Country::FR, + CampaignType::CLUB, + new PrintRules(maxPerCustomer: 3), + $this->requestedCoupon, + CampaignStatus::REJECTED, + new RejectionReason('First reason.'), + ); + + // WHEN + $action = fn (): CampaignRequest => $campaignRequest->reject(new RejectionReason('Second reason.')); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.'); + }); + 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')); From 7312d3a1924d8378be878568b9372013e2042792 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 23 Sep 2026 10:06:24 +0200 Subject: [PATCH 20/52] feat: :sparkles: add findAll method to campaign request --- .../List/CampaignRequestListItem.php | 20 ++ .../UseCases/CampaignRequest/List/Input.php | 19 ++ .../List/ListCampaignsRequest.php | 50 +++++ .../UseCases/CampaignRequest/List/Output.php | 16 ++ .../CampaignRequestFilters.php | 16 ++ .../CampaignRequestSortClause.php | 13 ++ .../CampaignRequestSortCriteria.php | 17 ++ .../CampaignRequestSortField.php | 12 ++ .../CampaignRequestRepository.php | 12 ++ .../List/ListCampaignsRequestTest.php | 171 ++++++++++++++++++ .../CampaignRequestSortCriteriaTest.php | 32 ++++ 11 files changed, 378 insertions(+) create mode 100644 src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php create mode 100644 src/Application/UseCases/CampaignRequest/List/Input.php create mode 100644 src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php create mode 100644 src/Application/UseCases/CampaignRequest/List/Output.php create mode 100644 src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php create mode 100644 src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortClause.php create mode 100644 src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteria.php create mode 100644 src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortField.php create mode 100644 tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php diff --git a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php new file mode 100644 index 0000000..05571eb --- /dev/null +++ b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php @@ -0,0 +1,20 @@ + */ +final readonly class ListCampaignsRequest implements UseCase +{ + public function __construct( + private CampaignRequestRepository $repository, + ) {} + + #[\Override] + public function execute(BaseInput $input): Output + { + Assert::isInstanceOf($input, Input::class); + + $result = $this->repository->findAll( + pagination: $input->pagination, + sortCriteria: $input->sort, + filters: $input->filters + ); + + $items = array_map( + static fn(CampaignRequest $campaignRequest): CampaignRequestListItem => new CampaignRequestListItem( + code: $campaignRequest->code, + applicant: $campaignRequest->applicant, + campaignType: $campaignRequest->campaignType, + status: $campaignRequest->status + ), + $result->getItems(), + ); + + return new Output( + campaignsRequest: new Paginated( + pagination: $result->getPagination(), + items: $items, + total: $result->getTotal(), + ), + ); + } +} diff --git a/src/Application/UseCases/CampaignRequest/List/Output.php b/src/Application/UseCases/CampaignRequest/List/Output.php new file mode 100644 index 0000000..fcf6678 --- /dev/null +++ b/src/Application/UseCases/CampaignRequest/List/Output.php @@ -0,0 +1,16 @@ + $campaignsRequest */ + public function __construct( + public Paginated $campaignsRequest, + ) {} +} diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php new file mode 100644 index 0000000..ccd333b --- /dev/null +++ b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php @@ -0,0 +1,16 @@ +clauses === []) { + throw new \DomainException('Campaign Request Sort Criteria requires at least one clause.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortField.php b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortField.php new file mode 100644 index 0000000..d44563f --- /dev/null +++ b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortField.php @@ -0,0 +1,12 @@ + @@ -16,4 +20,12 @@ interface CampaignRequestRepository extends Repository public function findById(CampaignRequestId $id): ?CampaignRequest; public function save(CampaignRequest $campaignRequest): void; + /** + * @return Paginated + */ + public function findAll( + Pagination $pagination, + ?CampaignRequestSortCriteria $sortCriteria = null, + ?CampaignRequestFilters $filters = null, + ): Paginated; } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php new file mode 100644 index 0000000..6eb63d4 --- /dev/null +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -0,0 +1,171 @@ +code, CampaignStatus::CREATED); + $input = new Input(pagination: $pagination, sort: $sort, filters: $filters); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findAll') + ->once() + ->with($pagination, CampaignRequestSortCriteria::class, CampaignRequestFilters::class) + ->andReturn($paginated); + + $useCase = new ListCampaignsRequest($repository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(Output::class); + + $items = $result->campaignsRequest->getItems(); + expect($items)->toHaveCount(2) + ->and($items[0])->toBeInstanceOf(CampaignRequestListItem::class) + ->and($items[0]->code)->toBe($campaignRequest1->code) + ->and($items[0]->applicant)->toBe($campaignRequest1->applicant) + ->and($items[0]->campaignType)->toBe($campaignRequest1->campaignType) + ->and($items[0]->status)->toBe($campaignRequest1->status) + ->and($result->campaignsRequest->getTotal())->toBe(2) + ->and($result->campaignsRequest->getPagination())->toBe($pagination); + }); + + it('returns an empty list without error when there are no campaigns request', function (): void { + // GIVEN + $pagination = Pagination::firstPage(); + $paginated = new Paginated($pagination, [], 0); + $input = new Input(pagination: $pagination); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findAll') + ->once() + ->with($pagination, null, null) + ->andReturn($paginated); + + $useCase = new ListCampaignsRequest($repository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result->campaignsRequest->getItems())->toBe([]) + ->and($result->campaignsRequest->getTotal())->toBe(0); + }); +}); + +describe('list brands use case assertions', function (): void { + it('rejects an input of the wrong type', function (): void { + // GIVEN + $repository = \Mockery::mock(CampaignRequestRepository::class); + $useCase = new ListCampaignsRequest($repository); + $invalidInput = new class() implements BaseInput {}; + + // WHEN + $action = fn(): Output => $useCase->execute($invalidInput); + + // THEN + expect($action)->toThrow(\InvalidArgumentException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php new file mode 100644 index 0000000..2c161e5 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php @@ -0,0 +1,32 @@ +clauses)->toBe($clauses); + }); + + it('rejects invalid campaign request sort criteria', function (): void { + // GIVEN + $clauses = []; + + // GIVEN / THEN + expect(fn(): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($clauses))->toThrow(\DomainException::class); + }); +}); From 50eb40841fe96198023c0458bd35909d85bc5fa3 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 23 Sep 2026 10:25:57 +0200 Subject: [PATCH 21/52] fix: :rotating_light: resolve phpcs errors --- .../List/CampaignRequestListItem.php | 3 +- .../UseCases/CampaignRequest/List/Input.php | 3 +- .../List/ListCampaignsRequest.php | 3 +- .../UseCases/CampaignRequest/List/Output.php | 3 +- .../CampaignRequestFilters.php | 3 +- .../CampaignRequestSortClause.php | 3 +- .../List/ListCampaignsRequestTest.php | 81 ++++++++++--------- 7 files changed, 54 insertions(+), 45 deletions(-) diff --git a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php index 05571eb..6d622f4 100644 --- a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php +++ b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php @@ -16,5 +16,6 @@ final readonly class CampaignRequestListItem public Applicant $applicant, public CampaignType $campaignType, public CampaignStatus $status - ) {} + ) { + } } diff --git a/src/Application/UseCases/CampaignRequest/List/Input.php b/src/Application/UseCases/CampaignRequest/List/Input.php index b13ae0e..9b2802c 100644 --- a/src/Application/UseCases/CampaignRequest/List/Input.php +++ b/src/Application/UseCases/CampaignRequest/List/Input.php @@ -15,5 +15,6 @@ final readonly class Input implements UseCaseInput public Pagination $pagination, public ?CampaignRequestSortCriteria $sort = null, public ?CampaignRequestFilters $filters = null, - ) {} + ) { + } } diff --git a/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php b/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php index 2c26ac0..1c5fa1d 100644 --- a/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php +++ b/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php @@ -16,7 +16,8 @@ final readonly class ListCampaignsRequest implements UseCase { public function __construct( private CampaignRequestRepository $repository, - ) {} + ) { + } #[\Override] public function execute(BaseInput $input): Output diff --git a/src/Application/UseCases/CampaignRequest/List/Output.php b/src/Application/UseCases/CampaignRequest/List/Output.php index fcf6678..ef1d456 100644 --- a/src/Application/UseCases/CampaignRequest/List/Output.php +++ b/src/Application/UseCases/CampaignRequest/List/Output.php @@ -12,5 +12,6 @@ final readonly class Output implements UseCaseOutput /** @param Paginated $campaignsRequest */ public function __construct( public Paginated $campaignsRequest, - ) {} + ) { + } } diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php index ccd333b..b6d6dc3 100644 --- a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php +++ b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php @@ -12,5 +12,6 @@ final readonly class CampaignRequestFilters public function __construct( public ?BrandCode $brandCode = null, public ?CampaignStatus $status = null, - ) {} + ) { + } } diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortClause.php b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortClause.php index 1f51e80..d61623a 100644 --- a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortClause.php +++ b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortClause.php @@ -9,5 +9,6 @@ final readonly class CampaignRequestSortClause public function __construct( public CampaignRequestSortField $field, public \SortDirection $direction, - ) {} + ) { + } } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 6eb63d4..994ac48 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -43,41 +43,37 @@ use Shared\ValueObjects\Pagination\Pagination; covers(ListCampaignsRequest::class, Input::class, Output::class, CampaignRequestListItem::class); -function aRequestedCoupon(): RequestedCoupon -{ - return RequestedCoupon::hydrate( - RequestedCouponId::generate(), - new CouponName('Coupon A'), - new CouponProductName('Product A'), - 100, - 10, - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC1'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), - new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), - ); -} - -function bRequestedCoupon(): RequestedCoupon -{ - return RequestedCoupon::hydrate( - RequestedCouponId::generate(), - new CouponName('Coupon B'), - new CouponProductName('Product B'), - 100, - 10, - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC2'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), - new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), - ); -} - describe('list campaigns request use case', function (): void { + beforeEach(function (): void { + $this->couponA = RequestedCoupon::hydrate( + RequestedCouponId::generate(), + new CouponName('Coupon A'), + new CouponProductName('Product A'), + 100, + 10, + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + new MoneyAmount(new Money(Currency::EUR, 300)), + new ProcessingCode('PC1'), + new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new GS1('01234567890128', BarcodeFormat::EAN13), + new CouponValidity(30, DateTime::now()), + ); + + $this->couponB = RequestedCoupon::hydrate( + RequestedCouponId::generate(), + new CouponName('Coupon B'), + new CouponProductName('Product B'), + 100, + 10, + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + new MoneyAmount(new Money(Currency::EUR, 300)), + new ProcessingCode('PC2'), + new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new GS1('01234567890128', BarcodeFormat::EAN13), + new CouponValidity(30, DateTime::now()), + ); + }); + it('maps repository campaigns request to list items, preserving pagination and sort', function (): void { // GIVEN $pagination = Pagination::firstPage(); @@ -88,7 +84,7 @@ describe('list campaigns request use case', function (): void { Country::FR, CampaignType::CLUB, new PrintRules(maxPerCustomer: 3), - aRequestedCoupon(), + $this->couponA, CampaignStatus::CREATED, ); $campaignRequest2 = CampaignRequest::hydrate( @@ -98,12 +94,17 @@ describe('list campaigns request use case', function (): void { Country::FR, CampaignType::CLUB, new PrintRules(maxPerCustomer: 3), - bRequestedCoupon(), - CampaignStatus::REJECTED, + $this->couponB, + CampaignStatus::CREATED, ); $paginated = new Paginated($pagination, [$campaignRequest1, $campaignRequest2], 2); - $sort = new CampaignRequestSortCriteria(clauses: [new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending), new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending)]); + $sort = new CampaignRequestSortCriteria( + clauses: [ + new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending), + new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending) + ] + ); $filters = new CampaignRequestFilters($campaignRequest2->code, CampaignStatus::CREATED); $input = new Input(pagination: $pagination, sort: $sort, filters: $filters); @@ -160,7 +161,9 @@ describe('list brands use case assertions', function (): void { // GIVEN $repository = \Mockery::mock(CampaignRequestRepository::class); $useCase = new ListCampaignsRequest($repository); - $invalidInput = new class() implements BaseInput {}; + $invalidInput = new class () implements BaseInput + { + }; // WHEN $action = fn(): Output => $useCase->execute($invalidInput); From 01b4a049b28987e8ddebec32362f1a1cfd36a62b Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Thu, 24 Sep 2026 09:50:12 +0200 Subject: [PATCH 22/52] fix: :rotating_light: resolve pint errors --- .../UseCases/CampaignRequest/List/ListCampaignsRequest.php | 2 +- .../CampaignRequest/List/ListCampaignsRequestTest.php | 6 +----- .../CampaignRequest/CampaignRequestSortCriteriaTest.php | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php b/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php index 1c5fa1d..f61059a 100644 --- a/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php +++ b/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php @@ -31,7 +31,7 @@ final readonly class ListCampaignsRequest implements UseCase ); $items = array_map( - static fn(CampaignRequest $campaignRequest): CampaignRequestListItem => new CampaignRequestListItem( + static fn (CampaignRequest $campaignRequest): CampaignRequestListItem => new CampaignRequestListItem( code: $campaignRequest->code, applicant: $campaignRequest->applicant, campaignType: $campaignRequest->campaignType, diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 994ac48..5a06de4 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -4,10 +4,6 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\List; -use Application\UseCases\CampaignRequest\List\CampaignRequestListItem; -use Application\UseCases\CampaignRequest\List\Input; -use Application\UseCases\CampaignRequest\List\ListCampaignsRequest; -use Application\UseCases\CampaignRequest\List\Output; use Domain\Enums\BarcodeFormat; use Domain\Enums\Campaign\CampaignStatus; use Domain\Enums\Campaign\CampaignType; @@ -166,7 +162,7 @@ describe('list brands use case assertions', function (): void { }; // WHEN - $action = fn(): Output => $useCase->execute($invalidInput); + $action = fn (): Output => $useCase->execute($invalidInput); // THEN expect($action)->toThrow(\InvalidArgumentException::class); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php index 2c161e5..14f54e6 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php @@ -27,6 +27,6 @@ describe('campaign request sort criteria', function (): void { $clauses = []; // GIVEN / THEN - expect(fn(): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($clauses))->toThrow(\DomainException::class); + expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($clauses))->toThrow(\DomainException::class); }); }); From e5e64d69be01e6a3c05f864a3e5bd42de2fd0fe2 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Thu, 24 Sep 2026 11:28:29 +0200 Subject: [PATCH 23/52] refactor: :recycle: align Coupon entity with UML cardinalities --- src/Domain/Models/Entities/Coupon.php | 29 +++-- .../Domain/Models/Entities/CouponTest.php | 111 ++++++++++++++---- 2 files changed, 110 insertions(+), 30 deletions(-) diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index fdd285b..563b12e 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Domain\Models\Entities; +use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; @@ -36,10 +37,12 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier public CouponDiffusion $diffusion, public CouponAvailabilityCalendar $availability, public CouponValidity $validity, - public CouponLegalInformation $legal, - public GS1 $gtin, + /** @var array $legal */ + public array $legal, public ProcessingCode $code, public BrandCode $brandCode, + public ActorId $createdBy, + public ?GS1 $gtin = null, public ?\DateTimeInterface $updatedAt = null, ) { } @@ -59,6 +62,9 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier return $this->updatedAt; } + /** + * @param array $legal + */ public static function create( CouponName $name, bool $status, @@ -69,10 +75,11 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier CouponDiffusion $diffusion, CouponAvailabilityCalendar $availability, CouponValidity $validity, - CouponLegalInformation $legal, - GS1 $gtin, + array $legal, ProcessingCode $code, BrandCode $brandCode, + ActorId $createdBy, + ?GS1 $gtin = null, ?\DateTimeInterface $updatedAt = null, ): Coupon { return new self( @@ -87,13 +94,17 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier availability: $availability, validity: $validity, legal: $legal, - gtin: $gtin, code: $code, brandCode: $brandCode, + createdBy: $createdBy, + gtin: $gtin, updatedAt: $updatedAt, ); } + /** + * @param array $legal + */ public static function hydrate( CouponId $id, CouponName $name, @@ -105,10 +116,11 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier CouponDiffusion $diffusion, CouponAvailabilityCalendar $availability, CouponValidity $validity, - CouponLegalInformation $legal, - GS1 $gtin, + array $legal, ProcessingCode $code, BrandCode $brandCode, + ActorId $createdBy, + ?GS1 $gtin = null, ?\DateTimeInterface $updatedAt = null, ): self { return new self( @@ -123,9 +135,10 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier availability: $availability, validity: $validity, legal: $legal, - gtin: $gtin, code: $code, brandCode: $brandCode, + createdBy: $createdBy, + gtin: $gtin, updatedAt: $updatedAt, ); } diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index ca5be82..8351b16 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -8,6 +8,7 @@ use Carbon\CarbonImmutable; use Domain\Enums\BarcodeFormat; use Domain\Enums\Country; use Domain\Models\Entities\Coupon; +use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; @@ -46,10 +47,11 @@ describe('coupon', function (): void { $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); + $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); // WHEN $coupon = Coupon::hydrate( @@ -64,9 +66,10 @@ describe('coupon', function (): void { $availability, $validity, $legal, - $gtin, $code, - $brandCode + $brandCode, + $createdBy, + $gtin, ); // THEN @@ -81,9 +84,64 @@ describe('coupon', function (): void { ->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->createdBy)->toBe($createdBy) + ->and($coupon->gtin)->toBe($gtin) + ->and($coupon->updatedAt)->toBeNull() + ->and($coupon->updatedAt())->toBeNull(); + }); + + 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(); + + // WHEN + $coupon = Coupon::hydrate( + $id, + $name, + $status, + $productName, + $amount, + $picture, + $product, + $diffusion, + $availability, + $validity, + $legal, + $code, + $brandCode, + $createdBy, + ); + + // THEN + expect($coupon->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->code)->toBe($code) + ->and($coupon->brandCode)->toBe($brandCode) + ->and($coupon->createdBy)->toBe($createdBy) ->and($coupon->updatedAt)->toBeNull() ->and($coupon->updatedAt())->toBeNull(); }); @@ -100,10 +158,11 @@ describe('coupon', function (): void { $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); + $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')); // WHEN @@ -119,9 +178,10 @@ describe('coupon', function (): void { $availability, $validity, $legal, - $gtin, $code, $brandCode, + $createdBy, + $gtin, $updatedAt ); @@ -141,10 +201,11 @@ describe('coupon', function (): void { $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); + $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); // WHEN $coupon = Coupon::create( @@ -158,9 +219,10 @@ describe('coupon', function (): void { $availability, $validity, $legal, - $gtin, $code, - $brandCode + $brandCode, + $createdBy, + $gtin, ); // THEN @@ -175,9 +237,10 @@ describe('coupon', function (): void { ->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->createdBy)->toBe($createdBy) + ->and($coupon->gtin)->toBe($gtin) ->and($coupon->updatedAt)->toBeNull() ->and($coupon->updatedAt())->toBeNull(); }); @@ -193,10 +256,11 @@ describe('coupon', function (): void { $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); + $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); // WHEN $firstCoupon = Coupon::create( @@ -210,9 +274,10 @@ describe('coupon', function (): void { $availability, $validity, $legal, - $gtin, $code, - $brandCode + $brandCode, + $createdBy, + $gtin ); $secondCoupon = Coupon::create( $name, @@ -225,9 +290,10 @@ describe('coupon', function (): void { $availability, $validity, $legal, - $gtin, $code, - $brandCode + $brandCode, + $createdBy, + $gtin ); // THEN @@ -249,10 +315,11 @@ describe('coupon', function (): void { 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 CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR)], new ProcessingCode('5425003944130'), - new BrandCode('AAA01') + new BrandCode('AAA01'), + ActorId::generate(), + new GS1('110101010', BarcodeFormat::EAN13), ); // WHEN From 19afb9f6369a5ce37b58dd62c5e8021eb0b9caff Mon Sep 17 00:00:00 2001 From: K Muller Date: Thu, 24 Sep 2026 14:56:17 +0200 Subject: [PATCH 24/52] fix: :bug: delete legacy base enum and backed int for enums --- src/Domain/Enums/BarcodeFormat.php | 24 +--- src/Domain/Enums/Campaign/CampaignStatus.php | 24 +--- src/Domain/Enums/Campaign/CampaignType.php | 21 +-- src/Domain/Enums/Country.php | 24 +--- .../Coupon/CouponLegalInformation.php | 2 +- .../ValueObjects/Coupon/CouponProductInfo.php | 2 +- src/Shared/Enums/BaseEnum.php | 133 ------------------ src/Shared/Enums/Currency.php | 16 +-- src/Shared/ValueObjects/Money.php | 4 +- tests/Unit/Domain/Enums/BarcodeFormatTest.php | 66 +-------- .../Enums/Campaign/CampaignStatusTest.php | 66 +-------- .../Enums/Campaign/CampaignTypeTest.php | 61 +------- tests/Unit/Domain/Enums/CountryTest.php | 66 +-------- tests/Unit/Shared/Enums/CurrencyTest.php | 52 +------ 14 files changed, 37 insertions(+), 524 deletions(-) delete mode 100644 src/Shared/Enums/BaseEnum.php diff --git a/src/Domain/Enums/BarcodeFormat.php b/src/Domain/Enums/BarcodeFormat.php index db2dc18..5c625c6 100644 --- a/src/Domain/Enums/BarcodeFormat.php +++ b/src/Domain/Enums/BarcodeFormat.php @@ -4,25 +4,9 @@ declare(strict_types=1); namespace Domain\Enums; -use Shared\Enums\BaseEnum; - -enum BarcodeFormat: int +enum BarcodeFormat: string { - /** @use BaseEnum */ - use BaseEnum; - - case EAN13 = 1; - case CODE128 = 2; - case GS1_128 = 3; - - /** - * Optional labels definition - * - * @phpstan-ignore-next-line - */ - private const LABELS = [ - self::EAN13->name => 'EAN', - self::CODE128->name => 'CODE 128', - self::GS1_128->name => 'GS1_128', - ]; + case EAN13 = "ean"; + case CODE128 = "code128"; + case GS1_128 = "gs1"; } diff --git a/src/Domain/Enums/Campaign/CampaignStatus.php b/src/Domain/Enums/Campaign/CampaignStatus.php index 96142de..c617a78 100644 --- a/src/Domain/Enums/Campaign/CampaignStatus.php +++ b/src/Domain/Enums/Campaign/CampaignStatus.php @@ -4,25 +4,9 @@ declare(strict_types=1); namespace Domain\Enums\Campaign; -use Shared\Enums\BaseEnum; - -enum CampaignStatus: int +enum CampaignStatus: string { - /** @use \Shared\Enums\BaseEnum */ - use BaseEnum; - - case CREATED = 1; - case TRANSFORMED = 2; - case REJECTED = 3; - - /** - * Optional labels definition - * - * @phpstan-ignore-next-line - */ - private const LABELS = [ - self::CREATED->name => 'Crée', - self::TRANSFORMED->name => 'Transformé', - self::REJECTED->name => 'Rejeté' - ]; + case CREATED = "created"; + case TRANSFORMED = "transformed"; + case REJECTED = "rejected"; } diff --git a/src/Domain/Enums/Campaign/CampaignType.php b/src/Domain/Enums/Campaign/CampaignType.php index 5728cb3..93cde5f 100644 --- a/src/Domain/Enums/Campaign/CampaignType.php +++ b/src/Domain/Enums/Campaign/CampaignType.php @@ -4,23 +4,8 @@ declare(strict_types=1); namespace Domain\Enums\Campaign; -use Shared\Enums\BaseEnum; - -enum CampaignType: int +enum CampaignType: string { - /** @use \Shared\Enums\BaseEnum */ - use BaseEnum; - - case CLUB = 1; - case GAME = 2; - - /** - * Optional labels definition - * - * @phpstan-ignore-next-line - */ - private const LABELS = [ - self::CLUB->name => 'CLUB', - self::GAME->name => 'GAME', - ]; + case CLUB = "club"; + case GAME = "game"; } diff --git a/src/Domain/Enums/Country.php b/src/Domain/Enums/Country.php index a75b0e4..969ac5e 100644 --- a/src/Domain/Enums/Country.php +++ b/src/Domain/Enums/Country.php @@ -4,25 +4,9 @@ declare(strict_types=1); namespace Domain\Enums; -use Shared\Enums\BaseEnum; - -enum Country: int +enum Country: string { - /** @use \Shared\Enums\BaseEnum */ - use BaseEnum; - - case FR = 1; - case BE = 2; - case LU = 3; - - /** - * Optional labels definition - * - * @phpstan-ignore-next-line - */ - private const LABELS = [ - self::FR->name => 'FR', - self::BE->name => 'BE', - self::LU->name => 'LU', - ]; + case FR = "fr"; + case BE = "be"; + case LU = "lu"; } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php index 3f4fbd5..8a83105 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php @@ -23,6 +23,6 @@ final readonly class CouponLegalInformation implements Equatable #[\Override] public function equals(mixed $other): bool { - return $other instanceof self && $this->legalText === $other->legalText && $this->forLocale->equals($other->forLocale); + return $other instanceof self && $this->legalText === $other->legalText && $this->forLocale === $other->forLocale; } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php index 97928fc..8d2a70a 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php @@ -41,6 +41,6 @@ final readonly class CouponProductInfo implements Equatable && $this->name === $other->name && $this->range === $other->range && $this->validityText === $other->validityText - && $this->forLocale->equals($other->forLocale); + && $this->forLocale === $other->forLocale; } } diff --git a/src/Shared/Enums/BaseEnum.php b/src/Shared/Enums/BaseEnum.php deleted file mode 100644 index cb4bee0..0000000 --- a/src/Shared/Enums/BaseEnum.php +++ /dev/null @@ -1,133 +0,0 @@ -value; - } - - public function label(): string - { - return self::toLabels()[$this->name] ?? ucfirst(strtolower($this->name)); - } - - public function name(): string - { - return $this->name; - } - - /** - * @return array - */ - public static function toLabels(): array - { - /** @var array|null */ - static $cache = null; - - if ($cache !== null) { - return $cache; - } - - $labels = defined(self::class . '::' . self::LABELS_CONSTANT_NAME) - ? constant(self::class . '::' . self::LABELS_CONSTANT_NAME) - : null; - - $result = []; - foreach (self::cases() as $enum) { - if ($labels !== null && ! isset($labels[$enum->name])) { - throw new \Error('Missing definition label ' . self::class . " for '{$enum->name}'"); - } - $result[$enum->name] = $labels[$enum->name] ?? ucfirst(strtolower($enum->name)); - } - - return $cache = $result; - } - - /** - * @return array - */ - public static function toValues(): array - { - /** @var array|null */ - static $cache = null; - - if ($cache !== null) { - return $cache; - } - - return $cache = array_column( - self::cases(), - 'value', - 'name' - ); - } - - /** - * @param self|V $value - */ - public static function make(self | int | string $value, string $field = 'value'): ?self - { - if ($value instanceof self) { - return $value; - } - - if ($field === 'value') { - $tryValue = null; - try { - $tryValue = self::tryFrom(intval($value)); - } catch (\ValueError) { - $tryValue = self::tryFrom(strval($value)); - } finally { - if ($tryValue !== null) { - return $tryValue; - } - } - } - - if ($field === 'label') { - $normalizedSearch = preg_replace('/[\W_]+/u', '', mb_strtolower((string) $value)); - - foreach (self::cases() as $enum) { - $normalizedLabel = preg_replace('/[\W_]+/u', '', mb_strtolower($enum->label())); - if ($normalizedLabel === $normalizedSearch) { - return $enum; - } - } - - return null; - } - - foreach (self::cases() as $enum) { - if ($enum->{$field} === $value) { - return $enum; - } - } - - return null; - } - - public function equals(mixed $other): bool - { - return $this === $other; - } - - public function jsonSerialize(): mixed - { - return $this->value; - } -} diff --git a/src/Shared/Enums/Currency.php b/src/Shared/Enums/Currency.php index ac68c2d..93b8181 100644 --- a/src/Shared/Enums/Currency.php +++ b/src/Shared/Enums/Currency.php @@ -4,19 +4,7 @@ declare(strict_types=1); namespace Shared\Enums; -enum Currency: int +enum Currency: string { - /** @use BaseEnum */ - use BaseEnum; - - case EUR = 1; - - /** - * Optional labels definition - * - * @phpstan-ignore-next-line - */ - private const LABELS = [ - self::EUR->name => 'Euro', - ]; + case EUR = "euro"; } diff --git a/src/Shared/ValueObjects/Money.php b/src/Shared/ValueObjects/Money.php index 08529cb..f70521b 100644 --- a/src/Shared/ValueObjects/Money.php +++ b/src/Shared/ValueObjects/Money.php @@ -31,12 +31,12 @@ final readonly class Money implements \Stringable, Equatable public function equals(mixed $other): bool { return $other instanceof self - && $this->currency->equals($other->currency) + && $this->currency->value === $other->currency->value && $this->value === $other->value; } public function __toString(): string { - return sprintf('%d %s', $this->value, $this->currency->name()); + return sprintf('%d %s', $this->value, $this->currency->name); } } diff --git a/tests/Unit/Domain/Enums/BarcodeFormatTest.php b/tests/Unit/Domain/Enums/BarcodeFormatTest.php index b9c9882..e91bf1d 100644 --- a/tests/Unit/Domain/Enums/BarcodeFormatTest.php +++ b/tests/Unit/Domain/Enums/BarcodeFormatTest.php @@ -11,71 +11,13 @@ covers(BarcodeFormat::class); describe('barcode format', function (): void { it('has the expected cases with their backing values', function (): void { // THEN - expect(BarcodeFormat::EAN13->value())->toBe(1) - ->and(BarcodeFormat::CODE128->value())->toBe(2) - ->and(BarcodeFormat::GS1_128->value())->toBe(3); + expect(BarcodeFormat::EAN13->value)->toBe("ean") + ->and(BarcodeFormat::CODE128->value)->toBe("code128") + ->and(BarcodeFormat::GS1_128->value)->toBe("gs1"); }); 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'); + expect(BarcodeFormat::EAN13->name)->toBe('EAN13'); }); }); diff --git a/tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php b/tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php index 6f9df27..259264f 100644 --- a/tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php +++ b/tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php @@ -11,71 +11,13 @@ covers(CampaignStatus::class); describe('campaign status', function (): void { it('has the expected cases with their backing values', function (): void { // THEN - expect(CampaignStatus::CREATED->value())->toBe(1) - ->and(CampaignStatus::TRANSFORMED->value())->toBe(2) - ->and(CampaignStatus::REJECTED->value())->toBe(3); + expect(CampaignStatus::CREATED->value)->toBe("created") + ->and(CampaignStatus::TRANSFORMED->value)->toBe("transformed") + ->and(CampaignStatus::REJECTED->value)->toBe("rejected"); }); it('exposes its case name', function (): void { // THEN - expect(CampaignStatus::CREATED->name())->toBe('CREATED'); - }); - - it('returns the defined label', function (): void { - // THEN - expect(CampaignStatus::CREATED->label())->toBe('Crée') - ->and(CampaignStatus::TRANSFORMED->label())->toBe('Transformé') - ->and(CampaignStatus::REJECTED->label())->toBe('Rejeté'); - }); - - it('lists all labels keyed by case name', function (): void { - // THEN - expect(CampaignStatus::toLabels())->toBe([ - 'CREATED' => 'Crée', - 'TRANSFORMED' => 'Transformé', - 'REJECTED' => 'Rejeté', - ]); - }); - - it('lists all values keyed by case name', function (): void { - // THEN - expect(CampaignStatus::toValues())->toBe([ - 'CREATED' => 1, - 'TRANSFORMED' => 2, - 'REJECTED' => 3, - ]); - }); - - it('makes an instance from its backing value', function (): void { - // THEN - expect(CampaignStatus::make(1))->toBe(CampaignStatus::CREATED); - }); - - it('makes an instance from a label', function (): void { - // THEN - expect(CampaignStatus::make('Transformé', 'label'))->toBe(CampaignStatus::TRANSFORMED); - }); - - it('returns the same instance when already an enum case', function (): void { - // THEN - expect(CampaignStatus::make(CampaignStatus::REJECTED))->toBe(CampaignStatus::REJECTED); - }); - - it('returns null for an unknown value', function (): void { - // THEN - expect(CampaignStatus::make(99))->toBeNull(); - }); - - it('implements equals correctly', function (): void { - // THEN - expect(CampaignStatus::CREATED->equals(CampaignStatus::CREATED))->toBeTrue() - ->and(CampaignStatus::CREATED->equals(CampaignStatus::REJECTED))->toBeFalse() - ->and(CampaignStatus::CREATED->equals(null))->toBeFalse(); - }); - - it('serializes to json as its backing value', function (): void { - // THEN - expect(CampaignStatus::CREATED->jsonSerialize())->toBe(1) - ->and(json_encode(CampaignStatus::CREATED))->toBe('1'); + expect(CampaignStatus::CREATED->name)->toBe('CREATED'); }); }); diff --git a/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php b/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php index 2504339..f0d43b7 100644 --- a/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php +++ b/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php @@ -11,67 +11,12 @@ covers(CampaignType::class); describe('campaign type', function (): void { it('has the expected cases with their backing values', function (): void { // THEN - expect(CampaignType::CLUB->value())->toBe(1) - ->and(CampaignType::GAME->value())->toBe(2); + expect(CampaignType::CLUB->value)->toBe("club") + ->and(CampaignType::GAME->value)->toBe("game"); }); it('exposes its case name', function (): void { // THEN - expect(CampaignType::CLUB->name())->toBe('CLUB'); - }); - - it('returns the defined label', function (): void { - // THEN - expect(CampaignType::CLUB->label())->toBe('CLUB') - ->and(CampaignType::GAME->label())->toBe('GAME'); - }); - - it('lists all labels keyed by case name', function (): void { - // THEN - expect(CampaignType::toLabels())->toBe([ - 'CLUB' => 'CLUB', - 'GAME' => 'GAME', - ]); - }); - - it('lists all values keyed by case name', function (): void { - // THEN - expect(CampaignType::toValues())->toBe([ - 'CLUB' => 1, - 'GAME' => 2, - ]); - }); - - it('makes an instance from its backing value', function (): void { - // THEN - expect(CampaignType::make(1))->toBe(CampaignType::CLUB); - }); - - it('makes an instance from a label', function (): void { - // THEN - expect(CampaignType::make('GAME', 'label'))->toBe(CampaignType::GAME); - }); - - it('returns the same instance when already an enum case', function (): void { - // THEN - expect(CampaignType::make(CampaignType::CLUB))->toBe(CampaignType::CLUB); - }); - - it('returns null for an unknown value', function (): void { - // THEN - expect(CampaignType::make(99))->toBeNull(); - }); - - it('implements equals correctly', function (): void { - // THEN - expect(CampaignType::CLUB->equals(CampaignType::CLUB))->toBeTrue() - ->and(CampaignType::CLUB->equals(CampaignType::GAME))->toBeFalse() - ->and(CampaignType::CLUB->equals(null))->toBeFalse(); - }); - - it('serializes to json as its backing value', function (): void { - // THEN - expect(CampaignType::CLUB->jsonSerialize())->toBe(1) - ->and(json_encode(CampaignType::CLUB))->toBe('1'); + expect(CampaignType::CLUB->name)->toBe('CLUB'); }); }); diff --git a/tests/Unit/Domain/Enums/CountryTest.php b/tests/Unit/Domain/Enums/CountryTest.php index e9c49c6..3252171 100644 --- a/tests/Unit/Domain/Enums/CountryTest.php +++ b/tests/Unit/Domain/Enums/CountryTest.php @@ -11,71 +11,13 @@ covers(Country::class); describe('country', function (): void { it('has the expected cases with their backing values', function (): void { // THEN - expect(Country::FR->value())->toBe(1) - ->and(Country::BE->value())->toBe(2) - ->and(Country::LU->value())->toBe(3); + expect(Country::FR->value)->toBe("fr") + ->and(Country::BE->value)->toBe("be") + ->and(Country::LU->value)->toBe("lu"); }); 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'); + expect(Country::FR->name)->toBe('FR'); }); }); diff --git a/tests/Unit/Shared/Enums/CurrencyTest.php b/tests/Unit/Shared/Enums/CurrencyTest.php index 0f01dd3..1d0a2a3 100644 --- a/tests/Unit/Shared/Enums/CurrencyTest.php +++ b/tests/Unit/Shared/Enums/CurrencyTest.php @@ -11,56 +11,6 @@ covers(Currency::class); describe('currency', function (): void { it('has the expected cases with their backing values', function (): void { // THEN - expect(Currency::EUR->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(); + expect(Currency::EUR->value)->toBe("euro"); }); }); From eae419371366828b3963e4bc5675adef9cfcc3bd Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Thu, 24 Sep 2026 14:29:26 +0200 Subject: [PATCH 25/52] refactor: :recycle: replace trim with mb_trim --- src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php | 2 +- src/Domain/Models/ValueObjects/Campaign/CampaignCode.php | 2 +- src/Domain/Models/ValueObjects/Campaign/CampaignName.php | 2 +- .../Models/ValueObjects/CampaignRequest/Applicant.php | 4 ++-- .../Models/ValueObjects/CampaignRequest/RejectionReason.php | 2 +- .../Models/ValueObjects/Coupon/CouponLegalInformation.php | 2 +- src/Domain/Models/ValueObjects/Coupon/CouponProduct.php | 2 +- src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php | 6 +++--- src/Domain/Models/ValueObjects/CouponName.php | 2 +- src/Domain/Models/ValueObjects/CouponProductName.php | 2 +- src/Domain/Models/ValueObjects/GS1.php | 2 +- src/Domain/Models/ValueObjects/ProcessingCode.php | 2 +- src/Domain/Models/ValueObjects/Stock/StockAdjustment.php | 2 +- src/Domain/Models/ValueObjects/Tag/TagName.php | 2 +- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php b/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php index 483148f..ce1a70e 100644 --- a/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php +++ b/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php @@ -9,7 +9,7 @@ final readonly class ActorDisplayName public function __construct( public string $value, ) { - $trimmed = trim($value); + $trimmed = mb_trim($value); if ($trimmed === '') { throw new \DomainException('Display name cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php index b0eb68f..755e838 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php @@ -9,7 +9,7 @@ final readonly class CampaignCode public function __construct( public string $code, ) { - $trimmed = trim($code); + $trimmed = mb_trim($code); if ($trimmed === '') { throw new \DomainException('Campaign code cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignName.php b/src/Domain/Models/ValueObjects/Campaign/CampaignName.php index 46cc92f..67ecd4d 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignName.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignName.php @@ -9,7 +9,7 @@ final readonly class CampaignName public function __construct( public string $value, ) { - $trimmed = trim($value); + $trimmed = mb_trim($value); if ($trimmed === '') { throw new \DomainException('Campaign name cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/Applicant.php b/src/Domain/Models/ValueObjects/CampaignRequest/Applicant.php index 7375c56..f333fe9 100644 --- a/src/Domain/Models/ValueObjects/CampaignRequest/Applicant.php +++ b/src/Domain/Models/ValueObjects/CampaignRequest/Applicant.php @@ -13,7 +13,7 @@ final readonly class Applicant public string $lastname, public Email $email, ) { - $trimmedFirstname = trim($firstname); + $trimmedFirstname = mb_trim($firstname); if ($trimmedFirstname === '') { throw new \DomainException('Applicant firstname cannot be empty.'); @@ -23,7 +23,7 @@ final readonly class Applicant throw new \DomainException('Applicant firstname exceeds the maximum allowed length.'); } - $trimmedLastname = trim($lastname); + $trimmedLastname = mb_trim($lastname); if ($trimmedLastname === '') { throw new \DomainException('Applicant lastname cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php b/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php index 73e4658..601a519 100644 --- a/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php +++ b/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php @@ -9,7 +9,7 @@ final readonly class RejectionReason public function __construct( public string $value, ) { - $trimmed = trim($value); + $trimmed = mb_trim($value); if ($trimmed === '') { throw new \DomainException('Rejection reason cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php index 8a83105..9196c01 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php @@ -13,7 +13,7 @@ final readonly class CouponLegalInformation implements Equatable public string $legalText, public Country $forLocale ) { - $trimmed = trim($this->legalText); + $trimmed = mb_trim($this->legalText); if ($trimmed === '') { throw new \DomainException('Legal text cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php index d0bcdaf..c029a1e 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php @@ -15,7 +15,7 @@ final readonly class CouponProduct /** @var CouponProductInfo[] */ public array $labels, ) { - $trimmed = trim($this->name); + $trimmed = mb_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 8d2a70a..36a4971 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php @@ -15,19 +15,19 @@ final readonly class CouponProductInfo implements Equatable public string $validityText, public Country $forLocale ) { - $trimmedName = trim($this->name); + $trimmedName = mb_trim($this->name); if ($trimmedName === '') { throw new \DomainException('Product Info name cannot be empty.'); } - $trimmedRange = trim($this->range); + $trimmedRange = mb_trim($this->range); if ($trimmedRange === '') { throw new \DomainException('Product Info range cannot be empty.'); } - $trimmedValidityText = trim($this->validityText); + $trimmedValidityText = mb_trim($this->validityText); if ($trimmedValidityText === '') { throw new \DomainException('Product Info validity text cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/CouponName.php b/src/Domain/Models/ValueObjects/CouponName.php index 764da54..4793a82 100644 --- a/src/Domain/Models/ValueObjects/CouponName.php +++ b/src/Domain/Models/ValueObjects/CouponName.php @@ -9,7 +9,7 @@ final readonly class CouponName public function __construct( public string $value, ) { - $trimmed = trim($value); + $trimmed = mb_trim($value); if ($trimmed === '') { throw new \DomainException('Coupon name cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/CouponProductName.php b/src/Domain/Models/ValueObjects/CouponProductName.php index 3812226..c7ea495 100644 --- a/src/Domain/Models/ValueObjects/CouponProductName.php +++ b/src/Domain/Models/ValueObjects/CouponProductName.php @@ -9,7 +9,7 @@ final readonly class CouponProductName public function __construct( public string $value, ) { - $trimmed = trim($value); + $trimmed = mb_trim($value); if ($trimmed === '') { throw new \DomainException('Coupon product name cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/GS1.php b/src/Domain/Models/ValueObjects/GS1.php index 4950d8d..0b8ec74 100644 --- a/src/Domain/Models/ValueObjects/GS1.php +++ b/src/Domain/Models/ValueObjects/GS1.php @@ -13,7 +13,7 @@ final readonly class GS1 implements \Stringable, Equatable public string $value, public BarcodeFormat $format ) { - $trimmed = trim($this->value); + $trimmed = mb_trim($this->value); if ($trimmed === '') { throw new \DomainException('GS1 value cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/ProcessingCode.php b/src/Domain/Models/ValueObjects/ProcessingCode.php index f5d293b..6cf613c 100644 --- a/src/Domain/Models/ValueObjects/ProcessingCode.php +++ b/src/Domain/Models/ValueObjects/ProcessingCode.php @@ -11,7 +11,7 @@ final readonly class ProcessingCode implements \Stringable, Equatable public function __construct( public string $value, ) { - $trimmed = trim($this->value); + $trimmed = mb_trim($this->value); if ($trimmed === '') { throw new \DomainException('Processing Code value cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/Stock/StockAdjustment.php b/src/Domain/Models/ValueObjects/Stock/StockAdjustment.php index 6b5a767..1d59f00 100644 --- a/src/Domain/Models/ValueObjects/Stock/StockAdjustment.php +++ b/src/Domain/Models/ValueObjects/Stock/StockAdjustment.php @@ -18,7 +18,7 @@ final readonly class StockAdjustment throw new \DomainException('Stock Amount should be more than 0.'); } - $trimmed = trim($reason); + $trimmed = mb_trim($reason); if ($trimmed === '') { throw new \DomainException('Reason cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/Tag/TagName.php b/src/Domain/Models/ValueObjects/Tag/TagName.php index 10cc16e..9926ebf 100644 --- a/src/Domain/Models/ValueObjects/Tag/TagName.php +++ b/src/Domain/Models/ValueObjects/Tag/TagName.php @@ -9,7 +9,7 @@ final readonly class TagName public function __construct( public string $value, ) { - $trimmed = trim($value); + $trimmed = mb_trim($value); if ($trimmed === '') { throw new \DomainException('Tag name cannot be empty.'); From a58d60481466857400bbf3ca1a196b539f9f691e Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Thu, 24 Sep 2026 15:24:54 +0200 Subject: [PATCH 26/52] refactor: :recycle: replace trim with mb_trim --- src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php | 4 +--- src/Domain/Models/ValueObjects/Brand/BrandName.php | 2 +- src/Domain/Models/ValueObjects/Campaign/CampaignCode.php | 4 +--- src/Domain/Models/ValueObjects/Campaign/CampaignName.php | 4 +--- .../Models/ValueObjects/CampaignRequest/RejectionReason.php | 4 +--- .../Models/ValueObjects/Coupon/CouponLegalInformation.php | 4 +--- src/Domain/Models/ValueObjects/Coupon/CouponProduct.php | 4 +--- src/Domain/Models/ValueObjects/CouponName.php | 4 +--- src/Domain/Models/ValueObjects/CouponProductName.php | 4 +--- src/Domain/Models/ValueObjects/GS1.php | 4 +--- src/Domain/Models/ValueObjects/PicturePath.php | 2 +- src/Domain/Models/ValueObjects/ProcessingCode.php | 4 +--- src/Domain/Models/ValueObjects/Stock/StockAdjustment.php | 4 +--- src/Domain/Models/ValueObjects/Tag/TagName.php | 4 +--- src/Shared/DDD/Models/EntityId.php | 2 +- src/Shared/Helpers/DatesHelper.php | 2 +- src/Shared/ValueObjects/Email.php | 2 +- src/Shared/ValueObjects/Rcs.php | 2 +- 18 files changed, 18 insertions(+), 42 deletions(-) diff --git a/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php b/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php index ce1a70e..d18b739 100644 --- a/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php +++ b/src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php @@ -9,9 +9,7 @@ final readonly class ActorDisplayName public function __construct( public string $value, ) { - $trimmed = mb_trim($value); - - if ($trimmed === '') { + if (mb_trim($this->value) === '') { throw new \DomainException('Display name cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/Brand/BrandName.php b/src/Domain/Models/ValueObjects/Brand/BrandName.php index 3321d91..eeea0d1 100644 --- a/src/Domain/Models/ValueObjects/Brand/BrandName.php +++ b/src/Domain/Models/ValueObjects/Brand/BrandName.php @@ -9,7 +9,7 @@ final readonly class BrandName public function __construct( public string $value, ) { - $trimmed = trim($value); + $trimmed = mb_trim($value); if ($trimmed === '') { throw new \DomainException('Brand name cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php index 755e838..afe38a9 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php @@ -9,9 +9,7 @@ final readonly class CampaignCode public function __construct( public string $code, ) { - $trimmed = mb_trim($code); - - if ($trimmed === '') { + if (mb_trim($this->code) === '') { throw new \DomainException('Campaign code cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignName.php b/src/Domain/Models/ValueObjects/Campaign/CampaignName.php index 67ecd4d..9fd6b92 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignName.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignName.php @@ -9,9 +9,7 @@ final readonly class CampaignName public function __construct( public string $value, ) { - $trimmed = mb_trim($value); - - if ($trimmed === '') { + if (mb_trim($this->value) === '') { throw new \DomainException('Campaign name cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php b/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php index 601a519..c0bbe40 100644 --- a/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php +++ b/src/Domain/Models/ValueObjects/CampaignRequest/RejectionReason.php @@ -9,9 +9,7 @@ final readonly class RejectionReason public function __construct( public string $value, ) { - $trimmed = mb_trim($value); - - if ($trimmed === '') { + if (mb_trim($this->value) === '') { throw new \DomainException('Rejection reason cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php index 9196c01..24db4b1 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php @@ -13,9 +13,7 @@ final readonly class CouponLegalInformation implements Equatable public string $legalText, public Country $forLocale ) { - $trimmed = mb_trim($this->legalText); - - if ($trimmed === '') { + if (mb_trim($this->legalText) === '') { throw new \DomainException('Legal text cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php index c029a1e..f297569 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php @@ -15,9 +15,7 @@ final readonly class CouponProduct /** @var CouponProductInfo[] */ public array $labels, ) { - $trimmed = mb_trim($this->name); - - if ($trimmed === '') { + if (mb_trim($this->name) === '') { throw new \DomainException('Coupon Product name cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/CouponName.php b/src/Domain/Models/ValueObjects/CouponName.php index 4793a82..a681b49 100644 --- a/src/Domain/Models/ValueObjects/CouponName.php +++ b/src/Domain/Models/ValueObjects/CouponName.php @@ -9,9 +9,7 @@ final readonly class CouponName public function __construct( public string $value, ) { - $trimmed = mb_trim($value); - - if ($trimmed === '') { + if (mb_trim($this->value) === '') { throw new \DomainException('Coupon name cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/CouponProductName.php b/src/Domain/Models/ValueObjects/CouponProductName.php index c7ea495..826389d 100644 --- a/src/Domain/Models/ValueObjects/CouponProductName.php +++ b/src/Domain/Models/ValueObjects/CouponProductName.php @@ -9,9 +9,7 @@ final readonly class CouponProductName public function __construct( public string $value, ) { - $trimmed = mb_trim($value); - - if ($trimmed === '') { + if (mb_trim($this->value) === '') { throw new \DomainException('Coupon product name cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/GS1.php b/src/Domain/Models/ValueObjects/GS1.php index 0b8ec74..1f646a2 100644 --- a/src/Domain/Models/ValueObjects/GS1.php +++ b/src/Domain/Models/ValueObjects/GS1.php @@ -13,9 +13,7 @@ final readonly class GS1 implements \Stringable, Equatable public string $value, public BarcodeFormat $format ) { - $trimmed = mb_trim($this->value); - - if ($trimmed === '') { + if (mb_trim($this->value) === '') { throw new \DomainException('GS1 value cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/PicturePath.php b/src/Domain/Models/ValueObjects/PicturePath.php index b0844c4..a1b95ce 100644 --- a/src/Domain/Models/ValueObjects/PicturePath.php +++ b/src/Domain/Models/ValueObjects/PicturePath.php @@ -25,7 +25,7 @@ final readonly class PicturePath implements \Stringable, Equatable public function __construct( public string $value, ) { - $trimmed = trim($this->value); + $trimmed = mb_trim($this->value); if ($trimmed === '') { throw new \DomainException('Picture Path cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/ProcessingCode.php b/src/Domain/Models/ValueObjects/ProcessingCode.php index 6cf613c..97dd7ff 100644 --- a/src/Domain/Models/ValueObjects/ProcessingCode.php +++ b/src/Domain/Models/ValueObjects/ProcessingCode.php @@ -11,9 +11,7 @@ final readonly class ProcessingCode implements \Stringable, Equatable public function __construct( public string $value, ) { - $trimmed = mb_trim($this->value); - - if ($trimmed === '') { + if (mb_trim($this->value) === '') { throw new \DomainException('Processing Code value cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/Stock/StockAdjustment.php b/src/Domain/Models/ValueObjects/Stock/StockAdjustment.php index 1d59f00..9e3a67d 100644 --- a/src/Domain/Models/ValueObjects/Stock/StockAdjustment.php +++ b/src/Domain/Models/ValueObjects/Stock/StockAdjustment.php @@ -18,9 +18,7 @@ final readonly class StockAdjustment throw new \DomainException('Stock Amount should be more than 0.'); } - $trimmed = mb_trim($reason); - - if ($trimmed === '') { + if (mb_trim($this->reason) === '') { throw new \DomainException('Reason cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/Tag/TagName.php b/src/Domain/Models/ValueObjects/Tag/TagName.php index 9926ebf..dd78651 100644 --- a/src/Domain/Models/ValueObjects/Tag/TagName.php +++ b/src/Domain/Models/ValueObjects/Tag/TagName.php @@ -9,9 +9,7 @@ final readonly class TagName public function __construct( public string $value, ) { - $trimmed = mb_trim($value); - - if ($trimmed === '') { + if (mb_trim($this->value) === '') { throw new \DomainException('Tag name cannot be empty.'); } } diff --git a/src/Shared/DDD/Models/EntityId.php b/src/Shared/DDD/Models/EntityId.php index b776f46..537b407 100644 --- a/src/Shared/DDD/Models/EntityId.php +++ b/src/Shared/DDD/Models/EntityId.php @@ -9,7 +9,7 @@ abstract readonly class EntityId implements \Stringable, Equatable public function __construct( public string $value ) { - if (trim($value) === '') { + if (mb_trim($this->value) === '') { throw new \DomainException('EntityId value cannot be empty'); } } diff --git a/src/Shared/Helpers/DatesHelper.php b/src/Shared/Helpers/DatesHelper.php index 7b7ebfd..8c1a0ca 100644 --- a/src/Shared/Helpers/DatesHelper.php +++ b/src/Shared/Helpers/DatesHelper.php @@ -63,7 +63,7 @@ final class DatesHelper public static function parseDateTime(string $datetime): \DateTimeInterface { - $datetime = trim($datetime); + $datetime = mb_trim($datetime); if ($datetime === '') { throw InvalidDateFormatException::forDateString($datetime); diff --git a/src/Shared/ValueObjects/Email.php b/src/Shared/ValueObjects/Email.php index fba31ab..816b096 100644 --- a/src/Shared/ValueObjects/Email.php +++ b/src/Shared/ValueObjects/Email.php @@ -20,7 +20,7 @@ final readonly class Email implements \Stringable, Equatable public function __construct( string $value, ) { - $trimmed = trim($value); + $trimmed = mb_trim($value); if ($trimmed === '') { throw new \DomainException('Email cannot be empty.'); diff --git a/src/Shared/ValueObjects/Rcs.php b/src/Shared/ValueObjects/Rcs.php index bef5e2b..3f54e2d 100644 --- a/src/Shared/ValueObjects/Rcs.php +++ b/src/Shared/ValueObjects/Rcs.php @@ -21,7 +21,7 @@ final readonly class Rcs implements \Stringable, Equatable public function __construct( public string $value, ) { - $trimmed = trim($this->value); + $trimmed = mb_trim($this->value); if ($trimmed === '') { throw new \DomainException('Rcs text cannot be empty.'); From 59e4dbf2eecefd9b162e3ce53b42c134a54afbdd Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Fri, 25 Sep 2026 09:44:57 +0200 Subject: [PATCH 27/52] 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.'); }); }); From 6d7aecbd1d313c2e3f0123da181293e2dfc3a7fd Mon Sep 17 00:00:00 2001 From: K Muller Date: Fri, 25 Sep 2026 10:24:35 +0200 Subject: [PATCH 28/52] fix: :bug: put enum in vo folder / use global country enum --- .../Find/Output/CampaignRequestFound.php | 12 +- .../List/CampaignRequestListItem.php | 8 +- .../Reject/Output/CampaignRequestRejected.php | 4 +- src/Domain/Enums/BarcodeFormat.php | 6 +- src/Domain/Enums/Campaign/CampaignStatus.php | 12 - src/Domain/Enums/Campaign/CampaignType.php | 11 - src/Domain/Enums/Country.php | 580 +++++++++++++++++- .../UnknownCountryIsoCodeException.php | 20 + src/Domain/Models/Entities/Campaign.php | 8 +- .../Models/Entities/CampaignRequest.php | 26 +- .../CampaignRequestFilters.php | 3 +- .../CampaignRequest/CampaignRequestStatus.php | 12 + .../CampaignRequest/CampaignRequestType.php | 11 + .../Models/ValueObjects/CountryAuthorized.php | 46 ++ .../Coupon/CouponLegalInformation.php | 4 +- .../ValueObjects/Coupon/CouponProductInfo.php | 4 +- src/Shared/Enums/Currency.php | 2 +- src/Shared/ValueObjects/Money.php | 2 +- .../Find/FindCampaignRequestTest.php | 25 +- .../List/ListCampaignsRequestTest.php | 19 +- .../Reject/RejectCampaignRequestTest.php | 21 +- tests/Unit/Domain/Enums/BarcodeFormatTest.php | 6 +- .../Enums/Campaign/CampaignStatusTest.php | 12 +- .../Enums/Campaign/CampaignTypeTest.php | 10 +- tests/Unit/Domain/Enums/CountryTest.php | 8 +- .../Models/Entities/CampaignRequestTest.php | 69 ++- .../Domain/Models/Entities/CampaignTest.php | 15 +- .../Domain/Models/Entities/CouponTest.php | 38 +- .../CampaignRequestFiltersTest.php | 6 +- .../Models/ValueObjects/CountryAuthorized.php | 65 ++ .../Coupon/CouponLegalInformationTest.php | 5 +- .../Coupon/CouponProductInfoTest.php | 5 +- tests/Unit/Shared/Enums/CurrencyTest.php | 2 +- 33 files changed, 907 insertions(+), 170 deletions(-) delete mode 100644 src/Domain/Enums/Campaign/CampaignStatus.php delete mode 100644 src/Domain/Enums/Campaign/CampaignType.php create mode 100644 src/Domain/Exceptions/UnknownCountryIsoCodeException.php create mode 100644 src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestStatus.php create mode 100644 src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestType.php create mode 100644 src/Domain/Models/ValueObjects/CountryAuthorized.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php diff --git a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php index ecb675e..eb0d481 100644 --- a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php +++ b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php @@ -4,13 +4,13 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\Find\Output; -use Domain\Enums\Campaign\CampaignStatus; -use Domain\Enums\Campaign\CampaignType; -use Domain\Enums\Country; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\Entities\Brand; use Domain\Models\Entities\RequestedCoupon; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\PrintRules; final readonly class CampaignRequestFound implements Output @@ -18,11 +18,11 @@ final readonly class CampaignRequestFound implements Output public function __construct( public Brand $brand, public Applicant $applicant, - public Country $country, - public CampaignType $campaignType, + public CountryAuthorized $country, + public CampaignRequestType $campaignType, public ?PrintRules $printRules, public RequestedCoupon $requestedCoupon, - public CampaignStatus $status, + public CampaignRequestStatus $status, public ?RejectionReason $rejectionReason, public \DateTimeInterface $createdAt, public ?\DateTimeInterface $updatedAt, diff --git a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php index 6d622f4..ebfa33e 100644 --- a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php +++ b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\List; -use Domain\Enums\Campaign\CampaignStatus; -use Domain\Enums\Campaign\CampaignType; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; @@ -14,8 +14,8 @@ final readonly class CampaignRequestListItem public function __construct( public BrandCode $code, public Applicant $applicant, - public CampaignType $campaignType, - public CampaignStatus $status + public CampaignRequestType $campaignType, + public CampaignRequestStatus $status ) { } } diff --git a/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php b/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php index e27372f..ceca5c6 100644 --- a/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php +++ b/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\Reject\Output; -use Domain\Enums\Campaign\CampaignStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; @@ -12,7 +12,7 @@ final readonly class CampaignRequestRejected implements Output { public function __construct( public CampaignRequestId $id, - public CampaignStatus $status, + public CampaignRequestStatus $status, public RejectionReason $rejectionReason, public \DateTimeInterface $updatedAt, ) { diff --git a/src/Domain/Enums/BarcodeFormat.php b/src/Domain/Enums/BarcodeFormat.php index 5c625c6..f648d9d 100644 --- a/src/Domain/Enums/BarcodeFormat.php +++ b/src/Domain/Enums/BarcodeFormat.php @@ -6,7 +6,7 @@ namespace Domain\Enums; enum BarcodeFormat: string { - case EAN13 = "ean"; - case CODE128 = "code128"; - case GS1_128 = "gs1"; + case EAN13 = 'ean'; + case CODE128 = 'code128'; + case GS1_128 = 'gs1'; } diff --git a/src/Domain/Enums/Campaign/CampaignStatus.php b/src/Domain/Enums/Campaign/CampaignStatus.php deleted file mode 100644 index c617a78..0000000 --- a/src/Domain/Enums/Campaign/CampaignStatus.php +++ /dev/null @@ -1,12 +0,0 @@ -value => 'Afghanistan', + self::ALAND_ISLANDS->value => 'Åland Islands', + self::ALBANIA->value => 'Albania', + self::ALGERIA->value => 'Algeria', + self::AMERICAN_SAMOA->value => 'American Samoa', + self::ANDORRA->value => 'Andorra', + self::ANGOLA->value => 'Angola', + self::ANGUILLA->value => 'Anguilla', + self::ANTARCTICA->value => 'Antarctica', + self::ANTIGUA_AND_BARBUDA->value => 'Antigua and Barbuda', + self::ARGENTINA->value => 'Argentina', + self::ARMENIA->value => 'Armenia', + self::ARUBA->value => 'Aruba', + self::AUSTRALIA->value => 'Australia', + self::AUSTRIA->value => 'Austria', + self::AZERBAIJAN->value => 'Azerbaijan', + self::BAHRAIN->value => 'Bahrain', + self::BAHAMAS->value => 'Bahamas', + self::BANGLADESH->value => 'Bangladesh', + self::BARBADOS->value => 'Barbados', + self::BELARUS->value => 'Belarus', + self::BELGIUM->value => 'Belgium', + self::BELIZE->value => 'Belize', + self::BENIN->value => 'Benin', + self::BERMUDA->value => 'Bermuda', + self::BHUTAN->value => 'Bhutan', + self::BOLIVIA_PLURINATIONAL_STATE_OF->value => 'Bolivia, Plurinational State of', + self::BONAIRE_SINT_EUSTATIUS_AND_SABA->value => 'Bonaire, Sint Eustatius and Saba', + self::BOSNIA_AND_HERZEGOVINA->value => 'Bosnia and Herzegovina', + self::BOTSWANA->value => 'Botswana', + self::BOUVET_ISLAND->value => 'Bouvet Island', + self::BRAZIL->value => 'Brazil', + self::BRITISH_INDIAN_OCEAN_TERRITORY->value => 'British Indian Ocean Territory', + self::BRUNEI_DARUSSALAM->value => 'Brunei Darussalam', + self::BULGARIA->value => 'Bulgaria', + self::BURKINA_FASO->value => 'Burkina Faso', + self::BURUNDI->value => 'Burundi', + self::CAMBODIA->value => 'Cambodia', + self::CAMEROON->value => 'Cameroon', + self::CANADA->value => 'Canada', + self::CAPE_VERDE->value => 'Cape Verde', + self::CAYMAN_ISLANDS->value => 'Cayman Islands', + self::CENTRAL_AFRICAN_REPUBLIC->value => 'Central African Republic', + self::CHAD->value => 'Chad', + self::CHILE->value => 'Chile', + self::CHINA->value => 'China', + self::CHRISTMAS_ISLAND->value => 'Christmas Island', + self::COCOS_KEELING_ISLANDS->value => 'Cocos (Keeling) Islands', + self::COLOMBIA->value => 'Colombia', + self::COMOROS->value => 'Comoros', + self::CONGO->value => 'Congo', + self::CONGO_THE_DEMOCRATIC_REPUBLIC_OF_THE->value => 'Congo, the Democratic Republic of the', + self::COOK_ISLANDS->value => 'Cook Islands', + self::COSTA_RICA->value => 'Costa Rica', + self::COTE_DIVOIRE->value => 'Côte d\'Ivoire', + self::CROATIA->value => 'Croatia', + self::CUBA->value => 'Cuba', + self::CURACAO->value => 'Curaçao', + self::CYPRUS->value => 'Cyprus', + self::CZECH_REPUBLIC->value => 'Czech Republic', + self::DENMARK->value => 'Denmark', + self::DJIBOUTI->value => 'Djibouti', + self::DOMINICA->value => 'Dominica', + self::DOMINICAN_REPUBLIC->value => 'Dominican Republic', + self::ECUADOR->value => 'Ecuador', + self::EGYPT->value => 'Egypt', + self::EL_SALVADOR->value => 'El Salvador', + self::EQUATORIAL_GUINEA->value => 'Equatorial Guinea', + self::ERITREA->value => 'Eritrea', + self::ESTONIA->value => 'Estonia', + self::ETHIOPIA->value => 'Ethiopia', + self::FALKLAND_ISLANDS_MALVINAS->value => 'Falkland Islands (Malvinas)', + self::FAROE_ISLANDS->value => 'Faroe Islands', + self::FIJI->value => 'Fiji', + self::FINLAND->value => 'Finland', + self::FRANCE->value => 'France', + self::FRENCH_GUIANA->value => 'French Guiana', + self::FRENCH_POLYNESIA->value => 'French Polynesia', + self::FRENCH_SOUTHERN_TERRITORIES->value => 'French Southern Territories', + self::GABON->value => 'Gabon', + self::GAMBIA->value => 'Gambia', + self::GEORGIA->value => 'Georgia', + self::GERMANY->value => 'Germany', + self::GHANA->value => 'Ghana', + self::GIBRALTAR->value => 'Gibraltar', + self::GREECE->value => 'Greece', + self::GREENLAND->value => 'Greenland', + self::GRENADA->value => 'Grenada', + self::GUADELOUPE->value => 'Guadeloupe', + self::GUAM->value => 'Guam', + self::GUATEMALA->value => 'Guatemala', + self::GUERNSEY->value => 'Guernsey', + self::GUINEA->value => 'Guinea', + self::GUINEA_BISSAU->value => 'GuineaBissau', + self::GUYANA->value => 'Guyana', + self::HAITI->value => 'Haiti', + self::HEARD_ISLAND_AND_MCDONALD_ISLANDS->value => 'Heard Island and McDonald Islands', + self::HOLY_SEE_VATICAN_CITY_STATE->value => 'Holy See (Vatican City State)', + self::HONDURAS->value => 'Honduras', + self::HONG_KONG->value => 'Hong Kong', + self::HUNGARY->value => 'Hungary', + self::ICELAND->value => 'Iceland', + self::INDIA->value => 'India', + self::INDONESIA->value => 'Indonesia', + self::IRAN_ISLAMIC_REPUBLIC_OF->value => 'Iran, Islamic Republic of', + self::IRAQ->value => 'Iraq', + self::IRELAND->value => 'Ireland', + self::ISLE_OF_MAN->value => 'Isle of Man', + self::ISRAEL->value => 'Israel', + self::ITALY->value => 'Italy', + self::JAMAICA->value => 'Jamaica', + self::JAPAN->value => 'Japan', + self::JERSEY->value => 'Jersey', + self::JORDAN->value => 'Jordan', + self::KAZAKHSTAN->value => 'Kazakhstan', + self::KENYA->value => 'Kenya', + self::KIRIBATI->value => 'Kiribati', + self::KOREA_DEMOCRATIC_PEOPLES_REPUBLIC_OF->value => 'Korea, Democratic People\'s Republic of', + self::KOREA_REPUBLIC_OF->value => 'Korea, Republic of', + self::KUWAIT->value => 'Kuwait', + self::KYRGYZSTAN->value => 'Kyrgyzstan', + self::LAO_PEOPLES_DEMOCRATIC_REPUBLIC->value => 'Lao People\'s Democratic Republic', + self::LATVIA->value => 'Latvia', + self::LEBANON->value => 'Lebanon', + self::LESOTHO->value => 'Lesotho', + self::LIBERIA->value => 'Liberia', + self::LIBYA->value => 'Libya', + self::LIECHTENSTEIN->value => 'Liechtenstein', + self::LITHUANIA->value => 'Lithuania', + self::LUXEMBOURG->value => 'Luxembourg', + self::MACAO->value => 'Macao', + self::MACEDONIA_THE_FORMER_YUGOSLAV_REPUBLIC_OF->value => 'Macedonia, the Former Yugoslav Republic of', + self::MADAGASCAR->value => 'Madagascar', + self::MALAWI->value => 'Malawi', + self::MALAYSIA->value => 'Malaysia', + self::MALDIVES->value => 'Maldives', + self::MALI->value => 'Mali', + self::MALTA->value => 'Malta', + self::MARSHALL_ISLANDS->value => 'Marshall Islands', + self::MARTINIQUE->value => 'Martinique', + self::MAURITANIA->value => 'Mauritania', + self::MAURITIUS->value => 'Mauritius', + self::MAYOTTE->value => 'Mayotte', + self::MEXICO->value => 'Mexico', + self::MICRONESIA_FEDERATED_STATES_OF->value => 'Micronesia, Federated States of', + self::MOLDOVA_REPUBLIC_OF->value => 'Moldova, Republic of', + self::MONACO->value => 'Monaco', + self::MONGOLIA->value => 'Mongolia', + self::MONTENEGRO->value => 'Montenegro', + self::MONTSERRAT->value => 'Montserrat', + self::MOROCCO->value => 'Morocco', + self::MOZAMBIQUE->value => 'Mozambique', + self::MYANMAR->value => 'Myanmar', + self::NAMIBIA->value => 'Namibia', + self::NAURU->value => 'Nauru', + self::NEPAL->value => 'Nepal', + self::NETHERLANDS->value => 'Netherlands', + self::NEW_CALEDONIA->value => 'New Caledonia', + self::NEW_ZEALAND->value => 'New Zealand', + self::NICARAGUA->value => 'Nicaragua', + self::NIGER->value => 'Niger', + self::NIGERIA->value => 'Nigeria', + self::NIUE->value => 'Niue', + self::NORFOLK_ISLAND->value => 'Norfolk Island', + self::NORTHERN_MARIANA_ISLANDS->value => 'Northern Mariana Islands', + self::NORWAY->value => 'Norway', + self::OMAN->value => 'Oman', + self::PAKISTAN->value => 'Pakistan', + self::PALAU->value => 'Palau', + self::PALESTINE_STATE_OF->value => 'Palestine, State of', + self::PANAMA->value => 'Panama', + self::PAPUA_NEW_GUINEA->value => 'Papua New Guinea', + self::PARAGUAY->value => 'Paraguay', + self::PERU->value => 'Peru', + self::PHILIPPINES->value => 'Philippines', + self::PITCAIRN->value => 'Pitcairn', + self::POLAND->value => 'Poland', + self::PORTUGAL->value => 'Portugal', + self::PUERTO_RICO->value => 'Puerto Rico', + self::QATAR->value => 'Qatar', + self::REUNION->value => 'Réunion', + self::ROMANIA->value => 'Romania', + self::RUSSIAN_FEDERATION->value => 'Russian Federation', + self::RWANDA->value => 'Rwanda', + self::SAINT_BARTHELEMY->value => 'Saint Barthélemy', + self::SAINT_HELENA_ASCENSION_AND_TRISTAN_DA_CUNHA->value => 'Saint Helena, Ascension and Tristan da Cunha', + self::SAINT_KITTS_AND_NEVIS->value => 'Saint Kitts and Nevis', + self::SAINT_LUCIA->value => 'Saint Lucia', + self::SAINT_MARTIN_FRENCH_PART->value => 'Saint Martin (French part)', + self::SAINT_PIERRE_AND_MIQUELON->value => 'Saint Pierre and Miquelon', + self::SAINT_VINCENT_AND_THE_GRENADINES->value => 'Saint Vincent and the Grenadines', + self::SAMOA->value => 'Samoa', + self::SAN_MARINO->value => 'San Marino', + self::SAO_TOME_AND_PRINCIPE->value => 'Sao Tome and Principe', + self::SAUDI_ARABIA->value => 'Saudi Arabia', + self::SENEGAL->value => 'Senegal', + self::SERBIA->value => 'Serbia', + self::SEYCHELLES->value => 'Seychelles', + self::SIERRA_LEONE->value => 'Sierra Leone', + self::SINGAPORE->value => 'Singapore', + self::SINT_MAARTEN_DUTCH_PART->value => 'Sint Maarten (Dutch part)', + self::SLOVAKIA->value => 'Slovakia', + self::SLOVENIA->value => 'Slovenia', + self::SOLOMON_ISLANDS->value => 'Solomon Islands', + self::SOMALIA->value => 'Somalia', + self::SOUTH_AFRICA->value => 'South Africa', + self::SOUTH_GEORGIA_AND_THE_SOUTH_SANDWICH_ISLANDS->value => 'South Georgia and the South Sandwich Islands', + self::SOUTH_SUDAN->value => 'South Sudan', + self::SPAIN->value => 'Spain', + self::SRI_LANKA->value => 'Sri Lanka', + self::SUDAN->value => 'Sudan', + self::SURINAME->value => 'Suriname', + self::SVALBARD_AND_JAN_MAYEN->value => 'Svalbard and Jan Mayen', + self::SWAZILAND->value => 'Swaziland', + self::SWEDEN->value => 'Sweden', + self::SWITZERLAND->value => 'Switzerland', + self::SYRIAN_ARAB_REPUBLIC->value => 'Syrian Arab Republic', + self::TAIWAN_PROVINCE_OF_CHINA->value => 'Taiwan, Province of China', + self::TAJIKISTAN->value => 'Tajikistan', + self::TANZANIA_UNITED_REPUBLIC_OF->value => 'Tanzania, United Republic of', + self::THAILAND->value => 'Thailand', + self::TIMOR_LESTE->value => 'TimorLeste', + self::TOGO->value => 'Togo', + self::TOKELAU->value => 'Tokelau', + self::TONGA->value => 'Tonga', + self::TRINIDAD_AND_TOBAGO->value => 'Trinidad and Tobago', + self::TUNISIA->value => 'Tunisia', + self::TURKEY->value => 'Turkey', + self::TURKMENISTAN->value => 'Turkmenistan', + self::TURKS_AND_CAICOS_ISLANDS->value => 'Turks and Caicos Islands', + self::TUVALU->value => 'Tuvalu', + self::UGANDA->value => 'Uganda', + self::UKRAINE->value => 'Ukraine', + self::UNITED_ARAB_EMIRATES->value => 'United Arab Emirates', + self::UNITED_KINGDOM->value => 'United Kingdom', + self::UNITED_STATES->value => 'United States', + self::UNITED_STATES_MINOR_OUTLYING_ISLANDS->value => 'United States Minor Outlying Islands', + self::URUGUAY->value => 'Uruguay', + self::UZBEKISTAN->value => 'Uzbekistan', + self::VANUATU->value => 'Vanuatu', + self::VENEZUELA_BOLIVARIAN_REPUBLIC_OF->value => 'Venezuela, Bolivarian Republic of', + self::VIET_NAM->value => 'Viet Nam', + self::VIRGIN_ISLANDS_BRITISH->value => 'Virgin Islands, British', + self::VIRGIN_ISLANDS_US->value => 'Virgin Islands, U.S.', + self::WALLIS_AND_FUTUNA->value => 'Wallis and Futuna', + self::WESTERN_SAHARA->value => 'Western Sahara', + self::YEMEN->value => 'Yemen', + self::ZAMBIA->value => 'Zambia', + self::ZIMBABWE->value => 'Zimbabwe', + ]; + /** + * Custom added ISOCODES definition + */ + private const ISOCODES = [ + self::FRANCE->name => 'FR', + self::GUADELOUPE->name => 'GSP', + self::MARTINIQUE->name => 'MQ', + self::FRENCH_GUIANA->name => 'GF', + self::REUNION->name => 'RE', + self::SAINT_PIERRE_AND_MIQUELON->name => 'PM', + self::MAYOTTE->name => 'YT', + self::SAINT_BARTHELEMY->name => 'BL', + self::SAINT_MARTIN_FRENCH_PART->name => 'MF', + self::WALLIS_AND_FUTUNA->name => 'WF', + self::FRENCH_POLYNESIA->name => 'PF', + self::NEW_CALEDONIA->name => 'NC', + ]; + + /** + * Custom added Numerical ISOCODES definition + */ + private const ISOCODES_NUM = [ + self::FRANCE->name => 250, + self::GUADELOUPE->name => 312, + self::MARTINIQUE->name => 474, + self::FRENCH_GUIANA->name => 254, + self::REUNION->name => 638, + self::SAINT_PIERRE_AND_MIQUELON->name => 666, + self::MAYOTTE->name => 175, + self::SAINT_BARTHELEMY->name => 652, + self::SAINT_MARTIN_FRENCH_PART->name => 663, + self::WALLIS_AND_FUTUNA->name => 876, + self::FRENCH_POLYNESIA->name => 258, + self::NEW_CALEDONIA->name => 540, + ]; + + /** + * Get ISOCODE + */ + public function iso(): string + { + return self::ISOCODES[$this->name] ?? throw UnknownCountryIsoCodeException::fromCountry($this); + } + + /** + * Get ISOCODE NUM + */ + public function isoNum(): int + { + return self::ISOCODES_NUM[$this->name] ?? throw UnknownCountryIsoCodeException::fromCountry($this); + } + + /** + * Get ISOCODE + */ + public function isoCode(): ?string + { + return self::ISOCODES[$this->name] ?? null; + } + + /** + * Get enum ISOCODES + * + * @return array + */ + public static function toIsoCodes(): array + { + return self::ISOCODES; + } } diff --git a/src/Domain/Exceptions/UnknownCountryIsoCodeException.php b/src/Domain/Exceptions/UnknownCountryIsoCodeException.php new file mode 100644 index 0000000..6b566a3 --- /dev/null +++ b/src/Domain/Exceptions/UnknownCountryIsoCodeException.php @@ -0,0 +1,20 @@ +name}'"); + } +} diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php index 4030a0e..f517879 100644 --- a/src/Domain/Models/Entities/Campaign.php +++ b/src/Domain/Models/Entities/Campaign.php @@ -4,12 +4,12 @@ declare(strict_types=1); namespace Domain\Models\Entities; -use Domain\Enums\Country; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; +use Domain\Models\ValueObjects\CountryAuthorized; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; @@ -25,7 +25,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier public CampaignName $name, public bool $publication, public bool $printAllAtOnce, - public Country $availableIn, + public CountryAuthorized $availableIn, /** @var array $coupons */ public array $coupons, public BrandCode $brand, @@ -62,7 +62,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier CampaignName $name, bool $publication, bool $printAllAtOnce, - Country $availableIn, + CountryAuthorized $availableIn, /** @var array $coupons */ array $coupons, BrandCode $brand, @@ -92,7 +92,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier CampaignName $name, bool $publication, bool $printAllAtOnce, - Country $availableIn, + CountryAuthorized $availableIn, array $coupons, BrandCode $brand, ?\DateTimeInterface $updatedAt = null, diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index bdf1de0..7858146 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -4,13 +4,13 @@ declare(strict_types=1); namespace Domain\Models\Entities; -use Domain\Enums\Campaign\CampaignStatus; -use Domain\Enums\Campaign\CampaignType; -use Domain\Enums\Country; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\PrintRules; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; @@ -25,19 +25,19 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent public CampaignRequestId $id, public BrandCode $code, public Applicant $applicant, - public Country $country, - public CampaignType $campaignType, + public CountryAuthorized $country, + public CampaignRequestType $campaignType, public ?PrintRules $printRules, public RequestedCoupon $requestedCoupon, - public CampaignStatus $status, + public CampaignRequestStatus $status, public ?RejectionReason $rejectionReason = null, public ?\DateTimeInterface $updatedAt = null, ) { - if ($this->status === CampaignStatus::REJECTED && ! $this->rejectionReason instanceof RejectionReason) { + if ($this->status === CampaignRequestStatus::REJECTED && ! $this->rejectionReason instanceof RejectionReason) { throw new \DomainException('A rejection reason is required when the campaign request is rejected.'); } - if ($this->status !== CampaignStatus::REJECTED && $this->rejectionReason instanceof RejectionReason) { + if ($this->status !== CampaignRequestStatus::REJECTED && $this->rejectionReason instanceof RejectionReason) { throw new \DomainException('A rejection reason can only be set when the campaign request is rejected.'); } } @@ -62,11 +62,11 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent CampaignRequestId $id, BrandCode $code, Applicant $applicant, - Country $country, - CampaignType $campaignType, + CountryAuthorized $country, + CampaignRequestType $campaignType, ?PrintRules $printRules, RequestedCoupon $requestedCoupon, - CampaignStatus $status, + CampaignRequestStatus $status, ?RejectionReason $rejectionReason = null, ?\DateTimeInterface $updatedAt = null, ): self { @@ -86,7 +86,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent public function reject(RejectionReason $reason): self { - if ($this->status !== CampaignStatus::CREATED) { + if ($this->status !== CampaignRequestStatus::CREATED) { throw new \DomainException('Only a created campaign request can be rejected.'); } @@ -98,7 +98,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent campaignType: $this->campaignType, printRules: $this->printRules, requestedCoupon: $this->requestedCoupon, - status: CampaignStatus::REJECTED, + status: CampaignRequestStatus::REJECTED, rejectionReason: $reason, updatedAt: DatesHelper::now(), ); diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php index e60445d..5978375 100644 --- a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php +++ b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php @@ -4,14 +4,13 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\CampaignRequest; -use Domain\Enums\Campaign\CampaignStatus; use Domain\Models\ValueObjects\Brand\BrandCode; final readonly class CampaignRequestFilters { public function __construct( public ?BrandCode $brandCode = null, - public ?CampaignStatus $status = null, + public ?CampaignRequestStatus $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/CampaignRequestStatus.php b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestStatus.php new file mode 100644 index 0000000..0907efb --- /dev/null +++ b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestStatus.php @@ -0,0 +1,12 @@ + */ + private const array AUTHORIZED = [ + Country::FRANCE, + Country::BELGIUM, + Country::LUXEMBOURG, + ]; + + public function __construct( + public Country $country, + ) { + if (! in_array($this->country, self::AUTHORIZED, true)) { + throw new \DomainException(sprintf('Country "%s" is not authorized.', $this->country->name)); + } + } + + /** + * @return list + */ + public static function authorizedCountries(): array + { + return self::AUTHORIZED; + } + + #[Override] + public function equals(mixed $other): bool + { + return $other instanceof self && $this->country === $other->country; + } + + public function __toString(): string + { + return $this->country->value; + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php index 24db4b1..0a2a344 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Coupon; -use Domain\Enums\Country; +use Domain\Models\ValueObjects\CountryAuthorized; use Shared\DDD\Models\Equatable; final readonly class CouponLegalInformation implements Equatable { public function __construct( public string $legalText, - public Country $forLocale + public CountryAuthorized $forLocale ) { if (mb_trim($this->legalText) === '') { throw new \DomainException('Legal text cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php index 36a4971..853343f 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Coupon; -use Domain\Enums\Country; +use Domain\Models\ValueObjects\CountryAuthorized; use Shared\DDD\Models\Equatable; final readonly class CouponProductInfo implements Equatable @@ -13,7 +13,7 @@ final readonly class CouponProductInfo implements Equatable public string $name, public string $range, public string $validityText, - public Country $forLocale + public CountryAuthorized $forLocale ) { $trimmedName = mb_trim($this->name); diff --git a/src/Shared/Enums/Currency.php b/src/Shared/Enums/Currency.php index 93b8181..08fda79 100644 --- a/src/Shared/Enums/Currency.php +++ b/src/Shared/Enums/Currency.php @@ -6,5 +6,5 @@ namespace Shared\Enums; enum Currency: string { - case EUR = "euro"; + case EUR = 'euro'; } diff --git a/src/Shared/ValueObjects/Money.php b/src/Shared/ValueObjects/Money.php index f70521b..a23553d 100644 --- a/src/Shared/ValueObjects/Money.php +++ b/src/Shared/ValueObjects/Money.php @@ -32,7 +32,7 @@ final readonly class Money implements \Stringable, Equatable { return $other instanceof self && $this->currency->value === $other->currency->value - && $this->value === $other->value; + && $this->value === $other->value; } public function __toString(): string diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 62dc9b8..9f3667d 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -10,9 +10,9 @@ use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestFound; use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestNotFound; use Application\UseCases\CampaignRequest\Find\Output\Output; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Campaign\CampaignStatus; -use Domain\Enums\Campaign\CampaignType; use Domain\Enums\Country; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Exceptions\BrandNotFoundException; use Domain\Models\Entities\Brand; use Domain\Models\Entities\CampaignRequest; @@ -23,6 +23,7 @@ use Domain\Models\ValueObjects\Brand\BrandName; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; @@ -70,11 +71,11 @@ describe('find campaign request use case', function (): void { $id, $code, new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::CREATED, + CampaignRequestStatus::CREATED, ); $brand = Brand::hydrate( BrandId::generate(), @@ -117,11 +118,11 @@ describe('find campaign request use case', function (): void { $id, $code, new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::REJECTED, + CampaignRequestStatus::REJECTED, $rejectionReason, ); @@ -147,7 +148,7 @@ describe('find campaign request use case', function (): void { // THEN expect($result)->toBeInstanceOf(CampaignRequestFound::class) - ->and($result->status)->toBe(CampaignStatus::REJECTED) + ->and($result->status)->toBe(CampaignRequestStatus::REJECTED) ->and($result->rejectionReason)->toBe($rejectionReason); }); @@ -178,11 +179,11 @@ describe('find campaign request use case', function (): void { $id, $code, new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::CREATED, + CampaignRequestStatus::CREATED, ); $repository = \Mockery::mock(CampaignRequestRepository::class); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 5a06de4..8775d10 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\List; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Campaign\CampaignStatus; -use Domain\Enums\Campaign\CampaignType; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\Entities\RequestedCoupon; @@ -17,6 +17,7 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortClause; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; @@ -77,21 +78,21 @@ describe('list campaigns request use case', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->couponA, - CampaignStatus::CREATED, + CampaignRequestStatus::CREATED, ); $campaignRequest2 = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA02'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->couponB, - CampaignStatus::CREATED, + CampaignRequestStatus::CREATED, ); $paginated = new Paginated($pagination, [$campaignRequest1, $campaignRequest2], 2); @@ -101,7 +102,7 @@ describe('list campaigns request use case', function (): void { new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending) ] ); - $filters = new CampaignRequestFilters($campaignRequest2->code, CampaignStatus::CREATED); + $filters = new CampaignRequestFilters($campaignRequest2->code, CampaignRequestStatus::CREATED); $input = new Input(pagination: $pagination, sort: $sort, filters: $filters); $repository = \Mockery::mock(CampaignRequestRepository::class); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 23c3b30..7581f27 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -11,8 +11,8 @@ use Application\UseCases\CampaignRequest\Reject\Output\Output; use Application\UseCases\CampaignRequest\Reject\RejectCampaignRequest; use Carbon\CarbonImmutable; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Campaign\CampaignStatus; -use Domain\Enums\Campaign\CampaignType; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\Entities\RequestedCoupon; @@ -20,6 +20,7 @@ use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; @@ -69,18 +70,18 @@ describe('reject campaign request use case', function (): void { $id, new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::CREATED, + CampaignRequestStatus::CREATED, ); $reason = new RejectionReason('Missing legal documents.'); $repository = \Mockery::mock(CampaignRequestRepository::class); $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); $repository->shouldReceive('save')->once()->with(\Mockery::on( - fn (CampaignRequest $saved): bool => $saved->status === CampaignStatus::REJECTED + fn (CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED && $saved->rejectionReason === $reason && $saved->updatedAt !== null, )); @@ -93,7 +94,7 @@ describe('reject campaign request use case', function (): void { // THEN expect($result)->toBeInstanceOf(CampaignRequestRejected::class) ->and($result->id)->toBe($id) - ->and($result->status)->toBe(CampaignStatus::REJECTED) + ->and($result->status)->toBe(CampaignRequestStatus::REJECTED) ->and($result->rejectionReason)->toBe($reason) ->and($result->updatedAt->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00'); }); @@ -122,11 +123,11 @@ describe('reject campaign request use case', function (): void { $id, new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::TRANSFORMED, + CampaignRequestStatus::TRANSFORMED, ); $repository = \Mockery::mock(CampaignRequestRepository::class); diff --git a/tests/Unit/Domain/Enums/BarcodeFormatTest.php b/tests/Unit/Domain/Enums/BarcodeFormatTest.php index e91bf1d..d1af8e0 100644 --- a/tests/Unit/Domain/Enums/BarcodeFormatTest.php +++ b/tests/Unit/Domain/Enums/BarcodeFormatTest.php @@ -11,9 +11,9 @@ covers(BarcodeFormat::class); describe('barcode format', function (): void { it('has the expected cases with their backing values', function (): void { // THEN - expect(BarcodeFormat::EAN13->value)->toBe("ean") - ->and(BarcodeFormat::CODE128->value)->toBe("code128") - ->and(BarcodeFormat::GS1_128->value)->toBe("gs1"); + expect(BarcodeFormat::EAN13->value)->toBe('ean') + ->and(BarcodeFormat::CODE128->value)->toBe('code128') + ->and(BarcodeFormat::GS1_128->value)->toBe('gs1'); }); it('exposes its case name', function (): void { diff --git a/tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php b/tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php index 259264f..bcf43f5 100644 --- a/tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php +++ b/tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php @@ -4,20 +4,20 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Enums\Campaign; -use Domain\Enums\Campaign\CampaignStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; -covers(CampaignStatus::class); +covers(CampaignRequestStatus::class); describe('campaign status', function (): void { it('has the expected cases with their backing values', function (): void { // THEN - expect(CampaignStatus::CREATED->value)->toBe("created") - ->and(CampaignStatus::TRANSFORMED->value)->toBe("transformed") - ->and(CampaignStatus::REJECTED->value)->toBe("rejected"); + expect(CampaignRequestStatus::CREATED->value)->toBe('created') + ->and(CampaignRequestStatus::TRANSFORMED->value)->toBe('transformed') + ->and(CampaignRequestStatus::REJECTED->value)->toBe('rejected'); }); it('exposes its case name', function (): void { // THEN - expect(CampaignStatus::CREATED->name)->toBe('CREATED'); + expect(CampaignRequestStatus::CREATED->name)->toBe('CREATED'); }); }); diff --git a/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php b/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php index f0d43b7..737fccd 100644 --- a/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php +++ b/tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php @@ -4,19 +4,19 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Enums\Campaign; -use Domain\Enums\Campaign\CampaignType; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; -covers(CampaignType::class); +covers(CampaignRequestType::class); describe('campaign type', function (): void { it('has the expected cases with their backing values', function (): void { // THEN - expect(CampaignType::CLUB->value)->toBe("club") - ->and(CampaignType::GAME->value)->toBe("game"); + expect(CampaignRequestType::CLUB->value)->toBe('club') + ->and(CampaignRequestType::GAME->value)->toBe('game'); }); it('exposes its case name', function (): void { // THEN - expect(CampaignType::CLUB->name)->toBe('CLUB'); + expect(CampaignRequestType::CLUB->name)->toBe('CLUB'); }); }); diff --git a/tests/Unit/Domain/Enums/CountryTest.php b/tests/Unit/Domain/Enums/CountryTest.php index 3252171..5445fdc 100644 --- a/tests/Unit/Domain/Enums/CountryTest.php +++ b/tests/Unit/Domain/Enums/CountryTest.php @@ -11,13 +11,13 @@ covers(Country::class); describe('country', function (): void { it('has the expected cases with their backing values', function (): void { // THEN - expect(Country::FR->value)->toBe("fr") - ->and(Country::BE->value)->toBe("be") - ->and(Country::LU->value)->toBe("lu"); + expect(Country::FRANCE->value)->toBe('france') + ->and(Country::BELGIUM->value)->toBe('belgium') + ->and(Country::LUXEMBOURG->value)->toBe('luxembourg'); }); it('exposes its case name', function (): void { // THEN - expect(Country::FR->name)->toBe('FR'); + expect(Country::FRANCE->name)->toBe('FRANCE'); }); }); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 94e6262..0cb2349 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Campaign\CampaignStatus; -use Domain\Enums\Campaign\CampaignType; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\Entities\RequestedCoupon; @@ -31,6 +31,7 @@ use Shared\ValueObjects\DateTime; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; use Carbon\CarbonImmutable; +use Domain\Models\ValueObjects\CountryAuthorized; covers(CampaignRequest::class); @@ -60,11 +61,11 @@ describe('campaign request', function (): void { $id = CampaignRequestId::generate(); $code = new BrandCode('AAA01'); $applicant = new Applicant('John', 'Doe', new Email('john.doe@example.com')); - $country = Country::FR; - $campaignType = CampaignType::CLUB; + $country = new CountryAuthorized(Country::FRANCE); + $campaignType = CampaignRequestType::CLUB; $printRules = new PrintRules(maxPerCustomer: 3); $requestedCoupon = $this->requestedCoupon; - $status = CampaignStatus::CREATED; + $status = CampaignRequestStatus::CREATED; // WHEN $campaignRequest = CampaignRequest::hydrate( @@ -97,11 +98,11 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, null, $this->requestedCoupon, - CampaignStatus::CREATED, + CampaignRequestStatus::CREATED, ); // THEN @@ -117,11 +118,11 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::CREATED, + CampaignRequestStatus::CREATED, null, $updatedAt, ); @@ -140,16 +141,16 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::REJECTED, + CampaignRequestStatus::REJECTED, $rejectionReason, ); // THEN - expect($campaignRequest->status)->toBe(CampaignStatus::REJECTED) + expect($campaignRequest->status)->toBe(CampaignRequestStatus::REJECTED) ->and($campaignRequest->rejectionReason)->toBe($rejectionReason); }); @@ -159,11 +160,11 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::REJECTED, + CampaignRequestStatus::REJECTED, ); // THEN @@ -179,11 +180,11 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::CREATED, + CampaignRequestStatus::CREATED, new RejectionReason('Missing legal documents.'), ); @@ -201,11 +202,11 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::CREATED, + CampaignRequestStatus::CREATED, ); $reason = new RejectionReason('Missing legal documents.'); @@ -213,7 +214,7 @@ describe('campaign request', function (): void { $rejected = $campaignRequest->reject($reason); // THEN - expect($rejected->status)->toBe(CampaignStatus::REJECTED) + expect($rejected->status)->toBe(CampaignRequestStatus::REJECTED) ->and($rejected->rejectionReason)->toBe($reason) ->and($rejected->id())->toBe($campaignRequest->id()) ->and($rejected->code)->toBe($campaignRequest->code) @@ -232,11 +233,11 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::TRANSFORMED, + CampaignRequestStatus::TRANSFORMED, ); // WHEN @@ -252,11 +253,11 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::REJECTED, + CampaignRequestStatus::REJECTED, new RejectionReason('First reason.'), ); @@ -275,11 +276,11 @@ describe('campaign request', function (): void { $id, new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - Country::FR, - CampaignType::CLUB, + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, - CampaignStatus::CREATED, + CampaignRequestStatus::CREATED, ); // WHEN diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index 1eb807a..e50c295 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -13,6 +13,7 @@ use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\Stock\StockId; @@ -41,7 +42,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FR; + $availableIn = new CountryAuthorized(Country::FRANCE); $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; $brand = new BrandCode('AAA01'); @@ -79,7 +80,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FR; + $availableIn = new CountryAuthorized(Country::FRANCE); $coupons = []; $brand = new BrandCode('AAA01'); @@ -107,7 +108,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FR; + $availableIn = new CountryAuthorized(Country::FRANCE); $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; $brand = new BrandCode('AAA01'); $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); @@ -137,7 +138,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FR; + $availableIn = new CountryAuthorized(Country::FRANCE); $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; $brand = new BrandCode('AAA01'); @@ -173,7 +174,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FR; + $availableIn = new CountryAuthorized(Country::FRANCE); $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; $brand = new BrandCode('AAA01'); @@ -214,7 +215,7 @@ describe('campaign', function (): void { new CampaignName('Name'), \boolval(0), \boolval(1), - Country::FR, + new CountryAuthorized(Country::FRANCE), [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))], new BrandCode('AAA01') ); @@ -233,7 +234,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FR; + $availableIn = new CountryAuthorized(Country::FRANCE); $coupons = ['PC1']; $brand = new BrandCode('AAA01'); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index 9b485a5..d98d68a 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -10,6 +10,7 @@ use Domain\Enums\Country; use Domain\Models\Entities\Coupon; use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; use Domain\Models\ValueObjects\Coupon\CouponId; @@ -48,7 +49,12 @@ describe('coupon', function (): void { $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)]; + $legalInformation = [ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + new CountryAuthorized(Country::FRANCE) + ) + ]; $code = new ProcessingCode('5425003944130'); $brandCode = new BrandCode('AAA01'); $createdBy = ActorId::generate(); @@ -105,7 +111,12 @@ describe('coupon', function (): void { $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)]; + $legalInformation = [ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + new CountryAuthorized(Country::FRANCE) + ) + ]; $code = new ProcessingCode('5425003944130'); $brandCode = new BrandCode('AAA01'); $createdBy = ActorId::generate(); @@ -159,7 +170,12 @@ describe('coupon', function (): void { $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)]; + $legalInformation = [ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + new CountryAuthorized(Country::FRANCE) + ) + ]; $code = new ProcessingCode('5425003944130'); $brandCode = new BrandCode('AAA01'); $gtin = new GS1('110101010', BarcodeFormat::EAN13); @@ -202,7 +218,12 @@ describe('coupon', function (): void { $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)]; + $legalInformation = [ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + new CountryAuthorized(Country::FRANCE) + ) + ]; $code = new ProcessingCode('5425003944130'); $brandCode = new BrandCode('AAA01'); $createdBy = ActorId::generate(); @@ -257,7 +278,12 @@ describe('coupon', function (): void { $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)]; + $legalInformation = [ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + new CountryAuthorized(Country::FRANCE) + ) + ]; $code = new ProcessingCode('5425003944130'); $brandCode = new BrandCode('AAA01'); $createdBy = ActorId::generate(); @@ -316,7 +342,7 @@ describe('coupon', function (): void { 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 CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], new ProcessingCode('5425003944130'), new BrandCode('AAA01'), ActorId::generate(), diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php index 4052f9f..6083d0b 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; -use Domain\Enums\Campaign\CampaignStatus; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestFilters; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; covers(CampaignRequestFilters::class); @@ -14,7 +14,7 @@ describe('campaign request filters', function (): void { it('creates filters with both fields set', function (): void { // GIVEN $brandCode = new BrandCode('AAA01'); - $status = CampaignStatus::CREATED; + $status = CampaignRequestStatus::CREATED; // WHEN $filters = new CampaignRequestFilters(brandCode: $brandCode, status: $status); @@ -38,7 +38,7 @@ describe('campaign request filters', function (): void { it('creates filters with only status set', function (): void { // GIVEN - $status = CampaignStatus::CREATED; + $status = CampaignRequestStatus::CREATED; // WHEN $filters = new CampaignRequestFilters(status: $status); diff --git a/tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php b/tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php new file mode 100644 index 0000000..517d2c7 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php @@ -0,0 +1,65 @@ +toBeInstanceOf(CountryAuthorized::class) + ->and($authorized->country)->toBe($country); + })->with([ + 'france' => 'france', + 'belgium' => 'belgium', + 'luxembourg' => 'luxembourg', + ]); + + it('rejects a country that is not authorized', function (): void { + // WHEN + $action = fn (): CountryAuthorized => new CountryAuthorized(Country::GERMANY); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Country "GERMANY" is not authorized.'); + }); + + it('lists the authorized countries', function (): void { + // THEN + expect(CountryAuthorized::authorizedCountries())->toBe([ + Country::FRANCE, + Country::BELGIUM, + Country::LUXEMBOURG, + ]); + }); + + it('implements equals correctly', function (): void { + // GIVEN + $france1 = new CountryAuthorized(Country::FRANCE); + $france2 = new CountryAuthorized(Country::FRANCE); + $belgium = new CountryAuthorized(Country::BELGIUM); + + // THEN + expect($france1->equals($france2))->toBeTrue() + ->and($france1->equals($belgium))->toBeFalse() + ->and($france1->equals(null))->toBeFalse(); + }); + + it('converts to string as the country backing value', function (): void { + // GIVEN + $authorized = new CountryAuthorized(Country::FRANCE); + + // THEN + expect((string) $authorized)->toBe('france'); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php index 7f46ee8..c3c8693 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; use Domain\Enums\Country; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; covers(CouponLegalInformation::class); @@ -13,7 +14,7 @@ describe('coupon legal information', function (): void { it('creates a valid coupon legal information', function (): void { // GIVEN $legalText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; - $forLocale = Country::FR; + $forLocale = new CountryAuthorized(Country::FRANCE); // WHEN $couponLegalInformation = new CouponLegalInformation($legalText, $forLocale); @@ -26,7 +27,7 @@ describe('coupon legal information', function (): void { it('rejects invalid coupon legal information', function (): void { // GIVEN $legalText = ''; - $forLocale = Country::FR; + $forLocale = new CountryAuthorized(Country::FRANCE); // 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 index bef8718..13ae8cd 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; use Domain\Enums\Country; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponProductInfo; covers(CouponProductInfo::class); @@ -15,7 +16,7 @@ describe('coupon product info', function (): void { $name = 'Lorem ipsum'; $range = 'Lorem'; $validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; - $forLocale = Country::FR; + $forLocale = new CountryAuthorized(Country::FRANCE); // WHEN $couponProductInfo = new CouponProductInfo($name, $range, $validityText, $forLocale); @@ -32,7 +33,7 @@ describe('coupon product info', function (): void { $name = ''; $range = ''; $validityText = ''; - $forLocale = Country::FR; + $forLocale = new CountryAuthorized(Country::FRANCE); // GIVEN / THEN expect(fn (): CouponProductInfo => new CouponProductInfo($name, $range, $validityText, $forLocale))->toThrow(\DomainException::class); diff --git a/tests/Unit/Shared/Enums/CurrencyTest.php b/tests/Unit/Shared/Enums/CurrencyTest.php index 1d0a2a3..6de9498 100644 --- a/tests/Unit/Shared/Enums/CurrencyTest.php +++ b/tests/Unit/Shared/Enums/CurrencyTest.php @@ -11,6 +11,6 @@ covers(Currency::class); describe('currency', function (): void { it('has the expected cases with their backing values', function (): void { // THEN - expect(Currency::EUR->value)->toBe("euro"); + expect(Currency::EUR->value)->toBe('euro'); }); }); From c773e9de4e33f53c68e876ce36ab3b5dedef782e Mon Sep 17 00:00:00 2001 From: K Muller Date: Fri, 25 Sep 2026 11:54:47 +0200 Subject: [PATCH 29/52] fix: :bug: add Quantity and WarningQuantity as VO --- .../Models/Entities/RequestedCoupon.php | 10 ++-- .../ValueObjects/Campaign/CampaignCoupon.php | 6 +-- .../RequestedCouponQuantity.php | 16 +++++++ .../ValueObjects/WarningThresholdQuantity.php | 16 +++++++ .../Find/FindCampaignRequestTest.php | 6 ++- .../List/ListCampaignsRequestTest.php | 10 ++-- .../Reject/RejectCampaignRequestTest.php | 6 ++- .../Models/Entities/CampaignRequestTest.php | 6 ++- .../Domain/Models/Entities/CampaignTest.php | 46 +++++++++++++++++-- .../Models/Entities/RequestedCouponTest.php | 14 +++--- .../Campaign/CampaignCouponTest.php | 26 ++--------- .../RequestedCouponQuantityTest.php | 39 ++++++++++++++++ .../WarningThresholdQuantityTest.php | 39 ++++++++++++++++ 13 files changed, 188 insertions(+), 52 deletions(-) create mode 100644 src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantity.php create mode 100644 src/Domain/Models/ValueObjects/WarningThresholdQuantity.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php diff --git a/src/Domain/Models/Entities/RequestedCoupon.php b/src/Domain/Models/Entities/RequestedCoupon.php index 3ea6715..43c9c05 100644 --- a/src/Domain/Models/Entities/RequestedCoupon.php +++ b/src/Domain/Models/Entities/RequestedCoupon.php @@ -13,8 +13,10 @@ use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; +use Domain\Models\ValueObjects\WarningThresholdQuantity; /** * @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId> @@ -25,8 +27,8 @@ final readonly class RequestedCoupon implements AggregateRoot, HasTimestampIdent public RequestedCouponId $id, public CouponName $name, public CouponProductName $productName, - public int $quantity, - public int $warningThresholdQuantity, + public RequestedCouponQuantity $quantity, + public WarningThresholdQuantity $warningThresholdQuantity, public Period $period, public MoneyAmount $moneyAmount, public ProcessingCode $processingCode, @@ -56,8 +58,8 @@ final readonly class RequestedCoupon implements AggregateRoot, HasTimestampIdent RequestedCouponId $id, CouponName $name, CouponProductName $productName, - int $quantity, - int $warningThresholdQuantity, + RequestedCouponQuantity $quantity, + WarningThresholdQuantity $warningThresholdQuantity, Period $period, MoneyAmount $moneyAmount, ProcessingCode $processingCode, diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php index 2e7ca71..a3ed524 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php @@ -7,17 +7,15 @@ namespace Domain\Models\ValueObjects\Campaign; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\Stock\StockId; +use Domain\Models\ValueObjects\WarningThresholdQuantity; final readonly class CampaignCoupon { public function __construct( public ProcessingCode $code, public StockId $qty, - public int $warningThresholdQty, + public WarningThresholdQuantity $warningThresholdQty, public ?PrintRules $printRules = null, ) { - if ($this->warningThresholdQty <= 0) { - throw new \DomainException('Campaign Coupon warningThresholdQty must be positive.'); - } } } diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantity.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantity.php new file mode 100644 index 0000000..9794fc2 --- /dev/null +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantity.php @@ -0,0 +1,16 @@ +value <= 0) { + throw new \DomainException('Quantity should be more than 0.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/WarningThresholdQuantity.php b/src/Domain/Models/ValueObjects/WarningThresholdQuantity.php new file mode 100644 index 0000000..682608e --- /dev/null +++ b/src/Domain/Models/ValueObjects/WarningThresholdQuantity.php @@ -0,0 +1,16 @@ +value < 0) { + throw new \DomainException('Warning threshold quantity cannot be negative.'); + } + } +} diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 9f3667d..1697e85 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -35,6 +35,8 @@ use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; +use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; @@ -52,8 +54,8 @@ describe('find campaign request use case', function (): void { RequestedCouponId::generate(), new CouponName('Coupon A'), new CouponProductName('Product A'), - 100, - 10, + new RequestedCouponQuantity(100), + new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 8775d10..866a348 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -29,6 +29,8 @@ use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; +use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Currency; @@ -46,8 +48,8 @@ describe('list campaigns request use case', function (): void { RequestedCouponId::generate(), new CouponName('Coupon A'), new CouponProductName('Product A'), - 100, - 10, + new RequestedCouponQuantity(100), + new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), @@ -60,8 +62,8 @@ describe('list campaigns request use case', function (): void { RequestedCouponId::generate(), new CouponName('Coupon B'), new CouponProductName('Product B'), - 100, - 10, + new RequestedCouponQuantity(100), + new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC2'), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 7581f27..5c3abc4 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -32,6 +32,8 @@ use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; +use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Currency; @@ -51,8 +53,8 @@ describe('reject campaign request use case', function (): void { RequestedCouponId::generate(), new CouponName('Coupon A'), new CouponProductName('Product A'), - 100, - 10, + new RequestedCouponQuantity(100), + new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 0cb2349..5b86beb 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -32,6 +32,8 @@ use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; use Carbon\CarbonImmutable; use Domain\Models\ValueObjects\CountryAuthorized; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; +use Domain\Models\ValueObjects\WarningThresholdQuantity; covers(CampaignRequest::class); @@ -45,8 +47,8 @@ describe('campaign request', function (): void { RequestedCouponId::generate(), new CouponName('Coupon A'), new CouponProductName('Product A'), - 100, - 10, + new RequestedCouponQuantity(100), + new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index e50c295..37511b7 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -19,6 +19,7 @@ use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\Stock\StockId; use Domain\Models\ValueObjects\Tag\TagId; use Domain\Models\ValueObjects\Tag\TagName; +use Domain\Models\ValueObjects\WarningThresholdQuantity; use Ramsey\Uuid\Uuid; covers(Campaign::class); @@ -43,7 +44,14 @@ describe('campaign', function (): void { $publication = \boolval(0); $printAllAtOnce = \boolval(1); $availableIn = new CountryAuthorized(Country::FRANCE); - $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; + $coupons = [ + new CampaignCoupon( + new ProcessingCode('PC1'), + StockId::generate(), + new WarningThresholdQuantity(100), + new PrintRules(maxPerCustomer: 3) + ) + ]; $brand = new BrandCode('AAA01'); // WHEN @@ -109,7 +117,14 @@ describe('campaign', function (): void { $publication = \boolval(0); $printAllAtOnce = \boolval(1); $availableIn = new CountryAuthorized(Country::FRANCE); - $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; + $coupons = [ + new CampaignCoupon( + new ProcessingCode('PC1'), + StockId::generate(), + new WarningThresholdQuantity(100), + new PrintRules(maxPerCustomer: 3) + ) + ]; $brand = new BrandCode('AAA01'); $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); @@ -139,7 +154,14 @@ describe('campaign', function (): void { $publication = \boolval(0); $printAllAtOnce = \boolval(1); $availableIn = new CountryAuthorized(Country::FRANCE); - $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; + $coupons = [ + new CampaignCoupon( + new ProcessingCode('PC1'), + StockId::generate(), + new WarningThresholdQuantity(100), + new PrintRules(maxPerCustomer: 3) + ) + ]; $brand = new BrandCode('AAA01'); // WHEN @@ -175,7 +197,14 @@ describe('campaign', function (): void { $publication = \boolval(0); $printAllAtOnce = \boolval(1); $availableIn = new CountryAuthorized(Country::FRANCE); - $coupons = [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))]; + $coupons = [ + new CampaignCoupon( + new ProcessingCode('PC1'), + StockId::generate(), + new WarningThresholdQuantity(100), + new PrintRules(maxPerCustomer: 3) + ) + ]; $brand = new BrandCode('AAA01'); // WHEN @@ -216,7 +245,14 @@ describe('campaign', function (): void { \boolval(0), \boolval(1), new CountryAuthorized(Country::FRANCE), - [new CampaignCoupon(new ProcessingCode('PC1'), StockId::generate(), 100, new PrintRules(maxPerCustomer: 3))], + [ + new CampaignCoupon( + new ProcessingCode('PC1'), + StockId::generate(), + new WarningThresholdQuantity(100), + new PrintRules(maxPerCustomer: 3) + ) + ], new BrandCode('AAA01') ); diff --git a/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php b/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php index b69de01..c365232 100644 --- a/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php @@ -17,6 +17,8 @@ use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; +use Domain\Models\ValueObjects\WarningThresholdQuantity; use Ramsey\Uuid\Uuid; use Shared\Enums\Currency; use Shared\ValueObjects\DateTime; @@ -34,8 +36,8 @@ describe('requested coupon', function (): void { $id = RequestedCouponId::generate(); $name = new CouponName('Coupon A'); $productName = new CouponProductName('Product A'); - $quantity = 100; - $warningThresholdQuantity = 10; + $quantity = new RequestedCouponQuantity(100); + $warningThresholdQuantity = new WarningThresholdQuantity(10); $period = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); $moneyAmount = new MoneyAmount(new Money(Currency::EUR, 300)); $processingCode = new ProcessingCode('PC1'); @@ -83,8 +85,8 @@ describe('requested coupon', function (): void { RequestedCouponId::generate(), new CouponName('Coupon A'), new CouponProductName('Product A'), - 100, - 10, + new RequestedCouponQuantity(100), + new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), @@ -107,8 +109,8 @@ describe('requested coupon', function (): void { $id, new CouponName('Coupon A'), new CouponProductName('Product A'), - 100, - 10, + new RequestedCouponQuantity(100), + new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php index 9d1b6ba..340d03c 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php @@ -8,6 +8,7 @@ use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\Stock\StockId; +use Domain\Models\ValueObjects\WarningThresholdQuantity; covers(CampaignCoupon::class); @@ -17,7 +18,7 @@ describe('campaign coupon', function (): void { $code = new ProcessingCode('PC1'); $qty = StockId::generate(); $printRules = new PrintRules(maxPerCustomer: 3); - $warningThresholdQty = 100; + $warningThresholdQty = new WarningThresholdQuantity(100); // WHEN $campaignCoupon = new CampaignCoupon($code, $qty, $warningThresholdQty, $printRules); @@ -33,7 +34,7 @@ describe('campaign coupon', function (): void { // GIVEN $code = new ProcessingCode('PC1'); $qty = StockId::generate(); - $warningThresholdQty = 100; + $warningThresholdQty = new WarningThresholdQuantity(100); // WHEN $campaignCoupon = new CampaignCoupon($code, $qty, $warningThresholdQty); @@ -43,25 +44,4 @@ describe('campaign coupon', function (): void { ->and($campaignCoupon->qty)->toBe($qty) ->and($campaignCoupon->warningThresholdQty)->toBe($warningThresholdQty); }); - - it('rejects invalid campaign coupon', function (): void { - // GIVEN - $code = new ProcessingCode('PC1'); - $qty = StockId::generate(); - $printRules = new PrintRules(maxPerCustomer: 3); - $warningThresholdQty = -100; - - // GIVEN / THEN - expect(fn (): CampaignCoupon => new CampaignCoupon($code, $qty, $warningThresholdQty, $printRules))->toThrow(\DomainException::class); - }); - - it('rejects invalid campaign coupon whitout print rules', function (): void { - // GIVEN - $code = new ProcessingCode('PC1'); - $qty = StockId::generate(); - $warningThresholdQty = -100; - - // GIVEN / THEN - expect(fn (): CampaignCoupon => new CampaignCoupon($code, $qty, $warningThresholdQty))->toThrow(\DomainException::class); - }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php new file mode 100644 index 0000000..5197b8d --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php @@ -0,0 +1,39 @@ +toBeInstanceOf(RequestedCouponQuantity::class) + ->and($quantity->value)->toBe($value); + }); + + it('rejects a quantity of zero', function (): void { + // WHEN + $action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity(0); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Quantity should be more than 0.'); + }); + + it('rejects a negative quantity', function (): void { + // WHEN + $action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity(-1); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Quantity should be more than 0.'); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php b/tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php new file mode 100644 index 0000000..318c519 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php @@ -0,0 +1,39 @@ +toBeInstanceOf(WarningThresholdQuantity::class) + ->and($threshold->value)->toBe($value); + }); + + it('accepts a zero value, meaning no warning', function (): void { + // WHEN + $threshold = new WarningThresholdQuantity(0); + + // THEN + expect($threshold->value)->toBe(0); + }); + + it('rejects a negative value', function (): void { + // WHEN + $action = fn (): WarningThresholdQuantity => new WarningThresholdQuantity(-1); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Warning threshold quantity cannot be negative.'); + }); +}); From e15a372e59b53ccffb2e717fde00f6af553b1f49 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Fri, 25 Sep 2026 11:20:51 +0200 Subject: [PATCH 30/52] refactor: :recycle: DateTime VO and use native DateTime class --- .../Coupon/CouponAvailabilityCalendar.php | 2 +- .../ValueObjects/Coupon/CouponValidity.php | 2 +- src/Domain/Models/ValueObjects/Period.php | 2 +- src/Shared/ValueObjects/DateTime.php | 51 ----------- .../Find/FindCampaignRequestTest.php | 4 +- .../List/ListCampaignsRequestTest.php | 6 +- .../Reject/RejectCampaignRequestTest.php | 4 +- .../Models/Entities/CampaignRequestTest.php | 4 +- .../Domain/Models/Entities/CouponTest.php | 2 +- .../Models/Entities/RequestedCouponTest.php | 8 +- .../Coupon/CouponAvailabilityCalendarTest.php | 2 +- .../Coupon/CouponDiffusionTest.php | 2 +- .../Coupon/CouponValidityTest.php | 2 +- .../RequestedCoupon/PeriodTest.php | 2 +- .../Unit/Shared/ValueObjects/DateTimeTest.php | 87 ------------------- 15 files changed, 21 insertions(+), 159 deletions(-) delete mode 100644 src/Shared/ValueObjects/DateTime.php delete mode 100644 tests/Unit/Shared/ValueObjects/DateTimeTest.php diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php b/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php index 6121fe0..718ff59 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php @@ -6,7 +6,7 @@ namespace Domain\Models\ValueObjects\Coupon; use Domain\Models\ValueObjects\Period; use Shared\DDD\Models\Equatable; -use Shared\ValueObjects\DateTime; +use DateTime; final readonly class CouponAvailabilityCalendar implements Equatable { diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponValidity.php b/src/Domain/Models/ValueObjects/Coupon/CouponValidity.php index a5304c9..ef59020 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponValidity.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponValidity.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Coupon; use Shared\DDD\Models\Equatable; -use Shared\ValueObjects\DateTime; +use DateTime; final readonly class CouponValidity implements Equatable { diff --git a/src/Domain/Models/ValueObjects/Period.php b/src/Domain/Models/ValueObjects/Period.php index af6559f..da672a4 100644 --- a/src/Domain/Models/ValueObjects/Period.php +++ b/src/Domain/Models/ValueObjects/Period.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects; -use Shared\ValueObjects\DateTime; +use DateTime; final readonly class Period { diff --git a/src/Shared/ValueObjects/DateTime.php b/src/Shared/ValueObjects/DateTime.php deleted file mode 100644 index d8007d1..0000000 --- a/src/Shared/ValueObjects/DateTime.php +++ /dev/null @@ -1,51 +0,0 @@ -value = \DateTimeImmutable::createFromInterface($dateTime) - ->setTimezone(new \DateTimeZone('UTC')); - } - - public static function now(): self - { - return new self(DatesHelper::now()); - } - - #[\Override] - public function equals(mixed $other): bool - { - return $other instanceof self - && $this->value->format(\DateTimeInterface::RFC3339_EXTENDED) === $other->value->format(\DateTimeInterface::RFC3339_EXTENDED); - } - - public function __toString(): string - { - return DatesHelper::toRfc3339($this->value); - } - - public function jsonSerialize(): string - { - return (string) $this; - } -} diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 1697e85..91fd4f9 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -41,7 +41,7 @@ use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Currency; -use Shared\ValueObjects\DateTime; +use DateTime; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; use Shared\ValueObjects\Rcs; @@ -61,7 +61,7 @@ describe('find campaign request use case', function (): void { new ProcessingCode('PC1'), new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), + new CouponValidity(30, new DateTime()), ); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 866a348..dbc8c7d 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -34,7 +34,7 @@ use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Currency; -use Shared\ValueObjects\DateTime; +use DateTime; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; use Shared\ValueObjects\Pagination\Paginated; @@ -55,7 +55,7 @@ describe('list campaigns request use case', function (): void { new ProcessingCode('PC1'), new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), + new CouponValidity(30, new DateTime()), ); $this->couponB = RequestedCoupon::hydrate( @@ -69,7 +69,7 @@ describe('list campaigns request use case', function (): void { new ProcessingCode('PC2'), new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), + new CouponValidity(30, new DateTime()), ); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 5c3abc4..5ffaa68 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -37,7 +37,7 @@ use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Currency; -use Shared\ValueObjects\DateTime; +use DateTime; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; @@ -60,7 +60,7 @@ describe('reject campaign request use case', function (): void { new ProcessingCode('PC1'), new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), + new CouponValidity(30, new DateTime()), ); }); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 5b86beb..caf16b5 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -27,7 +27,7 @@ use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId; use Ramsey\Uuid\Uuid; use Shared\Enums\Currency; -use Shared\ValueObjects\DateTime; +use DateTime; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; use Carbon\CarbonImmutable; @@ -54,7 +54,7 @@ describe('campaign request', function (): void { new ProcessingCode('PC1'), new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), + new CouponValidity(30, new DateTime()), ); }); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index d98d68a..f5971fb 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -27,7 +27,7 @@ use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\ProcessingCode; use Ramsey\Uuid\Uuid; use Shared\Enums\Currency; -use Shared\ValueObjects\DateTime; +use DateTime; use Shared\ValueObjects\Money; covers(Coupon::class); diff --git a/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php b/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php index c365232..a06c737 100644 --- a/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php @@ -21,7 +21,7 @@ use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Ramsey\Uuid\Uuid; use Shared\Enums\Currency; -use Shared\ValueObjects\DateTime; +use DateTime; use Shared\ValueObjects\Money; covers(RequestedCoupon::class); @@ -43,7 +43,7 @@ describe('requested coupon', function (): void { $processingCode = new ProcessingCode('PC1'); $couponPicture = new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')); $gs1 = new GS1('01234567890128', BarcodeFormat::EAN13); - $couponValidity = new CouponValidity(30, DateTime::now()); + $couponValidity = new CouponValidity(30, new DateTime()); // WHEN $coupon = RequestedCoupon::hydrate( @@ -92,7 +92,7 @@ describe('requested coupon', function (): void { new ProcessingCode('PC1'), new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), + new CouponValidity(30, new DateTime()), $updatedAt, ); @@ -116,7 +116,7 @@ describe('requested coupon', function (): void { new ProcessingCode('PC1'), new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, DateTime::now()), + new CouponValidity(30, new DateTime()), ); // WHEN diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php index 0ab0282..a663ef3 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php @@ -6,7 +6,7 @@ namespace Tests\Unit\Domain\Models\ValueObjects\Coupon; use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar; use Domain\Models\ValueObjects\Period; -use Shared\ValueObjects\DateTime; +use DateTime; covers(CouponAvailabilityCalendar::class); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php index 60d9196..15f0745 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php @@ -6,7 +6,7 @@ namespace Tests\Unit\Domain\Models\ValueObjects\Coupon; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; use Domain\Models\ValueObjects\Period; -use Shared\ValueObjects\DateTime; +use DateTime; covers(CouponDiffusion::class); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php index a4bb9e1..cd0db59 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; use Domain\Models\ValueObjects\Coupon\CouponValidity; -use Shared\ValueObjects\DateTime; +use DateTime; covers(CouponValidity::class); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php index 20ff709..0602074 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\RequestedCoupon; use Domain\Models\ValueObjects\Period; -use Shared\ValueObjects\DateTime; +use DateTime; covers(Period::class); diff --git a/tests/Unit/Shared/ValueObjects/DateTimeTest.php b/tests/Unit/Shared/ValueObjects/DateTimeTest.php deleted file mode 100644 index d7f081c..0000000 --- a/tests/Unit/Shared/ValueObjects/DateTimeTest.php +++ /dev/null @@ -1,87 +0,0 @@ -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 - 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 - 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); - }); -}); From d10b91fe48c35afa4752b9568fd287d1be14fc22 Mon Sep 17 00:00:00 2001 From: K Muller Date: Fri, 25 Sep 2026 12:40:57 +0200 Subject: [PATCH 31/52] fix: :bug: delete the assert on RejectCampaignRequest UseCase --- .../Reject/RejectCampaignRequest.php | 4 ---- .../Reject/RejectCampaignRequestTest.php | 17 +++++++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php index 5deb8f6..8b8bca1 100644 --- a/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php +++ b/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php @@ -8,7 +8,6 @@ use Application\UseCases\CampaignRequest\Reject\Output\CampaignRequestNotFound; use Application\UseCases\CampaignRequest\Reject\Output\CampaignRequestRejected; use Application\UseCases\CampaignRequest\Reject\Output\Output; use Domain\Models\Entities\CampaignRequest; -use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\DDD\UseCases\Transactional; @@ -38,9 +37,6 @@ final readonly class RejectCampaignRequest implements UseCase, Transactional $this->repository->save($campaignRequest); - Assert::isInstanceOf($campaignRequest->rejectionReason, RejectionReason::class); - Assert::notNull($campaignRequest->updatedAt()); - return new CampaignRequestRejected( id: $campaignRequest->id(), status: $campaignRequest->status, diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 5ffaa68..ba7b712 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -83,7 +83,7 @@ describe('reject campaign request use case', function (): void { $repository = \Mockery::mock(CampaignRequestRepository::class); $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); $repository->shouldReceive('save')->once()->with(\Mockery::on( - fn (CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED + fn(CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED && $saved->rejectionReason === $reason && $saved->updatedAt !== null, )); @@ -139,7 +139,7 @@ describe('reject campaign request use case', function (): void { $useCase = new RejectCampaignRequest($repository); // WHEN - $action = fn (): Output => $useCase->execute( + $action = fn(): Output => $useCase->execute( new Input(id: $id, reason: new RejectionReason('Missing legal documents.')) ); @@ -149,16 +149,21 @@ describe('reject campaign request use case', function (): void { }); describe('reject campaign request use case assertions', function (): void { + it('cannot build an input to reject a campaign request without a reason', function (): void { + // WHEN + $action = fn(): Input => new Input(id: CampaignRequestId::generate()); + + // THEN + expect($action)->toThrow(\ArgumentCountError::class); + }); it('rejects an input of the wrong type', function (): void { // GIVEN $repository = \Mockery::mock(CampaignRequestRepository::class); $useCase = new RejectCampaignRequest($repository); - $invalidInput = new class () implements BaseInput - { - }; + $invalidInput = new class() implements BaseInput {}; // WHEN - $action = fn (): Output => $useCase->execute($invalidInput); + $action = fn(): Output => $useCase->execute($invalidInput); // THEN expect($action)->toThrow(\InvalidArgumentException::class); From 973c833facaf496ea53a7a0a052c630d77798709 Mon Sep 17 00:00:00 2001 From: K Muller Date: Fri, 25 Sep 2026 12:14:48 +0200 Subject: [PATCH 32/52] fix: :bug: change entity requestedCoupon into VO --- .../Find/Output/CampaignRequestFound.php | 2 +- .../Models/Entities/CampaignRequest.php | 1 + .../Models/Entities/RequestedCoupon.php | 86 ------------ .../RequestedCoupon/RequestedCoupon.php | 32 +++++ .../RequestedCoupon/RequestedCouponId.php | 16 --- .../Find/FindCampaignRequestTest.php | 6 +- .../List/ListCampaignsRequestTest.php | 9 +- .../Reject/RejectCampaignRequestTest.php | 6 +- .../Models/Entities/CampaignRequestTest.php | 6 +- .../Models/Entities/RequestedCouponTest.php | 128 ------------------ .../RequestedCoupon/RequestedCouponIdTest.php | 28 ---- .../RequestedCoupon/RequestedCouponTest.php | 71 ++++++++++ 12 files changed, 114 insertions(+), 277 deletions(-) delete mode 100644 src/Domain/Models/Entities/RequestedCoupon.php create mode 100644 src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php delete mode 100644 src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponId.php delete mode 100644 tests/Unit/Domain/Models/Entities/RequestedCouponTest.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponIdTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php diff --git a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php index eb0d481..08dafd7 100644 --- a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php +++ b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php @@ -7,11 +7,11 @@ namespace Application\UseCases\CampaignRequest\Find\Output; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\Entities\Brand; -use Domain\Models\Entities\RequestedCoupon; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\PrintRules; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; final readonly class CampaignRequestFound implements Output { diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index 7858146..68e6cfd 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -12,6 +12,7 @@ use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\PrintRules; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; use Shared\Helpers\DatesHelper; diff --git a/src/Domain/Models/Entities/RequestedCoupon.php b/src/Domain/Models/Entities/RequestedCoupon.php deleted file mode 100644 index 43c9c05..0000000 --- a/src/Domain/Models/Entities/RequestedCoupon.php +++ /dev/null @@ -1,86 +0,0 @@ - - */ -final readonly class RequestedCoupon implements AggregateRoot, HasTimestampIdentifier -{ - private function __construct( - public RequestedCouponId $id, - public CouponName $name, - public CouponProductName $productName, - public RequestedCouponQuantity $quantity, - public WarningThresholdQuantity $warningThresholdQuantity, - public Period $period, - public MoneyAmount $moneyAmount, - public ProcessingCode $processingCode, - public CouponPicture $couponPicture, - public GS1 $gs1, - public CouponValidity $couponValidity, - public ?\DateTimeInterface $updatedAt = null, - ) { - } - - public function id(): RequestedCouponId - { - return $this->id; - } - - public function createdAt(): \DateTimeInterface - { - return $this->id->toDateTime(); - } - - public function updatedAt(): ?\DateTimeInterface - { - return $this->updatedAt; - } - - public static function hydrate( - RequestedCouponId $id, - CouponName $name, - CouponProductName $productName, - RequestedCouponQuantity $quantity, - WarningThresholdQuantity $warningThresholdQuantity, - Period $period, - MoneyAmount $moneyAmount, - ProcessingCode $processingCode, - CouponPicture $couponPicture, - GS1 $gs1, - CouponValidity $couponValidity, - ?\DateTimeInterface $updatedAt = null, - ): self { - return new self( - id: $id, - name: $name, - productName: $productName, - quantity: $quantity, - warningThresholdQuantity: $warningThresholdQuantity, - period: $period, - moneyAmount: $moneyAmount, - processingCode: $processingCode, - couponPicture: $couponPicture, - gs1: $gs1, - couponValidity: $couponValidity, - updatedAt: $updatedAt, - ); - } -} diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php new file mode 100644 index 0000000..616c082 --- /dev/null +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -0,0 +1,32 @@ +requestedCoupon = RequestedCoupon::hydrate( - RequestedCouponId::generate(), + $this->requestedCoupon = new RequestedCoupon( new CouponName('Coupon A'), new CouponProductName('Product A'), new RequestedCouponQuantity(100), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index dbc8c7d..de00837 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -9,7 +9,6 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; -use Domain\Models\Entities\RequestedCoupon; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestFilters; @@ -28,13 +27,13 @@ use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Currency; use DateTime; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; use Shared\ValueObjects\Pagination\Paginated; @@ -44,8 +43,7 @@ covers(ListCampaignsRequest::class, Input::class, Output::class, CampaignRequest describe('list campaigns request use case', function (): void { beforeEach(function (): void { - $this->couponA = RequestedCoupon::hydrate( - RequestedCouponId::generate(), + $this->couponA = new RequestedCoupon( new CouponName('Coupon A'), new CouponProductName('Product A'), new RequestedCouponQuantity(100), @@ -58,8 +56,7 @@ describe('list campaigns request use case', function (): void { new CouponValidity(30, new DateTime()), ); - $this->couponB = RequestedCoupon::hydrate( - RequestedCouponId::generate(), + $this->couponB = new RequestedCoupon( new CouponName('Coupon B'), new CouponProductName('Product B'), new RequestedCouponQuantity(100), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index ba7b712..da83261 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -15,7 +15,6 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; -use Domain\Models\Entities\RequestedCoupon; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; @@ -31,13 +30,13 @@ use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Currency; use DateTime; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; @@ -49,8 +48,7 @@ describe('reject campaign request use case', function (): void { }); beforeEach(function (): void { - $this->requestedCoupon = RequestedCoupon::hydrate( - RequestedCouponId::generate(), + $this->requestedCoupon = new RequestedCoupon( new CouponName('Coupon A'), new CouponProductName('Product A'), new RequestedCouponQuantity(100), diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index caf16b5..837ae04 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -9,7 +9,6 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; -use Domain\Models\Entities\RequestedCoupon; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; @@ -24,7 +23,6 @@ use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponId; use Ramsey\Uuid\Uuid; use Shared\Enums\Currency; use DateTime; @@ -32,6 +30,7 @@ use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; use Carbon\CarbonImmutable; use Domain\Models\ValueObjects\CountryAuthorized; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; @@ -43,8 +42,7 @@ describe('campaign request', function (): void { }); beforeEach(function (): void { - $this->requestedCoupon = RequestedCoupon::hydrate( - RequestedCouponId::generate(), + $this->requestedCoupon = new RequestedCoupon( new CouponName('Coupon A'), new CouponProductName('Product A'), new RequestedCouponQuantity(100), diff --git a/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php b/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php deleted file mode 100644 index a06c737..0000000 --- a/tests/Unit/Domain/Models/Entities/RequestedCouponTest.php +++ /dev/null @@ -1,128 +0,0 @@ -id())->toBe($id) - ->and($coupon->name)->toBe($name) - ->and($coupon->productName)->toBe($productName) - ->and($coupon->quantity)->toBe($quantity) - ->and($coupon->warningThresholdQuantity)->toBe($warningThresholdQuantity) - ->and($coupon->period)->toBe($period) - ->and($coupon->moneyAmount)->toBe($moneyAmount) - ->and($coupon->processingCode)->toBe($processingCode) - ->and($coupon->couponPicture)->toBe($couponPicture) - ->and($coupon->gs1)->toBe($gs1) - ->and($coupon->couponValidity)->toBe($couponValidity) - ->and($coupon->updatedAt)->toBeNull() - ->and($coupon->updatedAt())->toBeNull(); - }); - - it('hydrates a requested coupon with an explicit updatedAt date', function (): void { - // GIVEN - $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); - - // WHEN - $coupon = RequestedCoupon::hydrate( - RequestedCouponId::generate(), - new CouponName('Coupon A'), - new CouponProductName('Product A'), - new RequestedCouponQuantity(100), - new WarningThresholdQuantity(10), - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC1'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), - new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, new DateTime()), - $updatedAt, - ); - - // THEN - expect($coupon->updatedAt)->toBe($updatedAt) - ->and($coupon->updatedAt())->toBe($updatedAt); - }); - - 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 RequestedCouponId(Uuid::uuid7($expected)); - $coupon = RequestedCoupon::hydrate( - $id, - new CouponName('Coupon A'), - new CouponProductName('Product A'), - new RequestedCouponQuantity(100), - new WarningThresholdQuantity(10), - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC1'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), - new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, new DateTime()), - ); - - // 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/RequestedCoupon/RequestedCouponIdTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponIdTest.php deleted file mode 100644 index 017f4ac..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponIdTest.php +++ /dev/null @@ -1,28 +0,0 @@ -toBeInstanceOf(RequestedCouponId::class); - }); - - it('generates different ids on each call', function (): void { - // WHEN - $first = RequestedCouponId::generate(); - $second = RequestedCouponId::generate(); - - // THEN - expect((string) $first)->not->toBe((string) $second); - }); -}); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php new file mode 100644 index 0000000..f5107a9 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -0,0 +1,71 @@ +name)->toBe($name) + ->and($coupon->productName)->toBe($productName) + ->and($coupon->quantity)->toBe($quantity) + ->and($coupon->warningThresholdQuantity)->toBe($warningThresholdQuantity) + ->and($coupon->period)->toBe($period) + ->and($coupon->moneyAmount)->toBe($moneyAmount) + ->and($coupon->processingCode)->toBe($processingCode) + ->and($coupon->couponPicture)->toBe($couponPicture) + ->and($coupon->gs1)->toBe($gs1) + ->and($coupon->couponValidity)->toBe($couponValidity); + }); +}); From 611bf375ab6412acaa302edb300b39d16c2ed319 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Mon, 28 Sep 2026 09:53:52 +0200 Subject: [PATCH 33/52] feat: :sparkles: add list campaign request tests without sort and without filters --- .../List/ListCampaignsRequestTest.php | 113 ++++++++++++++---- 1 file changed, 93 insertions(+), 20 deletions(-) diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index de00837..9413e3f 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -5,8 +5,6 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\List; use Domain\Enums\BarcodeFormat; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; @@ -16,6 +14,8 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortClause; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\CouponName; @@ -30,7 +30,6 @@ use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; -use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Currency; use DateTime; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; @@ -129,6 +128,97 @@ describe('list campaigns request use case', function (): void { ->and($result->campaignsRequest->getPagination())->toBe($pagination); }); + it('lists campaigns request with filters but without sort', function (): void { + // GIVEN + $pagination = Pagination::firstPage(); + $campaignRequest = CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, + new PrintRules(maxPerCustomer: 3), + $this->couponA, + CampaignRequestStatus::CREATED, + ); + $paginated = new Paginated($pagination, [$campaignRequest], 1); + + $filters = new CampaignRequestFilters($campaignRequest->code, CampaignRequestStatus::CREATED); + $input = new Input(pagination: $pagination, filters: $filters); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findAll') + ->once() + ->with($pagination, null, $filters) + ->andReturn($paginated); + + $useCase = new ListCampaignsRequest($repository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + $items = $result->campaignsRequest->getItems(); + expect($items)->toHaveCount(1) + ->and($items[0])->toBeInstanceOf(CampaignRequestListItem::class) + ->and($items[0]->code)->toBe($campaignRequest->code) + ->and($items[0]->status)->toBe($campaignRequest->status) + ->and($result->campaignsRequest->getTotal())->toBe(1) + ->and($result->campaignsRequest->getPagination())->toBe($pagination); + }); + + it('lists campaigns request with sort but without filters', function (): void { + // GIVEN + $pagination = Pagination::firstPage(); + $campaignRequest1 = CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA01'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, + new PrintRules(maxPerCustomer: 3), + $this->couponA, + CampaignRequestStatus::CREATED, + ); + $campaignRequest2 = CampaignRequest::hydrate( + CampaignRequestId::generate(), + new BrandCode('AAA02'), + new Applicant('John', 'Doe', new Email('john.doe@example.com')), + new CountryAuthorized(Country::FRANCE), + CampaignRequestType::CLUB, + new PrintRules(maxPerCustomer: 3), + $this->couponB, + CampaignRequestStatus::CREATED, + ); + $paginated = new Paginated($pagination, [$campaignRequest2, $campaignRequest1], 2); + + $sort = new CampaignRequestSortCriteria( + clauses: [ + new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Descending), + ] + ); + $input = new Input(pagination: $pagination, sort: $sort); + + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldReceive('findAll') + ->once() + ->with($pagination, $sort, null) + ->andReturn($paginated); + + $useCase = new ListCampaignsRequest($repository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + $items = $result->campaignsRequest->getItems(); + expect($items)->toHaveCount(2) + ->and($items[0]->code)->toBe($campaignRequest2->code) + ->and($items[1]->code)->toBe($campaignRequest1->code) + ->and($result->campaignsRequest->getTotal())->toBe(2) + ->and($result->campaignsRequest->getPagination())->toBe($pagination); + }); + it('returns an empty list without error when there are no campaigns request', function (): void { // GIVEN $pagination = Pagination::firstPage(); @@ -151,20 +241,3 @@ describe('list campaigns request use case', function (): void { ->and($result->campaignsRequest->getTotal())->toBe(0); }); }); - -describe('list brands use case assertions', function (): void { - it('rejects an input of the wrong type', function (): void { - // GIVEN - $repository = \Mockery::mock(CampaignRequestRepository::class); - $useCase = new ListCampaignsRequest($repository); - $invalidInput = new class () implements BaseInput - { - }; - - // WHEN - $action = fn (): Output => $useCase->execute($invalidInput); - - // THEN - expect($action)->toThrow(\InvalidArgumentException::class); - }); -}); From f2fb5d8a236f971d61a3d85abc6307d8f18d5f95 Mon Sep 17 00:00:00 2001 From: K Muller Date: Mon, 28 Sep 2026 14:32:19 +0200 Subject: [PATCH 34/52] fix: :bug: fix linters --- .../Reject/Output/CampaignRequestRejected.php | 2 +- .../CampaignRequest/Reject/RejectCampaignRequest.php | 2 +- src/Domain/Models/Entities/CampaignRequest.php | 9 +++++++++ .../CampaignRequest/Reject/RejectCampaignRequestTest.php | 3 ++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php b/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php index ceca5c6..dcf3a78 100644 --- a/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php +++ b/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php @@ -14,7 +14,7 @@ final readonly class CampaignRequestRejected implements Output public CampaignRequestId $id, public CampaignRequestStatus $status, public RejectionReason $rejectionReason, - public \DateTimeInterface $updatedAt, + public ?\DateTimeInterface $updatedAt = null ) { } } diff --git a/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php index 8b8bca1..ac717f3 100644 --- a/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php +++ b/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php @@ -40,7 +40,7 @@ final readonly class RejectCampaignRequest implements UseCase, Transactional return new CampaignRequestRejected( id: $campaignRequest->id(), status: $campaignRequest->status, - rejectionReason: $campaignRequest->rejectionReason, + rejectionReason:$campaignRequest->rejectionReason(), updatedAt: $campaignRequest->updatedAt(), ); } diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index 68e6cfd..399ac65 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -104,4 +104,13 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent updatedAt: DatesHelper::now(), ); } + + public function rejectionReason(): RejectionReason + { + if (! $this->rejectionReason instanceof RejectionReason) { + throw new \DomainException('Only a rejected campaign request has a rejection reason.'); + } + + return $this->rejectionReason; + } } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index da83261..652ff31 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -158,7 +158,8 @@ describe('reject campaign request use case assertions', function (): void { // GIVEN $repository = \Mockery::mock(CampaignRequestRepository::class); $useCase = new RejectCampaignRequest($repository); - $invalidInput = new class() implements BaseInput {}; + $invalidInput = new class () implements BaseInput { + }; // WHEN $action = fn(): Output => $useCase->execute($invalidInput); From 65e834a06eb8b579ea6d13a5c826c583c3278e04 Mon Sep 17 00:00:00 2001 From: K Muller Date: Mon, 28 Sep 2026 14:32:19 +0200 Subject: [PATCH 35/52] fix: :bug: fix linters --- .../Reject/RejectCampaignRequestTest.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 652ff31..9f7ddab 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -81,7 +81,7 @@ describe('reject campaign request use case', function (): void { $repository = \Mockery::mock(CampaignRequestRepository::class); $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); $repository->shouldReceive('save')->once()->with(\Mockery::on( - fn(CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED + fn (CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED && $saved->rejectionReason === $reason && $saved->updatedAt !== null, )); @@ -137,7 +137,7 @@ describe('reject campaign request use case', function (): void { $useCase = new RejectCampaignRequest($repository); // WHEN - $action = fn(): Output => $useCase->execute( + $action = fn (): Output => $useCase->execute( new Input(id: $id, reason: new RejectionReason('Missing legal documents.')) ); @@ -149,7 +149,7 @@ describe('reject campaign request use case', function (): void { describe('reject campaign request use case assertions', function (): void { it('cannot build an input to reject a campaign request without a reason', function (): void { // WHEN - $action = fn(): Input => new Input(id: CampaignRequestId::generate()); + $action = fn (): Input => new Input(id: CampaignRequestId::generate()); // THEN expect($action)->toThrow(\ArgumentCountError::class); @@ -158,11 +158,12 @@ describe('reject campaign request use case assertions', function (): void { // GIVEN $repository = \Mockery::mock(CampaignRequestRepository::class); $useCase = new RejectCampaignRequest($repository); - $invalidInput = new class () implements BaseInput { + $invalidInput = new class () implements BaseInput + { }; // WHEN - $action = fn(): Output => $useCase->execute($invalidInput); + $action = fn (): Output => $useCase->execute($invalidInput); // THEN expect($action)->toThrow(\InvalidArgumentException::class); From 29c6428aaf791ee64fc6cd89f0c20fdf27ea34bf Mon Sep 17 00:00:00 2001 From: K Muller Date: Fri, 25 Sep 2026 16:21:20 +0200 Subject: [PATCH 36/52] fix: :bug: change Country into Shared --- src/Domain/Models/ValueObjects/CountryAuthorized.php | 2 +- .../DDD}/Exceptions/UnknownCountryIsoCodeException.php | 4 ++-- src/{Domain => Shared}/Enums/Country.php | 4 ++-- .../CampaignRequest/Find/FindCampaignRequestTest.php | 2 +- .../CampaignRequest/List/ListCampaignsRequestTest.php | 6 +++--- .../CampaignRequest/Reject/RejectCampaignRequestTest.php | 2 +- tests/Unit/Domain/Enums/CountryTest.php | 2 +- tests/Unit/Domain/Models/Entities/CampaignRequestTest.php | 2 +- tests/Unit/Domain/Models/Entities/CampaignTest.php | 2 +- tests/Unit/Domain/Models/Entities/CouponTest.php | 2 +- tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php | 2 +- .../ValueObjects/Coupon/CouponLegalInformationTest.php | 2 +- .../Models/ValueObjects/Coupon/CouponProductInfoTest.php | 2 +- 13 files changed, 17 insertions(+), 17 deletions(-) rename src/{Domain => Shared/DDD}/Exceptions/UnknownCountryIsoCodeException.php (87%) rename src/{Domain => Shared}/Enums/Country.php (99%) diff --git a/src/Domain/Models/ValueObjects/CountryAuthorized.php b/src/Domain/Models/ValueObjects/CountryAuthorized.php index d8e99d0..74c6515 100644 --- a/src/Domain/Models/ValueObjects/CountryAuthorized.php +++ b/src/Domain/Models/ValueObjects/CountryAuthorized.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects; -use Domain\Enums\Country; use Override; use Shared\DDD\Models\Equatable; +use Shared\Enums\Country; final readonly class CountryAuthorized implements \Stringable, Equatable { diff --git a/src/Domain/Exceptions/UnknownCountryIsoCodeException.php b/src/Shared/DDD/Exceptions/UnknownCountryIsoCodeException.php similarity index 87% rename from src/Domain/Exceptions/UnknownCountryIsoCodeException.php rename to src/Shared/DDD/Exceptions/UnknownCountryIsoCodeException.php index 6b566a3..263fc8e 100644 --- a/src/Domain/Exceptions/UnknownCountryIsoCodeException.php +++ b/src/Shared/DDD/Exceptions/UnknownCountryIsoCodeException.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Domain\Exceptions; +namespace Shared\DDD\Exceptions; -use Domain\Enums\Country; +use Shared\Enums\Country; final class UnknownCountryIsoCodeException extends \DomainException { diff --git a/src/Domain/Enums/Country.php b/src/Shared/Enums/Country.php similarity index 99% rename from src/Domain/Enums/Country.php rename to src/Shared/Enums/Country.php index 4a719f2..857447a 100644 --- a/src/Domain/Enums/Country.php +++ b/src/Shared/Enums/Country.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Domain\Enums; +namespace Shared\Enums; -use Domain\Exceptions\UnknownCountryIsoCodeException; +use Shared\DDD\Exceptions\UnknownCountryIsoCodeException; enum Country: string { diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index a286cc7..afbf59f 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -10,7 +10,7 @@ use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestFound; use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestNotFound; use Application\UseCases\CampaignRequest\Find\Output\Output; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Country; +use Shared\Enums\Country; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Exceptions\BrandNotFoundException; diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 9413e3f..0fb9f32 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -5,7 +5,9 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\List; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Country; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; +use Shared\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; @@ -14,8 +16,6 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortClause; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\CouponName; diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 9f7ddab..1edb473 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -13,7 +13,7 @@ use Carbon\CarbonImmutable; use Domain\Enums\BarcodeFormat; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; -use Domain\Enums\Country; +use Shared\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; diff --git a/tests/Unit/Domain/Enums/CountryTest.php b/tests/Unit/Domain/Enums/CountryTest.php index 5445fdc..20c5a31 100644 --- a/tests/Unit/Domain/Enums/CountryTest.php +++ b/tests/Unit/Domain/Enums/CountryTest.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Tests\Unit\Shared\Enums; -use Domain\Enums\Country; +use Shared\Enums\Country; covers(Country::class); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 837ae04..df36484 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -7,7 +7,7 @@ namespace Tests\Unit\Domain\Models\Entities; use Domain\Enums\BarcodeFormat; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; -use Domain\Enums\Country; +use Shared\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index 37511b7..b6caa2e 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; -use Domain\Enums\Country; +use Shared\Enums\Country; use Domain\Models\Entities\Campaign; use Domain\Models\Entities\Tag; use Domain\Models\ValueObjects\Brand\BrandCode; diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index f5971fb..89ae1d9 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -6,7 +6,7 @@ namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Country; +use Shared\Enums\Country; use Domain\Models\Entities\Coupon; use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; diff --git a/tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php b/tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php index 517d2c7..9bb5e8b 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php +++ b/tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; -use Domain\Enums\Country; +use Shared\Enums\Country; use Domain\Models\ValueObjects\CountryAuthorized; covers(CountryAuthorized::class); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php index c3c8693..df3d2ba 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; -use Domain\Enums\Country; +use Shared\Enums\Country; use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php index 13ae8cd..80e567f 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; -use Domain\Enums\Country; +use Shared\Enums\Country; use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponProductInfo; From 69faddcd0809ed8bc90a8d46665ef650e045b358 Mon Sep 17 00:00:00 2001 From: K Muller Date: Mon, 28 Sep 2026 10:22:20 +0200 Subject: [PATCH 37/52] fix: :bug: change reject test for testing reason null --- .../Reject/RejectCampaignRequestTest.php | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 1edb473..d4c6703 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -33,7 +33,6 @@ use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; -use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Currency; use DateTime; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; @@ -81,7 +80,7 @@ describe('reject campaign request use case', function (): void { $repository = \Mockery::mock(CampaignRequestRepository::class); $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); $repository->shouldReceive('save')->once()->with(\Mockery::on( - fn (CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED + fn(CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED && $saved->rejectionReason === $reason && $saved->updatedAt !== null, )); @@ -137,35 +136,26 @@ describe('reject campaign request use case', function (): void { $useCase = new RejectCampaignRequest($repository); // WHEN - $action = fn (): Output => $useCase->execute( + $action = fn(): Output => $useCase->execute( new Input(id: $id, reason: new RejectionReason('Missing legal documents.')) ); // THEN expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.'); }); -}); -describe('reject campaign request use case assertions', function (): void { - it('cannot build an input to reject a campaign request without a reason', function (): void { + it('never touches the repository when a rejection reason is missing', function (): void { + // GIVEN + $repository = \Mockery::mock(CampaignRequestRepository::class); + $repository->shouldNotReceive('findById'); + $repository->shouldNotReceive('save'); + + $useCase = new RejectCampaignRequest($repository); + // WHEN - $action = fn (): Input => new Input(id: CampaignRequestId::generate()); + $action = fn(): Output => $useCase->execute(new Input(id: CampaignRequestId::generate())); // THEN expect($action)->toThrow(\ArgumentCountError::class); }); - it('rejects an input of the wrong type', function (): void { - // GIVEN - $repository = \Mockery::mock(CampaignRequestRepository::class); - $useCase = new RejectCampaignRequest($repository); - $invalidInput = new class () implements BaseInput - { - }; - - // WHEN - $action = fn (): Output => $useCase->execute($invalidInput); - - // THEN - expect($action)->toThrow(\InvalidArgumentException::class); - }); }); From 2c693d90459d49233514db2e784e0d72a4b09f7a Mon Sep 17 00:00:00 2001 From: K Muller Date: Mon, 28 Sep 2026 14:57:31 +0200 Subject: [PATCH 38/52] fix: :bug: country in shared --- .../ValueObjects/CountryAuthorizedTest.php | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/Unit/Domain/Models/ValueObjects/CountryAuthorizedTest.php diff --git a/tests/Unit/Domain/Models/ValueObjects/CountryAuthorizedTest.php b/tests/Unit/Domain/Models/ValueObjects/CountryAuthorizedTest.php new file mode 100644 index 0000000..4019b00 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CountryAuthorizedTest.php @@ -0,0 +1,65 @@ +toBeInstanceOf(CountryAuthorized::class) + ->and($authorized->country)->toBe($country); + })->with([ + 'france' => 'france', + 'belgium' => 'belgium', + 'luxembourg' => 'luxembourg', + ]); + + it('rejects a country that is not authorized', function (): void { + // WHEN + $action = fn (): CountryAuthorized => new CountryAuthorized(Country::GERMANY); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Country "GERMANY" is not authorized.'); + }); + + it('lists the authorized countries', function (): void { + // THEN + expect(CountryAuthorized::authorizedCountries())->toBe([ + Country::FRANCE, + Country::BELGIUM, + Country::LUXEMBOURG, + ]); + }); + + it('implements equals correctly', function (): void { + // GIVEN + $france1 = new CountryAuthorized(Country::FRANCE); + $france2 = new CountryAuthorized(Country::FRANCE); + $belgium = new CountryAuthorized(Country::BELGIUM); + + // THEN + expect($france1->equals($france2))->toBeTrue() + ->and($france1->equals($belgium))->toBeFalse() + ->and($france1->equals(null))->toBeFalse(); + }); + + it('converts to string as the country backing value', function (): void { + // GIVEN + $authorized = new CountryAuthorized(Country::FRANCE); + + // THEN + expect((string) $authorized)->toBe('france'); + }); +}); From 44c65cf9f68c93edd25fcddc05bb5c86043b03e5 Mon Sep 17 00:00:00 2001 From: K Muller Date: Tue, 29 Sep 2026 09:02:22 +0200 Subject: [PATCH 39/52] feat: :sparkles: country enum is on coupon, and used as a backup --- .../Find/FindCampaignRequest.php | 1 - .../Find/Output/CampaignRequestFound.php | 1 - .../Models/Entities/CampaignRequest.php | 5 ---- .../Models/ValueObjects/CountryAuthorized.php | 23 ++++++++++++------- .../RequestedCoupon/RequestedCoupon.php | 2 ++ src/Shared/Enums/Country.php | 8 +++++++ .../Find/FindCampaignRequestTest.php | 5 +--- .../List/ListCampaignsRequestTest.php | 4 ++-- .../Reject/RejectCampaignRequestTest.php | 3 +-- .../Models/Entities/CampaignRequestTest.php | 14 +---------- .../RequestedCoupon/RequestedCouponTest.php | 8 +++++-- 11 files changed, 36 insertions(+), 38 deletions(-) diff --git a/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php index 7e28121..c4737e6 100644 --- a/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php +++ b/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php @@ -45,7 +45,6 @@ final readonly class FindCampaignRequest implements UseCase return new CampaignRequestFound( brand: $brand, applicant: $campaignRequest->applicant, - country: $campaignRequest->country, campaignType: $campaignRequest->campaignType, printRules: $campaignRequest->printRules, requestedCoupon: $campaignRequest->requestedCoupon, diff --git a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php index 08dafd7..13ef470 100644 --- a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php +++ b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php @@ -18,7 +18,6 @@ final readonly class CampaignRequestFound implements Output public function __construct( public Brand $brand, public Applicant $applicant, - public CountryAuthorized $country, public CampaignRequestType $campaignType, public ?PrintRules $printRules, public RequestedCoupon $requestedCoupon, diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index 399ac65..3a1674c 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -10,7 +10,6 @@ use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Shared\DDD\Models\AggregateRoot; @@ -26,7 +25,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent public CampaignRequestId $id, public BrandCode $code, public Applicant $applicant, - public CountryAuthorized $country, public CampaignRequestType $campaignType, public ?PrintRules $printRules, public RequestedCoupon $requestedCoupon, @@ -63,7 +61,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent CampaignRequestId $id, BrandCode $code, Applicant $applicant, - CountryAuthorized $country, CampaignRequestType $campaignType, ?PrintRules $printRules, RequestedCoupon $requestedCoupon, @@ -75,7 +72,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent id: $id, code: $code, applicant: $applicant, - country: $country, campaignType: $campaignType, printRules: $printRules, requestedCoupon: $requestedCoupon, @@ -95,7 +91,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent id: $this->id, code: $this->code, applicant: $this->applicant, - country: $this->country, campaignType: $this->campaignType, printRules: $this->printRules, requestedCoupon: $this->requestedCoupon, diff --git a/src/Domain/Models/ValueObjects/CountryAuthorized.php b/src/Domain/Models/ValueObjects/CountryAuthorized.php index 74c6515..948ab60 100644 --- a/src/Domain/Models/ValueObjects/CountryAuthorized.php +++ b/src/Domain/Models/ValueObjects/CountryAuthorized.php @@ -11,17 +11,21 @@ use Shared\Enums\Country; final readonly class CountryAuthorized implements \Stringable, Equatable { /** @var list */ - private const array AUTHORIZED = [ - Country::FRANCE, - Country::BELGIUM, - Country::LUXEMBOURG, - ]; + private static function authorized(): array + { + return [ + Country::FRANCE, + ...Country::benelux(), + ]; + } public function __construct( public Country $country, ) { - if (! in_array($this->country, self::AUTHORIZED, true)) { - throw new \DomainException(sprintf('Country "%s" is not authorized.', $this->country->name)); + if (! in_array($this->country, self::authorized(), true)) { + throw new \DomainException( + sprintf('Country "%s" is not authorized.', $this->country->name) + ); } } @@ -30,7 +34,10 @@ final readonly class CountryAuthorized implements \Stringable, Equatable */ public static function authorizedCountries(): array { - return self::AUTHORIZED; + return [ + Country::FRANCE, + ...Country::benelux(), + ]; } #[Override] diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php index 616c082..99485f5 100644 --- a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\RequestedCoupon; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; @@ -27,6 +28,7 @@ final readonly class RequestedCoupon public CouponPicture $couponPicture, public GS1 $gs1, public CouponValidity $couponValidity, + public CountryAuthorized $country, ) { } } diff --git a/src/Shared/Enums/Country.php b/src/Shared/Enums/Country.php index 857447a..b910688 100644 --- a/src/Shared/Enums/Country.php +++ b/src/Shared/Enums/Country.php @@ -583,4 +583,12 @@ enum Country: string { return self::ISOCODES; } + + public static function benelux(): array + { + return [ + self::BELGIUM, + self::LUXEMBOURG + ]; + } } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index afbf59f..e8eb36f 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -60,6 +60,7 @@ describe('find campaign request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), + new CountryAuthorized(Country::FRANCE) ); }); @@ -71,7 +72,6 @@ describe('find campaign request use case', function (): void { $id, $code, new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, @@ -101,7 +101,6 @@ describe('find campaign request use case', function (): void { expect($result)->toBeInstanceOf(CampaignRequestFound::class) ->and($result->brand)->toBe($brand) ->and($result->applicant)->toBe($campaignRequest->applicant) - ->and($result->country)->toBe($campaignRequest->country) ->and($result->campaignType)->toBe($campaignRequest->campaignType) ->and($result->printRules)->toBe($campaignRequest->printRules) ->and($result->requestedCoupon)->toBe($campaignRequest->requestedCoupon) @@ -118,7 +117,6 @@ describe('find campaign request use case', function (): void { $id, $code, new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, @@ -179,7 +177,6 @@ describe('find campaign request use case', function (): void { $id, $code, new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 0fb9f32..0b0d738 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -53,6 +53,7 @@ describe('list campaigns request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), + new CountryAuthorized(Country::FRANCE) ); $this->couponB = new RequestedCoupon( @@ -66,6 +67,7 @@ describe('list campaigns request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), + new CountryAuthorized(Country::FRANCE) ); }); @@ -76,7 +78,6 @@ describe('list campaigns request use case', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->couponA, @@ -86,7 +87,6 @@ describe('list campaigns request use case', function (): void { CampaignRequestId::generate(), new BrandCode('AAA02'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->couponB, diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index d4c6703..a80d22e 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -58,6 +58,7 @@ describe('reject campaign request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), + new CountryAuthorized(Country::FRANCE) ); }); @@ -69,7 +70,6 @@ describe('reject campaign request use case', function (): void { $id, new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, @@ -122,7 +122,6 @@ describe('reject campaign request use case', function (): void { $id, new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index df36484..0f06b44 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -53,6 +53,7 @@ describe('campaign request', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), + new CountryAuthorized(Country::FRANCE) ); }); @@ -61,7 +62,6 @@ describe('campaign request', function (): void { $id = CampaignRequestId::generate(); $code = new BrandCode('AAA01'); $applicant = new Applicant('John', 'Doe', new Email('john.doe@example.com')); - $country = new CountryAuthorized(Country::FRANCE); $campaignType = CampaignRequestType::CLUB; $printRules = new PrintRules(maxPerCustomer: 3); $requestedCoupon = $this->requestedCoupon; @@ -72,7 +72,6 @@ describe('campaign request', function (): void { $id, $code, $applicant, - $country, $campaignType, $printRules, $requestedCoupon, @@ -83,7 +82,6 @@ describe('campaign request', function (): void { expect($campaignRequest->id())->toBe($id) ->and($campaignRequest->code)->toBe($code) ->and($campaignRequest->applicant)->toBe($applicant) - ->and($campaignRequest->country)->toBe($country) ->and($campaignRequest->campaignType)->toBe($campaignType) ->and($campaignRequest->printRules)->toBe($printRules) ->and($campaignRequest->requestedCoupon)->toBe($requestedCoupon) @@ -98,7 +96,6 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, null, $this->requestedCoupon, @@ -118,7 +115,6 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, @@ -141,7 +137,6 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, @@ -160,7 +155,6 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, @@ -180,7 +174,6 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, @@ -202,7 +195,6 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, @@ -219,7 +211,6 @@ describe('campaign request', function (): void { ->and($rejected->id())->toBe($campaignRequest->id()) ->and($rejected->code)->toBe($campaignRequest->code) ->and($rejected->applicant)->toBe($campaignRequest->applicant) - ->and($rejected->country)->toBe($campaignRequest->country) ->and($rejected->campaignType)->toBe($campaignRequest->campaignType) ->and($rejected->printRules)->toBe($campaignRequest->printRules) ->and($rejected->requestedCoupon)->toBe($campaignRequest->requestedCoupon) @@ -233,7 +224,6 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, @@ -253,7 +243,6 @@ describe('campaign request', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, @@ -276,7 +265,6 @@ describe('campaign request', function (): void { $id, new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index f5107a9..ac4de15 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -19,7 +19,9 @@ use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Shared\Enums\Currency; use DateTime; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; +use Shared\Enums\Country; use Shared\ValueObjects\Money; covers(RequestedCoupon::class); @@ -41,7 +43,7 @@ describe('requested coupon', function (): void { $couponPicture = new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')); $gs1 = new GS1('01234567890128', BarcodeFormat::EAN13); $couponValidity = new CouponValidity(30, new DateTime()); - + $country = new CountryAuthorized(Country::FRANCE); // WHEN $coupon = new RequestedCoupon( $name, @@ -54,6 +56,7 @@ describe('requested coupon', function (): void { $couponPicture, $gs1, $couponValidity, + $country ); // THEN @@ -66,6 +69,7 @@ describe('requested coupon', function (): void { ->and($coupon->processingCode)->toBe($processingCode) ->and($coupon->couponPicture)->toBe($couponPicture) ->and($coupon->gs1)->toBe($gs1) - ->and($coupon->couponValidity)->toBe($couponValidity); + ->and($coupon->couponValidity)->toBe($couponValidity) + ->and($coupon->country)->toBe($country); }); }); From ca312020cdcfb6c2692844294304660ce9df5b7a Mon Sep 17 00:00:00 2001 From: K Muller Date: Tue, 29 Sep 2026 12:45:28 +0200 Subject: [PATCH 40/52] fix: :bug: change country and country authorized --- .../Find/Output/CampaignRequestFound.php | 1 - src/Domain/Models/Entities/Campaign.php | 8 +- .../Models/ValueObjects/CountryAuthorized.php | 53 -- .../Coupon/CouponLegalInformation.php | 4 +- .../ValueObjects/Coupon/CouponProductInfo.php | 4 +- .../RequestedCoupon/RequestedCoupon.php | 14 +- src/Shared/Enums/Country.php | 587 +----------------- .../Find/FindCampaignRequestTest.php | 3 +- .../List/ListCampaignsRequestTest.php | 5 +- .../Reject/RejectCampaignRequestTest.php | 3 +- tests/Unit/Domain/Enums/CountryTest.php | 3 +- .../Models/Entities/CampaignRequestTest.php | 3 +- .../Domain/Models/Entities/CampaignTest.php | 15 +- .../Domain/Models/Entities/CouponTest.php | 13 +- .../Models/ValueObjects/CountryAuthorized.php | 65 -- .../ValueObjects/CountryAuthorizedTest.php | 65 -- .../Coupon/CouponLegalInformationTest.php | 5 +- .../Coupon/CouponProductInfoTest.php | 5 +- .../RequestedCoupon/RequestedCouponTest.php | 3 +- 19 files changed, 45 insertions(+), 814 deletions(-) delete mode 100644 src/Domain/Models/ValueObjects/CountryAuthorized.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/CountryAuthorizedTest.php diff --git a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php index 13ef470..05af5aa 100644 --- a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php +++ b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php @@ -9,7 +9,6 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\Entities\Brand; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php index f517879..197a7dd 100644 --- a/src/Domain/Models/Entities/Campaign.php +++ b/src/Domain/Models/Entities/Campaign.php @@ -9,9 +9,9 @@ use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; -use Domain\Models\ValueObjects\CountryAuthorized; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; +use Shared\Enums\Country; /** * @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Campaign\CampaignId> @@ -25,7 +25,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier public CampaignName $name, public bool $publication, public bool $printAllAtOnce, - public CountryAuthorized $availableIn, + public Country $availableIn, /** @var array $coupons */ public array $coupons, public BrandCode $brand, @@ -62,7 +62,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier CampaignName $name, bool $publication, bool $printAllAtOnce, - CountryAuthorized $availableIn, + Country $availableIn, /** @var array $coupons */ array $coupons, BrandCode $brand, @@ -92,7 +92,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier CampaignName $name, bool $publication, bool $printAllAtOnce, - CountryAuthorized $availableIn, + Country $availableIn, array $coupons, BrandCode $brand, ?\DateTimeInterface $updatedAt = null, diff --git a/src/Domain/Models/ValueObjects/CountryAuthorized.php b/src/Domain/Models/ValueObjects/CountryAuthorized.php deleted file mode 100644 index 948ab60..0000000 --- a/src/Domain/Models/ValueObjects/CountryAuthorized.php +++ /dev/null @@ -1,53 +0,0 @@ - */ - private static function authorized(): array - { - return [ - Country::FRANCE, - ...Country::benelux(), - ]; - } - - public function __construct( - public Country $country, - ) { - if (! in_array($this->country, self::authorized(), true)) { - throw new \DomainException( - sprintf('Country "%s" is not authorized.', $this->country->name) - ); - } - } - - /** - * @return list - */ - public static function authorizedCountries(): array - { - return [ - Country::FRANCE, - ...Country::benelux(), - ]; - } - - #[Override] - public function equals(mixed $other): bool - { - return $other instanceof self && $this->country === $other->country; - } - - public function __toString(): string - { - return $this->country->value; - } -} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php index 0a2a344..5f18054 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Coupon; -use Domain\Models\ValueObjects\CountryAuthorized; use Shared\DDD\Models\Equatable; +use Shared\Enums\Country; final readonly class CouponLegalInformation implements Equatable { public function __construct( public string $legalText, - public CountryAuthorized $forLocale + public Country $forLocale ) { if (mb_trim($this->legalText) === '') { throw new \DomainException('Legal text cannot be empty.'); diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php index 853343f..2b00b78 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Coupon; -use Domain\Models\ValueObjects\CountryAuthorized; use Shared\DDD\Models\Equatable; +use Shared\Enums\Country; final readonly class CouponProductInfo implements Equatable { @@ -13,7 +13,7 @@ final readonly class CouponProductInfo implements Equatable public string $name, public string $range, public string $validityText, - public CountryAuthorized $forLocale + public Country $forLocale ) { $trimmedName = mb_trim($this->name); diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php index 99485f5..78b1bd7 100644 --- a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\RequestedCoupon; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; @@ -14,9 +13,11 @@ use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\WarningThresholdQuantity; +use Shared\Enums\Country; final readonly class RequestedCoupon { + /** @var array $country */ public function __construct( public CouponName $name, public CouponProductName $productName, @@ -26,9 +27,16 @@ final readonly class RequestedCoupon public MoneyAmount $moneyAmount, public ProcessingCode $processingCode, public CouponPicture $couponPicture, - public GS1 $gs1, + public ?GS1 $gs1, public CouponValidity $couponValidity, - public CountryAuthorized $country, + public array $country, ) { + // TODO: règle de validation ici pour Country + // Par défaut dans le tableau country il y aura Country::FRANCE + // Si GS1 est non null alors il ya aura aussi Country::BENELUX + // ça se passera au moment de la création donc peut être plutôt dans l'entité associée ? + + + // JE viens de passer GS1 en nullable, donc il faudra peut être tester } } diff --git a/src/Shared/Enums/Country.php b/src/Shared/Enums/Country.php index b910688..218dd84 100644 --- a/src/Shared/Enums/Country.php +++ b/src/Shared/Enums/Country.php @@ -4,591 +4,8 @@ declare(strict_types=1); namespace Shared\Enums; -use Shared\DDD\Exceptions\UnknownCountryIsoCodeException; - enum Country: string { - case AFGHANISTAN = 'afghanistan'; - case ALAND_ISLANDS = 'alandIslands'; - case ALBANIA = 'albania'; - case ALGERIA = 'algeria'; - case AMERICAN_SAMOA = 'americanSamoa'; - case ANDORRA = 'andorra'; - case ANGOLA = 'angola'; - case ANGUILLA = 'anguilla'; - case ANTARCTICA = 'antarctica'; - case ANTIGUA_AND_BARBUDA = 'antiguaAndBarbuda'; - case ARGENTINA = 'argentina'; - case ARMENIA = 'armenia'; - case ARUBA = 'aruba'; - case AUSTRALIA = 'australia'; - case AUSTRIA = 'austria'; - case AZERBAIJAN = 'azerbaijan'; - case BAHRAIN = 'bahrain'; - case BAHAMAS = 'bahamas'; - case BANGLADESH = 'bangladesh'; - case BARBADOS = 'barbados'; - case BELARUS = 'belarus'; - case BELGIUM = 'belgium'; - case BELIZE = 'belize'; - case BENIN = 'benin'; - case BERMUDA = 'bermuda'; - case BHUTAN = 'bhutan'; - case BOLIVIA_PLURINATIONAL_STATE_OF = 'boliviaPlurinationalStateOf'; - case BONAIRE_SINT_EUSTATIUS_AND_SABA = 'bonaireSintEustatiusAndSaba'; - case BOSNIA_AND_HERZEGOVINA = 'bosniaAndHerzegovina'; - case BOTSWANA = 'botswana'; - case BOUVET_ISLAND = 'bouvetIsland'; - case BRAZIL = 'brazil'; - case BRITISH_INDIAN_OCEAN_TERRITORY = 'britishIndianOceanTerritory'; - case BRUNEI_DARUSSALAM = 'bruneiDarussalam'; - case BULGARIA = 'bulgaria'; - case BURKINA_FASO = 'burkinaFaso'; - case BURUNDI = 'burundi'; - case CAMBODIA = 'cambodia'; - case CAMEROON = 'cameroon'; - case CANADA = 'canada'; - case CAPE_VERDE = 'capeVerde'; - case CAYMAN_ISLANDS = 'caymanIslands'; - case CENTRAL_AFRICAN_REPUBLIC = 'centralAfricanRepublic'; - case CHAD = 'chad'; - case CHILE = 'chile'; - case CHINA = 'china'; - case CHRISTMAS_ISLAND = 'christmasIsland'; - case COCOS_KEELING_ISLANDS = 'cocosKeelingIslands'; - case COLOMBIA = 'colombia'; - case COMOROS = 'comoros'; - case CONGO = 'congo'; - case CONGO_THE_DEMOCRATIC_REPUBLIC_OF_THE = 'congoTheDemocraticRepublicOfThe'; - case COOK_ISLANDS = 'cookIslands'; - case COSTA_RICA = 'costaRica'; - case COTE_DIVOIRE = 'coteDivoire'; - case CROATIA = 'croatia'; - case CUBA = 'cuba'; - case CURACAO = 'curacao'; - case CYPRUS = 'cyprus'; - case CZECH_REPUBLIC = 'czechRepublic'; - case DENMARK = 'denmark'; - case DJIBOUTI = 'djibouti'; - case DOMINICA = 'dominica'; - case DOMINICAN_REPUBLIC = 'dominicanRepublic'; - case ECUADOR = 'ecuador'; - case EGYPT = 'egypt'; - case EL_SALVADOR = 'elSalvador'; - case EQUATORIAL_GUINEA = 'equatorialGuinea'; - case ERITREA = 'eritrea'; - case ESTONIA = 'estonia'; - case ETHIOPIA = 'ethiopia'; - case FALKLAND_ISLANDS_MALVINAS = 'falklandIslandsMalvinas'; - case FAROE_ISLANDS = 'faroeIslands'; - case FIJI = 'fiji'; - case FINLAND = 'finland'; - case FRANCE = 'france'; - case FRENCH_GUIANA = 'frenchGuiana'; - case FRENCH_POLYNESIA = 'frenchPolynesia'; - case FRENCH_SOUTHERN_TERRITORIES = 'frenchSouthernTerritories'; - case GABON = 'gabon'; - case GAMBIA = 'gambia'; - case GEORGIA = 'georgia'; - case GERMANY = 'germany'; - case GHANA = 'ghana'; - case GIBRALTAR = 'gibraltar'; - case GREECE = 'greece'; - case GREENLAND = 'greenland'; - case GRENADA = 'grenada'; - case GUADELOUPE = 'guadeloupe'; - case GUAM = 'guam'; - case GUATEMALA = 'guatemala'; - case GUERNSEY = 'guernsey'; - case GUINEA = 'guinea'; - case GUINEA_BISSAU = 'guineaBissau'; - case GUYANA = 'guyana'; - case HAITI = 'haiti'; - case HEARD_ISLAND_AND_MCDONALD_ISLANDS = 'heardIslandAndMcdonaldIslands'; - case HOLY_SEE_VATICAN_CITY_STATE = 'holySeeVaticanCityState'; - case HONDURAS = 'honduras'; - case HONG_KONG = 'hongKong'; - case HUNGARY = 'hungary'; - case ICELAND = 'iceland'; - case INDIA = 'india'; - case INDONESIA = 'indonesia'; - case IRAN_ISLAMIC_REPUBLIC_OF = 'iranIslamicRepublicOf'; - case IRAQ = 'iraq'; - case IRELAND = 'ireland'; - case ISLE_OF_MAN = 'isleOfMan'; - case ISRAEL = 'israel'; - case ITALY = 'italy'; - case JAMAICA = 'jamaica'; - case JAPAN = 'japan'; - case JERSEY = 'jersey'; - case JORDAN = 'jordan'; - case KAZAKHSTAN = 'kazakhstan'; - case KENYA = 'kenya'; - case KIRIBATI = 'kiribati'; - case KOREA_DEMOCRATIC_PEOPLES_REPUBLIC_OF = 'koreaDemocraticPeoplesRepublicOf'; - case KOREA_REPUBLIC_OF = 'koreaRepublicOf'; - case KUWAIT = 'kuwait'; - case KYRGYZSTAN = 'kyrgyzstan'; - case LAO_PEOPLES_DEMOCRATIC_REPUBLIC = 'laoPeoplesDemocraticRepublic'; - case LATVIA = 'latvia'; - case LEBANON = 'lebanon'; - case LESOTHO = 'lesotho'; - case LIBERIA = 'liberia'; - case LIBYA = 'libya'; - case LIECHTENSTEIN = 'liechtenstein'; - case LITHUANIA = 'lithuania'; - case LUXEMBOURG = 'luxembourg'; - case MACAO = 'macao'; - case MACEDONIA_THE_FORMER_YUGOSLAV_REPUBLIC_OF = 'macedoniaTheFormerYugoslavRepublicOf'; - case MADAGASCAR = 'madagascar'; - case MALAWI = 'malawi'; - case MALAYSIA = 'malaysia'; - case MALDIVES = 'maldives'; - case MALI = 'mali'; - case MALTA = 'malta'; - case MARSHALL_ISLANDS = 'marshallIslands'; - case MARTINIQUE = 'martinique'; - case MAURITANIA = 'mauritania'; - case MAURITIUS = 'mauritius'; - case MAYOTTE = 'mayotte'; - case MEXICO = 'mexico'; - case MICRONESIA_FEDERATED_STATES_OF = 'micronesiaFederatedStatesOf'; - case MOLDOVA_REPUBLIC_OF = 'moldovaRepublicOf'; - case MONACO = 'monaco'; - case MONGOLIA = 'mongolia'; - case MONTENEGRO = 'montenegro'; - case MONTSERRAT = 'montserrat'; - case MOROCCO = 'morocco'; - case MOZAMBIQUE = 'mozambique'; - case MYANMAR = 'myanmar'; - case NAMIBIA = 'namibia'; - case NAURU = 'nauru'; - case NEPAL = 'nepal'; - case NETHERLANDS = 'netherlands'; - case NEW_CALEDONIA = 'newCaledonia'; - case NEW_ZEALAND = 'newZealand'; - case NICARAGUA = 'nicaragua'; - case NIGER = 'niger'; - case NIGERIA = 'nigeria'; - case NIUE = 'niue'; - case NORFOLK_ISLAND = 'norfolkIsland'; - case NORTHERN_MARIANA_ISLANDS = 'northernMarianaIslands'; - case NORWAY = 'norway'; - case OMAN = 'oman'; - case PAKISTAN = 'pakistan'; - case PALAU = 'palau'; - case PALESTINE_STATE_OF = 'palestineStateOf'; - case PANAMA = 'panama'; - case PAPUA_NEW_GUINEA = 'papuaNewGuinea'; - case PARAGUAY = 'paraguay'; - case PERU = 'peru'; - case PHILIPPINES = 'philippines'; - case PITCAIRN = 'pitcairn'; - case POLAND = 'poland'; - case PORTUGAL = 'portugal'; - case PUERTO_RICO = 'puertoRico'; - case QATAR = 'qatar'; - case REUNION = 'reunion'; - case ROMANIA = 'romania'; - case RUSSIAN_FEDERATION = 'russianFederation'; - case RWANDA = 'rwanda'; - case SAINT_BARTHELEMY = 'saintBarthelemy'; - case SAINT_HELENA_ASCENSION_AND_TRISTAN_DA_CUNHA = 'saintHelenaAscensionAndTristanDaCunha'; - case SAINT_KITTS_AND_NEVIS = 'saintKittsAndNevis'; - case SAINT_LUCIA = 'saintLucia'; - case SAINT_MARTIN_FRENCH_PART = 'saintMartinFrenchPart'; - case SAINT_PIERRE_AND_MIQUELON = 'saintPierreAndMiquelon'; - case SAINT_VINCENT_AND_THE_GRENADINES = 'saintVincentAndTheGrenadines'; - case SAMOA = 'samoa'; - case SAN_MARINO = 'sanMarino'; - case SAO_TOME_AND_PRINCIPE = 'saoTomeAndPrincipe'; - case SAUDI_ARABIA = 'saudiArabia'; - case SENEGAL = 'senegal'; - case SERBIA = 'serbia'; - case SEYCHELLES = 'seychelles'; - case SIERRA_LEONE = 'sierraLeone'; - case SINGAPORE = 'singapore'; - case SINT_MAARTEN_DUTCH_PART = 'sintMaartenDutchPart'; - case SLOVAKIA = 'slovakia'; - case SLOVENIA = 'slovenia'; - case SOLOMON_ISLANDS = 'solomonIslands'; - case SOMALIA = 'somalia'; - case SOUTH_AFRICA = 'southAfrica'; - case SOUTH_GEORGIA_AND_THE_SOUTH_SANDWICH_ISLANDS = 'southGeorgiaAndTheSouthSandwichIslands'; - case SOUTH_SUDAN = 'southSudan'; - case SPAIN = 'spain'; - case SRI_LANKA = 'sriLanka'; - case SUDAN = 'sudan'; - case SURINAME = 'suriname'; - case SVALBARD_AND_JAN_MAYEN = 'svalbardAndJanMayen'; - case SWAZILAND = 'swaziland'; - case SWEDEN = 'sweden'; - case SWITZERLAND = 'switzerland'; - case SYRIAN_ARAB_REPUBLIC = 'syrianArabRepublic'; - case TAIWAN_PROVINCE_OF_CHINA = 'taiwanProvinceOfChina'; - case TAJIKISTAN = 'tajikistan'; - case TANZANIA_UNITED_REPUBLIC_OF = 'tanzaniaUnitedRepublicOf'; - case THAILAND = 'thailand'; - case TIMOR_LESTE = 'timorLeste'; - case TOGO = 'togo'; - case TOKELAU = 'tokelau'; - case TONGA = 'tonga'; - case TRINIDAD_AND_TOBAGO = 'trinidadAndTobago'; - case TUNISIA = 'tunisia'; - case TURKEY = 'turkey'; - case TURKMENISTAN = 'turkmenistan'; - case TURKS_AND_CAICOS_ISLANDS = 'turksAndCaicosIslands'; - case TUVALU = 'tuvalu'; - case UGANDA = 'uganda'; - case UKRAINE = 'ukraine'; - case UNITED_ARAB_EMIRATES = 'unitedArabEmirates'; - case UNITED_KINGDOM = 'unitedKingdom'; - case UNITED_STATES = 'unitedStates'; - case UNITED_STATES_MINOR_OUTLYING_ISLANDS = 'unitedStatesMinorOutlyingIslands'; - case URUGUAY = 'uruguay'; - case UZBEKISTAN = 'uzbekistan'; - case VANUATU = 'vanuatu'; - case VENEZUELA_BOLIVARIAN_REPUBLIC_OF = 'venezuelaBolivarianRepublicOf'; - case VIET_NAM = 'vietNam'; - case VIRGIN_ISLANDS_BRITISH = 'virginIslandsBritish'; - case VIRGIN_ISLANDS_US = 'virginIslandsUs'; - case WALLIS_AND_FUTUNA = 'wallisAndFutuna'; - case WESTERN_SAHARA = 'westernSahara'; - case YEMEN = 'yemen'; - case ZAMBIA = 'zambia'; - case ZIMBABWE = 'zimbabwe'; - - /** - * Optionnal labels definition - * - * @phpstan-ignore-next-line - */ - private const LABELS = [ - self::AFGHANISTAN->value => 'Afghanistan', - self::ALAND_ISLANDS->value => 'Åland Islands', - self::ALBANIA->value => 'Albania', - self::ALGERIA->value => 'Algeria', - self::AMERICAN_SAMOA->value => 'American Samoa', - self::ANDORRA->value => 'Andorra', - self::ANGOLA->value => 'Angola', - self::ANGUILLA->value => 'Anguilla', - self::ANTARCTICA->value => 'Antarctica', - self::ANTIGUA_AND_BARBUDA->value => 'Antigua and Barbuda', - self::ARGENTINA->value => 'Argentina', - self::ARMENIA->value => 'Armenia', - self::ARUBA->value => 'Aruba', - self::AUSTRALIA->value => 'Australia', - self::AUSTRIA->value => 'Austria', - self::AZERBAIJAN->value => 'Azerbaijan', - self::BAHRAIN->value => 'Bahrain', - self::BAHAMAS->value => 'Bahamas', - self::BANGLADESH->value => 'Bangladesh', - self::BARBADOS->value => 'Barbados', - self::BELARUS->value => 'Belarus', - self::BELGIUM->value => 'Belgium', - self::BELIZE->value => 'Belize', - self::BENIN->value => 'Benin', - self::BERMUDA->value => 'Bermuda', - self::BHUTAN->value => 'Bhutan', - self::BOLIVIA_PLURINATIONAL_STATE_OF->value => 'Bolivia, Plurinational State of', - self::BONAIRE_SINT_EUSTATIUS_AND_SABA->value => 'Bonaire, Sint Eustatius and Saba', - self::BOSNIA_AND_HERZEGOVINA->value => 'Bosnia and Herzegovina', - self::BOTSWANA->value => 'Botswana', - self::BOUVET_ISLAND->value => 'Bouvet Island', - self::BRAZIL->value => 'Brazil', - self::BRITISH_INDIAN_OCEAN_TERRITORY->value => 'British Indian Ocean Territory', - self::BRUNEI_DARUSSALAM->value => 'Brunei Darussalam', - self::BULGARIA->value => 'Bulgaria', - self::BURKINA_FASO->value => 'Burkina Faso', - self::BURUNDI->value => 'Burundi', - self::CAMBODIA->value => 'Cambodia', - self::CAMEROON->value => 'Cameroon', - self::CANADA->value => 'Canada', - self::CAPE_VERDE->value => 'Cape Verde', - self::CAYMAN_ISLANDS->value => 'Cayman Islands', - self::CENTRAL_AFRICAN_REPUBLIC->value => 'Central African Republic', - self::CHAD->value => 'Chad', - self::CHILE->value => 'Chile', - self::CHINA->value => 'China', - self::CHRISTMAS_ISLAND->value => 'Christmas Island', - self::COCOS_KEELING_ISLANDS->value => 'Cocos (Keeling) Islands', - self::COLOMBIA->value => 'Colombia', - self::COMOROS->value => 'Comoros', - self::CONGO->value => 'Congo', - self::CONGO_THE_DEMOCRATIC_REPUBLIC_OF_THE->value => 'Congo, the Democratic Republic of the', - self::COOK_ISLANDS->value => 'Cook Islands', - self::COSTA_RICA->value => 'Costa Rica', - self::COTE_DIVOIRE->value => 'Côte d\'Ivoire', - self::CROATIA->value => 'Croatia', - self::CUBA->value => 'Cuba', - self::CURACAO->value => 'Curaçao', - self::CYPRUS->value => 'Cyprus', - self::CZECH_REPUBLIC->value => 'Czech Republic', - self::DENMARK->value => 'Denmark', - self::DJIBOUTI->value => 'Djibouti', - self::DOMINICA->value => 'Dominica', - self::DOMINICAN_REPUBLIC->value => 'Dominican Republic', - self::ECUADOR->value => 'Ecuador', - self::EGYPT->value => 'Egypt', - self::EL_SALVADOR->value => 'El Salvador', - self::EQUATORIAL_GUINEA->value => 'Equatorial Guinea', - self::ERITREA->value => 'Eritrea', - self::ESTONIA->value => 'Estonia', - self::ETHIOPIA->value => 'Ethiopia', - self::FALKLAND_ISLANDS_MALVINAS->value => 'Falkland Islands (Malvinas)', - self::FAROE_ISLANDS->value => 'Faroe Islands', - self::FIJI->value => 'Fiji', - self::FINLAND->value => 'Finland', - self::FRANCE->value => 'France', - self::FRENCH_GUIANA->value => 'French Guiana', - self::FRENCH_POLYNESIA->value => 'French Polynesia', - self::FRENCH_SOUTHERN_TERRITORIES->value => 'French Southern Territories', - self::GABON->value => 'Gabon', - self::GAMBIA->value => 'Gambia', - self::GEORGIA->value => 'Georgia', - self::GERMANY->value => 'Germany', - self::GHANA->value => 'Ghana', - self::GIBRALTAR->value => 'Gibraltar', - self::GREECE->value => 'Greece', - self::GREENLAND->value => 'Greenland', - self::GRENADA->value => 'Grenada', - self::GUADELOUPE->value => 'Guadeloupe', - self::GUAM->value => 'Guam', - self::GUATEMALA->value => 'Guatemala', - self::GUERNSEY->value => 'Guernsey', - self::GUINEA->value => 'Guinea', - self::GUINEA_BISSAU->value => 'GuineaBissau', - self::GUYANA->value => 'Guyana', - self::HAITI->value => 'Haiti', - self::HEARD_ISLAND_AND_MCDONALD_ISLANDS->value => 'Heard Island and McDonald Islands', - self::HOLY_SEE_VATICAN_CITY_STATE->value => 'Holy See (Vatican City State)', - self::HONDURAS->value => 'Honduras', - self::HONG_KONG->value => 'Hong Kong', - self::HUNGARY->value => 'Hungary', - self::ICELAND->value => 'Iceland', - self::INDIA->value => 'India', - self::INDONESIA->value => 'Indonesia', - self::IRAN_ISLAMIC_REPUBLIC_OF->value => 'Iran, Islamic Republic of', - self::IRAQ->value => 'Iraq', - self::IRELAND->value => 'Ireland', - self::ISLE_OF_MAN->value => 'Isle of Man', - self::ISRAEL->value => 'Israel', - self::ITALY->value => 'Italy', - self::JAMAICA->value => 'Jamaica', - self::JAPAN->value => 'Japan', - self::JERSEY->value => 'Jersey', - self::JORDAN->value => 'Jordan', - self::KAZAKHSTAN->value => 'Kazakhstan', - self::KENYA->value => 'Kenya', - self::KIRIBATI->value => 'Kiribati', - self::KOREA_DEMOCRATIC_PEOPLES_REPUBLIC_OF->value => 'Korea, Democratic People\'s Republic of', - self::KOREA_REPUBLIC_OF->value => 'Korea, Republic of', - self::KUWAIT->value => 'Kuwait', - self::KYRGYZSTAN->value => 'Kyrgyzstan', - self::LAO_PEOPLES_DEMOCRATIC_REPUBLIC->value => 'Lao People\'s Democratic Republic', - self::LATVIA->value => 'Latvia', - self::LEBANON->value => 'Lebanon', - self::LESOTHO->value => 'Lesotho', - self::LIBERIA->value => 'Liberia', - self::LIBYA->value => 'Libya', - self::LIECHTENSTEIN->value => 'Liechtenstein', - self::LITHUANIA->value => 'Lithuania', - self::LUXEMBOURG->value => 'Luxembourg', - self::MACAO->value => 'Macao', - self::MACEDONIA_THE_FORMER_YUGOSLAV_REPUBLIC_OF->value => 'Macedonia, the Former Yugoslav Republic of', - self::MADAGASCAR->value => 'Madagascar', - self::MALAWI->value => 'Malawi', - self::MALAYSIA->value => 'Malaysia', - self::MALDIVES->value => 'Maldives', - self::MALI->value => 'Mali', - self::MALTA->value => 'Malta', - self::MARSHALL_ISLANDS->value => 'Marshall Islands', - self::MARTINIQUE->value => 'Martinique', - self::MAURITANIA->value => 'Mauritania', - self::MAURITIUS->value => 'Mauritius', - self::MAYOTTE->value => 'Mayotte', - self::MEXICO->value => 'Mexico', - self::MICRONESIA_FEDERATED_STATES_OF->value => 'Micronesia, Federated States of', - self::MOLDOVA_REPUBLIC_OF->value => 'Moldova, Republic of', - self::MONACO->value => 'Monaco', - self::MONGOLIA->value => 'Mongolia', - self::MONTENEGRO->value => 'Montenegro', - self::MONTSERRAT->value => 'Montserrat', - self::MOROCCO->value => 'Morocco', - self::MOZAMBIQUE->value => 'Mozambique', - self::MYANMAR->value => 'Myanmar', - self::NAMIBIA->value => 'Namibia', - self::NAURU->value => 'Nauru', - self::NEPAL->value => 'Nepal', - self::NETHERLANDS->value => 'Netherlands', - self::NEW_CALEDONIA->value => 'New Caledonia', - self::NEW_ZEALAND->value => 'New Zealand', - self::NICARAGUA->value => 'Nicaragua', - self::NIGER->value => 'Niger', - self::NIGERIA->value => 'Nigeria', - self::NIUE->value => 'Niue', - self::NORFOLK_ISLAND->value => 'Norfolk Island', - self::NORTHERN_MARIANA_ISLANDS->value => 'Northern Mariana Islands', - self::NORWAY->value => 'Norway', - self::OMAN->value => 'Oman', - self::PAKISTAN->value => 'Pakistan', - self::PALAU->value => 'Palau', - self::PALESTINE_STATE_OF->value => 'Palestine, State of', - self::PANAMA->value => 'Panama', - self::PAPUA_NEW_GUINEA->value => 'Papua New Guinea', - self::PARAGUAY->value => 'Paraguay', - self::PERU->value => 'Peru', - self::PHILIPPINES->value => 'Philippines', - self::PITCAIRN->value => 'Pitcairn', - self::POLAND->value => 'Poland', - self::PORTUGAL->value => 'Portugal', - self::PUERTO_RICO->value => 'Puerto Rico', - self::QATAR->value => 'Qatar', - self::REUNION->value => 'Réunion', - self::ROMANIA->value => 'Romania', - self::RUSSIAN_FEDERATION->value => 'Russian Federation', - self::RWANDA->value => 'Rwanda', - self::SAINT_BARTHELEMY->value => 'Saint Barthélemy', - self::SAINT_HELENA_ASCENSION_AND_TRISTAN_DA_CUNHA->value => 'Saint Helena, Ascension and Tristan da Cunha', - self::SAINT_KITTS_AND_NEVIS->value => 'Saint Kitts and Nevis', - self::SAINT_LUCIA->value => 'Saint Lucia', - self::SAINT_MARTIN_FRENCH_PART->value => 'Saint Martin (French part)', - self::SAINT_PIERRE_AND_MIQUELON->value => 'Saint Pierre and Miquelon', - self::SAINT_VINCENT_AND_THE_GRENADINES->value => 'Saint Vincent and the Grenadines', - self::SAMOA->value => 'Samoa', - self::SAN_MARINO->value => 'San Marino', - self::SAO_TOME_AND_PRINCIPE->value => 'Sao Tome and Principe', - self::SAUDI_ARABIA->value => 'Saudi Arabia', - self::SENEGAL->value => 'Senegal', - self::SERBIA->value => 'Serbia', - self::SEYCHELLES->value => 'Seychelles', - self::SIERRA_LEONE->value => 'Sierra Leone', - self::SINGAPORE->value => 'Singapore', - self::SINT_MAARTEN_DUTCH_PART->value => 'Sint Maarten (Dutch part)', - self::SLOVAKIA->value => 'Slovakia', - self::SLOVENIA->value => 'Slovenia', - self::SOLOMON_ISLANDS->value => 'Solomon Islands', - self::SOMALIA->value => 'Somalia', - self::SOUTH_AFRICA->value => 'South Africa', - self::SOUTH_GEORGIA_AND_THE_SOUTH_SANDWICH_ISLANDS->value => 'South Georgia and the South Sandwich Islands', - self::SOUTH_SUDAN->value => 'South Sudan', - self::SPAIN->value => 'Spain', - self::SRI_LANKA->value => 'Sri Lanka', - self::SUDAN->value => 'Sudan', - self::SURINAME->value => 'Suriname', - self::SVALBARD_AND_JAN_MAYEN->value => 'Svalbard and Jan Mayen', - self::SWAZILAND->value => 'Swaziland', - self::SWEDEN->value => 'Sweden', - self::SWITZERLAND->value => 'Switzerland', - self::SYRIAN_ARAB_REPUBLIC->value => 'Syrian Arab Republic', - self::TAIWAN_PROVINCE_OF_CHINA->value => 'Taiwan, Province of China', - self::TAJIKISTAN->value => 'Tajikistan', - self::TANZANIA_UNITED_REPUBLIC_OF->value => 'Tanzania, United Republic of', - self::THAILAND->value => 'Thailand', - self::TIMOR_LESTE->value => 'TimorLeste', - self::TOGO->value => 'Togo', - self::TOKELAU->value => 'Tokelau', - self::TONGA->value => 'Tonga', - self::TRINIDAD_AND_TOBAGO->value => 'Trinidad and Tobago', - self::TUNISIA->value => 'Tunisia', - self::TURKEY->value => 'Turkey', - self::TURKMENISTAN->value => 'Turkmenistan', - self::TURKS_AND_CAICOS_ISLANDS->value => 'Turks and Caicos Islands', - self::TUVALU->value => 'Tuvalu', - self::UGANDA->value => 'Uganda', - self::UKRAINE->value => 'Ukraine', - self::UNITED_ARAB_EMIRATES->value => 'United Arab Emirates', - self::UNITED_KINGDOM->value => 'United Kingdom', - self::UNITED_STATES->value => 'United States', - self::UNITED_STATES_MINOR_OUTLYING_ISLANDS->value => 'United States Minor Outlying Islands', - self::URUGUAY->value => 'Uruguay', - self::UZBEKISTAN->value => 'Uzbekistan', - self::VANUATU->value => 'Vanuatu', - self::VENEZUELA_BOLIVARIAN_REPUBLIC_OF->value => 'Venezuela, Bolivarian Republic of', - self::VIET_NAM->value => 'Viet Nam', - self::VIRGIN_ISLANDS_BRITISH->value => 'Virgin Islands, British', - self::VIRGIN_ISLANDS_US->value => 'Virgin Islands, U.S.', - self::WALLIS_AND_FUTUNA->value => 'Wallis and Futuna', - self::WESTERN_SAHARA->value => 'Western Sahara', - self::YEMEN->value => 'Yemen', - self::ZAMBIA->value => 'Zambia', - self::ZIMBABWE->value => 'Zimbabwe', - ]; - /** - * Custom added ISOCODES definition - */ - private const ISOCODES = [ - self::FRANCE->name => 'FR', - self::GUADELOUPE->name => 'GSP', - self::MARTINIQUE->name => 'MQ', - self::FRENCH_GUIANA->name => 'GF', - self::REUNION->name => 'RE', - self::SAINT_PIERRE_AND_MIQUELON->name => 'PM', - self::MAYOTTE->name => 'YT', - self::SAINT_BARTHELEMY->name => 'BL', - self::SAINT_MARTIN_FRENCH_PART->name => 'MF', - self::WALLIS_AND_FUTUNA->name => 'WF', - self::FRENCH_POLYNESIA->name => 'PF', - self::NEW_CALEDONIA->name => 'NC', - ]; - - /** - * Custom added Numerical ISOCODES definition - */ - private const ISOCODES_NUM = [ - self::FRANCE->name => 250, - self::GUADELOUPE->name => 312, - self::MARTINIQUE->name => 474, - self::FRENCH_GUIANA->name => 254, - self::REUNION->name => 638, - self::SAINT_PIERRE_AND_MIQUELON->name => 666, - self::MAYOTTE->name => 175, - self::SAINT_BARTHELEMY->name => 652, - self::SAINT_MARTIN_FRENCH_PART->name => 663, - self::WALLIS_AND_FUTUNA->name => 876, - self::FRENCH_POLYNESIA->name => 258, - self::NEW_CALEDONIA->name => 540, - ]; - - /** - * Get ISOCODE - */ - public function iso(): string - { - return self::ISOCODES[$this->name] ?? throw UnknownCountryIsoCodeException::fromCountry($this); - } - - /** - * Get ISOCODE NUM - */ - public function isoNum(): int - { - return self::ISOCODES_NUM[$this->name] ?? throw UnknownCountryIsoCodeException::fromCountry($this); - } - - /** - * Get ISOCODE - */ - public function isoCode(): ?string - { - return self::ISOCODES[$this->name] ?? null; - } - - /** - * Get enum ISOCODES - * - * @return array - */ - public static function toIsoCodes(): array - { - return self::ISOCODES; - } - - public static function benelux(): array - { - return [ - self::BELGIUM, - self::LUXEMBOURG - ]; - } + case FRANCE = 'france'; + case BENELUX = 'benelux'; } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index e8eb36f..33abf38 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -22,7 +22,6 @@ use Domain\Models\ValueObjects\Brand\BrandName; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; @@ -60,7 +59,7 @@ describe('find campaign request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 0b0d738..15aa7d6 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -16,7 +16,6 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortClause; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; @@ -53,7 +52,7 @@ describe('list campaigns request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ); $this->couponB = new RequestedCoupon( @@ -67,7 +66,7 @@ describe('list campaigns request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index a80d22e..265eb36 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -19,7 +19,6 @@ use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; @@ -58,7 +57,7 @@ describe('reject campaign request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ); }); diff --git a/tests/Unit/Domain/Enums/CountryTest.php b/tests/Unit/Domain/Enums/CountryTest.php index 20c5a31..5a6c7b8 100644 --- a/tests/Unit/Domain/Enums/CountryTest.php +++ b/tests/Unit/Domain/Enums/CountryTest.php @@ -12,8 +12,7 @@ describe('country', function (): void { it('has the expected cases with their backing values', function (): void { // THEN expect(Country::FRANCE->value)->toBe('france') - ->and(Country::BELGIUM->value)->toBe('belgium') - ->and(Country::LUXEMBOURG->value)->toBe('luxembourg'); + ->and(Country::BENELUX->value)->toBe('benelux'); }); it('exposes its case name', function (): void { diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 0f06b44..d3bc3a6 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -29,7 +29,6 @@ use DateTime; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; use Carbon\CarbonImmutable; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; @@ -53,7 +52,7 @@ describe('campaign request', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ); }); diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index b6caa2e..25e96ba 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -13,7 +13,6 @@ use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\Stock\StockId; @@ -43,7 +42,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = new CountryAuthorized(Country::FRANCE); + $availableIn = Country::FRANCE; $coupons = [ new CampaignCoupon( new ProcessingCode('PC1'), @@ -88,7 +87,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = new CountryAuthorized(Country::FRANCE); + $availableIn = Country::FRANCE; $coupons = []; $brand = new BrandCode('AAA01'); @@ -116,7 +115,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = new CountryAuthorized(Country::FRANCE); + $availableIn = Country::FRANCE; $coupons = [ new CampaignCoupon( new ProcessingCode('PC1'), @@ -153,7 +152,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = new CountryAuthorized(Country::FRANCE); + $availableIn = Country::FRANCE; $coupons = [ new CampaignCoupon( new ProcessingCode('PC1'), @@ -196,7 +195,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = new CountryAuthorized(Country::FRANCE); + $availableIn = Country::FRANCE; $coupons = [ new CampaignCoupon( new ProcessingCode('PC1'), @@ -244,7 +243,7 @@ describe('campaign', function (): void { new CampaignName('Name'), \boolval(0), \boolval(1), - new CountryAuthorized(Country::FRANCE), + Country::FRANCE, [ new CampaignCoupon( new ProcessingCode('PC1'), @@ -270,7 +269,7 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = new CountryAuthorized(Country::FRANCE); + $availableIn = Country::FRANCE; $coupons = ['PC1']; $brand = new BrandCode('AAA01'); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index 89ae1d9..97c243c 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -10,7 +10,6 @@ use Shared\Enums\Country; use Domain\Models\Entities\Coupon; use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; use Domain\Models\ValueObjects\Coupon\CouponId; @@ -52,7 +51,7 @@ describe('coupon', function (): void { $legalInformation = [ new CouponLegalInformation( 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ) ]; $code = new ProcessingCode('5425003944130'); @@ -114,7 +113,7 @@ describe('coupon', function (): void { $legalInformation = [ new CouponLegalInformation( 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ) ]; $code = new ProcessingCode('5425003944130'); @@ -173,7 +172,7 @@ describe('coupon', function (): void { $legalInformation = [ new CouponLegalInformation( 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ) ]; $code = new ProcessingCode('5425003944130'); @@ -221,7 +220,7 @@ describe('coupon', function (): void { $legalInformation = [ new CouponLegalInformation( 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ) ]; $code = new ProcessingCode('5425003944130'); @@ -281,7 +280,7 @@ describe('coupon', function (): void { $legalInformation = [ new CouponLegalInformation( 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ) ]; $code = new ProcessingCode('5425003944130'); @@ -342,7 +341,7 @@ describe('coupon', function (): void { 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.', new CountryAuthorized(Country::FRANCE))], + [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], new ProcessingCode('5425003944130'), new BrandCode('AAA01'), ActorId::generate(), diff --git a/tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php b/tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php deleted file mode 100644 index 9bb5e8b..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/CountryAuthorized.php +++ /dev/null @@ -1,65 +0,0 @@ -toBeInstanceOf(CountryAuthorized::class) - ->and($authorized->country)->toBe($country); - })->with([ - 'france' => 'france', - 'belgium' => 'belgium', - 'luxembourg' => 'luxembourg', - ]); - - it('rejects a country that is not authorized', function (): void { - // WHEN - $action = fn (): CountryAuthorized => new CountryAuthorized(Country::GERMANY); - - // THEN - expect($action)->toThrow(\DomainException::class, 'Country "GERMANY" is not authorized.'); - }); - - it('lists the authorized countries', function (): void { - // THEN - expect(CountryAuthorized::authorizedCountries())->toBe([ - Country::FRANCE, - Country::BELGIUM, - Country::LUXEMBOURG, - ]); - }); - - it('implements equals correctly', function (): void { - // GIVEN - $france1 = new CountryAuthorized(Country::FRANCE); - $france2 = new CountryAuthorized(Country::FRANCE); - $belgium = new CountryAuthorized(Country::BELGIUM); - - // THEN - expect($france1->equals($france2))->toBeTrue() - ->and($france1->equals($belgium))->toBeFalse() - ->and($france1->equals(null))->toBeFalse(); - }); - - it('converts to string as the country backing value', function (): void { - // GIVEN - $authorized = new CountryAuthorized(Country::FRANCE); - - // THEN - expect((string) $authorized)->toBe('france'); - }); -}); diff --git a/tests/Unit/Domain/Models/ValueObjects/CountryAuthorizedTest.php b/tests/Unit/Domain/Models/ValueObjects/CountryAuthorizedTest.php deleted file mode 100644 index 4019b00..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/CountryAuthorizedTest.php +++ /dev/null @@ -1,65 +0,0 @@ -toBeInstanceOf(CountryAuthorized::class) - ->and($authorized->country)->toBe($country); - })->with([ - 'france' => 'france', - 'belgium' => 'belgium', - 'luxembourg' => 'luxembourg', - ]); - - it('rejects a country that is not authorized', function (): void { - // WHEN - $action = fn (): CountryAuthorized => new CountryAuthorized(Country::GERMANY); - - // THEN - expect($action)->toThrow(\DomainException::class, 'Country "GERMANY" is not authorized.'); - }); - - it('lists the authorized countries', function (): void { - // THEN - expect(CountryAuthorized::authorizedCountries())->toBe([ - Country::FRANCE, - Country::BELGIUM, - Country::LUXEMBOURG, - ]); - }); - - it('implements equals correctly', function (): void { - // GIVEN - $france1 = new CountryAuthorized(Country::FRANCE); - $france2 = new CountryAuthorized(Country::FRANCE); - $belgium = new CountryAuthorized(Country::BELGIUM); - - // THEN - expect($france1->equals($france2))->toBeTrue() - ->and($france1->equals($belgium))->toBeFalse() - ->and($france1->equals(null))->toBeFalse(); - }); - - it('converts to string as the country backing value', function (): void { - // GIVEN - $authorized = new CountryAuthorized(Country::FRANCE); - - // THEN - expect((string) $authorized)->toBe('france'); - }); -}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php index df3d2ba..77fa49e 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; use Shared\Enums\Country; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; covers(CouponLegalInformation::class); @@ -14,7 +13,7 @@ describe('coupon legal information', function (): void { it('creates a valid coupon legal information', function (): void { // GIVEN $legalText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; - $forLocale = new CountryAuthorized(Country::FRANCE); + $forLocale = Country::FRANCE; // WHEN $couponLegalInformation = new CouponLegalInformation($legalText, $forLocale); @@ -27,7 +26,7 @@ describe('coupon legal information', function (): void { it('rejects invalid coupon legal information', function (): void { // GIVEN $legalText = ''; - $forLocale = new CountryAuthorized(Country::FRANCE); + $forLocale = Country::FRANCE; // 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 index 80e567f..293e8b6 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; use Shared\Enums\Country; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponProductInfo; covers(CouponProductInfo::class); @@ -16,7 +15,7 @@ describe('coupon product info', function (): void { $name = 'Lorem ipsum'; $range = 'Lorem'; $validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; - $forLocale = new CountryAuthorized(Country::FRANCE); + $forLocale = Country::FRANCE; // WHEN $couponProductInfo = new CouponProductInfo($name, $range, $validityText, $forLocale); @@ -33,7 +32,7 @@ describe('coupon product info', function (): void { $name = ''; $range = ''; $validityText = ''; - $forLocale = new CountryAuthorized(Country::FRANCE); + $forLocale = Country::FRANCE; // GIVEN / THEN expect(fn (): CouponProductInfo => new CouponProductInfo($name, $range, $validityText, $forLocale))->toThrow(\DomainException::class); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index ac4de15..41beeb3 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -19,7 +19,6 @@ use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Shared\Enums\Currency; use DateTime; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Shared\Enums\Country; use Shared\ValueObjects\Money; @@ -43,7 +42,7 @@ describe('requested coupon', function (): void { $couponPicture = new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')); $gs1 = new GS1('01234567890128', BarcodeFormat::EAN13); $couponValidity = new CouponValidity(30, new DateTime()); - $country = new CountryAuthorized(Country::FRANCE); + $country = Country::FRANCE; // WHEN $coupon = new RequestedCoupon( $name, From aec9e6a575f2b7c7396a0875ee4e2781b3092ddd Mon Sep 17 00:00:00 2001 From: K Muller Date: Tue, 29 Sep 2026 14:16:55 +0200 Subject: [PATCH 41/52] feat: :sparkles: delete country from requestedcoupon --- .../RequestedCoupon/RequestedCoupon.php | 12 +-------- .../Find/FindCampaignRequestTest.php | 2 -- .../List/ListCampaignsRequestTest.php | 3 --- .../Reject/RejectCampaignRequestTest.php | 2 -- .../Models/Entities/CampaignRequestTest.php | 2 -- .../RequestedCoupon/RequestedCouponTest.php | 25 +++++++++++++++---- 6 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php index 78b1bd7..306e68b 100644 --- a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -13,11 +13,9 @@ use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\WarningThresholdQuantity; -use Shared\Enums\Country; final readonly class RequestedCoupon { - /** @var array $country */ public function __construct( public CouponName $name, public CouponProductName $productName, @@ -28,15 +26,7 @@ final readonly class RequestedCoupon public ProcessingCode $processingCode, public CouponPicture $couponPicture, public ?GS1 $gs1, - public CouponValidity $couponValidity, - public array $country, + public CouponValidity $couponValidity ) { - // TODO: règle de validation ici pour Country - // Par défaut dans le tableau country il y aura Country::FRANCE - // Si GS1 est non null alors il ya aura aussi Country::BENELUX - // ça se passera au moment de la création donc peut être plutôt dans l'entité associée ? - - - // JE viens de passer GS1 en nullable, donc il faudra peut être tester } } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 33abf38..11e7944 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -10,7 +10,6 @@ use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestFound; use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestNotFound; use Application\UseCases\CampaignRequest\Find\Output\Output; use Domain\Enums\BarcodeFormat; -use Shared\Enums\Country; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Exceptions\BrandNotFoundException; @@ -59,7 +58,6 @@ describe('find campaign request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), - Country::FRANCE ); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 15aa7d6..8a1915c 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -7,7 +7,6 @@ namespace Application\UseCases\CampaignRequest\List; use Domain\Enums\BarcodeFormat; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; -use Shared\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; @@ -52,7 +51,6 @@ describe('list campaigns request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), - Country::FRANCE ); $this->couponB = new RequestedCoupon( @@ -66,7 +64,6 @@ describe('list campaigns request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), - Country::FRANCE ); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 265eb36..7c73427 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -13,7 +13,6 @@ use Carbon\CarbonImmutable; use Domain\Enums\BarcodeFormat; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; -use Shared\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; @@ -57,7 +56,6 @@ describe('reject campaign request use case', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), - Country::FRANCE ); }); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index d3bc3a6..2217f6e 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -7,7 +7,6 @@ namespace Tests\Unit\Domain\Models\Entities; use Domain\Enums\BarcodeFormat; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; -use Shared\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\CampaignRequest\Applicant; @@ -52,7 +51,6 @@ describe('campaign request', function (): void { new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), new GS1('01234567890128', BarcodeFormat::EAN13), new CouponValidity(30, new DateTime()), - Country::FRANCE ); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index 41beeb3..2913dc3 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -20,7 +20,6 @@ use Domain\Models\ValueObjects\WarningThresholdQuantity; use Shared\Enums\Currency; use DateTime; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; -use Shared\Enums\Country; use Shared\ValueObjects\Money; covers(RequestedCoupon::class); @@ -42,7 +41,6 @@ describe('requested coupon', function (): void { $couponPicture = new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')); $gs1 = new GS1('01234567890128', BarcodeFormat::EAN13); $couponValidity = new CouponValidity(30, new DateTime()); - $country = Country::FRANCE; // WHEN $coupon = new RequestedCoupon( $name, @@ -55,7 +53,6 @@ describe('requested coupon', function (): void { $couponPicture, $gs1, $couponValidity, - $country ); // THEN @@ -68,7 +65,25 @@ describe('requested coupon', function (): void { ->and($coupon->processingCode)->toBe($processingCode) ->and($coupon->couponPicture)->toBe($couponPicture) ->and($coupon->gs1)->toBe($gs1) - ->and($coupon->couponValidity)->toBe($couponValidity) - ->and($coupon->country)->toBe($country); + ->and($coupon->couponValidity)->toBe($couponValidity); + }); + + it('hydrates a requested coupon without a GS1', function (): void { + // WHEN + $coupon = new RequestedCoupon( + new CouponName('Coupon A'), + new CouponProductName('Product A'), + new RequestedCouponQuantity(100), + new WarningThresholdQuantity(10), + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + new MoneyAmount(new Money(Currency::EUR, 300)), + new ProcessingCode('PC1'), + new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + null, + new CouponValidity(30, new DateTime()), + ); + + // THEN + expect($coupon->gs1)->toBeNull(); }); }); From 18551d8bdbe322c402db9bbfa5cf8064f00f18b7 Mon Sep 17 00:00:00 2001 From: K Muller Date: Tue, 29 Sep 2026 14:42:06 +0200 Subject: [PATCH 42/52] fix: :bug: resolve conflicts --- .../CampaignRequest/List/ListCampaignsRequestTest.php | 3 --- .../CampaignRequest/Reject/RejectCampaignRequestTest.php | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 8a1915c..483a049 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -131,7 +131,6 @@ describe('list campaigns request use case', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->couponA, @@ -170,7 +169,6 @@ describe('list campaigns request use case', function (): void { CampaignRequestId::generate(), new BrandCode('AAA01'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->couponA, @@ -180,7 +178,6 @@ describe('list campaigns request use case', function (): void { CampaignRequestId::generate(), new BrandCode('AAA02'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), - new CountryAuthorized(Country::FRANCE), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), $this->couponB, diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 7c73427..9ba1a97 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -77,7 +77,7 @@ describe('reject campaign request use case', function (): void { $repository = \Mockery::mock(CampaignRequestRepository::class); $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); $repository->shouldReceive('save')->once()->with(\Mockery::on( - fn(CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED + fn (CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED && $saved->rejectionReason === $reason && $saved->updatedAt !== null, )); @@ -132,7 +132,7 @@ describe('reject campaign request use case', function (): void { $useCase = new RejectCampaignRequest($repository); // WHEN - $action = fn(): Output => $useCase->execute( + $action = fn (): Output => $useCase->execute( new Input(id: $id, reason: new RejectionReason('Missing legal documents.')) ); @@ -149,7 +149,7 @@ describe('reject campaign request use case', function (): void { $useCase = new RejectCampaignRequest($repository); // WHEN - $action = fn(): Output => $useCase->execute(new Input(id: CampaignRequestId::generate())); + $action = fn (): Output => $useCase->execute(new Input(id: CampaignRequestId::generate())); // THEN expect($action)->toThrow(\ArgumentCountError::class); From 02dfb42056a5b71191c39cd56591891c3ba1d01e Mon Sep 17 00:00:00 2001 From: K Muller Date: Tue, 29 Sep 2026 16:53:38 +0200 Subject: [PATCH 43/52] fix: :bug: rename brand into brandcode --- .../Find/FindCampaignRequest.php | 4 ++-- .../List/CampaignRequestListItem.php | 2 +- .../List/ListCampaignsRequest.php | 2 +- .../Exceptions/BrandNotFoundException.php | 4 ++-- .../Models/Entities/CampaignRequest.php | 8 +++---- .../Find/FindCampaignRequestTest.php | 22 +++++++++---------- .../List/ListCampaignsRequestTest.php | 12 +++++----- .../Models/Entities/CampaignRequestTest.php | 4 ++-- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php index c4737e6..55c207e 100644 --- a/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php +++ b/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php @@ -36,10 +36,10 @@ final readonly class FindCampaignRequest implements UseCase return new CampaignRequestNotFound(); } - $brand = $this->brandRepository->findByCode($campaignRequest->code); + $brand = $this->brandRepository->findByCode($campaignRequest->brandCode); if (! $brand instanceof Brand) { - throw new BrandNotFoundException($campaignRequest->code); + throw new BrandNotFoundException($campaignRequest->brandCode); } return new CampaignRequestFound( diff --git a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php index ebfa33e..1ae91e0 100644 --- a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php +++ b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php @@ -12,7 +12,7 @@ use Domain\Models\ValueObjects\CampaignRequest\Applicant; final readonly class CampaignRequestListItem { public function __construct( - public BrandCode $code, + public BrandCode $brandCode, public Applicant $applicant, public CampaignRequestType $campaignType, public CampaignRequestStatus $status diff --git a/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php b/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php index f61059a..1b1d3de 100644 --- a/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php +++ b/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php @@ -32,7 +32,7 @@ final readonly class ListCampaignsRequest implements UseCase $items = array_map( static fn (CampaignRequest $campaignRequest): CampaignRequestListItem => new CampaignRequestListItem( - code: $campaignRequest->code, + brandCode: $campaignRequest->brandCode, applicant: $campaignRequest->applicant, campaignType: $campaignRequest->campaignType, status: $campaignRequest->status diff --git a/src/Domain/Exceptions/BrandNotFoundException.php b/src/Domain/Exceptions/BrandNotFoundException.php index a8901bb..eee250c 100644 --- a/src/Domain/Exceptions/BrandNotFoundException.php +++ b/src/Domain/Exceptions/BrandNotFoundException.php @@ -12,8 +12,8 @@ use Shared\DDD\Exceptions\NotFoundException; */ final class BrandNotFoundException extends NotFoundException { - public function __construct(BrandCode $code) + public function __construct(BrandCode $brandCode) { - parent::__construct(sprintf('Brand with code "%s" was not found.', $code->value)); + parent::__construct(sprintf('Brand with code "%s" was not found.', $brandCode->value)); } } diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index 3a1674c..92be622 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -23,7 +23,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent { private function __construct( public CampaignRequestId $id, - public BrandCode $code, + public BrandCode $brandCode, public Applicant $applicant, public CampaignRequestType $campaignType, public ?PrintRules $printRules, @@ -59,7 +59,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent public static function hydrate( CampaignRequestId $id, - BrandCode $code, + BrandCode $brandCode, Applicant $applicant, CampaignRequestType $campaignType, ?PrintRules $printRules, @@ -70,7 +70,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent ): self { return new self( id: $id, - code: $code, + brandCode: $brandCode, applicant: $applicant, campaignType: $campaignType, printRules: $printRules, @@ -89,7 +89,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent return new self( id: $this->id, - code: $this->code, + brandCode: $this->brandCode, applicant: $this->applicant, campaignType: $this->campaignType, printRules: $this->printRules, diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 11e7944..200ab32 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -63,11 +63,11 @@ describe('find campaign request use case', function (): void { it('finds a campaign request by its id, along with its brand', function (): void { // GIVEN - $id = CampaignRequestId::generate(); - $code = new BrandCode('AAA01'); - $campaignRequest = CampaignRequest::hydrate( + $id = CampaignRequestId::generate(); + $brandCode = new BrandCode('AAA01'); + $campaignRequest = CampaignRequest::hydrate( $id, - $code, + $brandCode, new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), @@ -76,7 +76,7 @@ describe('find campaign request use case', function (): void { ); $brand = Brand::hydrate( BrandId::generate(), - $code, + $brandCode, new BrandName('Brand A'), new PicturePath('brands/logo.jpg'), new Rcs('RCS PARIS 111 222 333'), @@ -87,7 +87,7 @@ describe('find campaign request use case', function (): void { $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); $brandRepository = \Mockery::mock(BrandRepository::class); - $brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn($brand); + $brandRepository->shouldReceive('findByCode')->once()->with($brandCode)->andReturn($brand); $useCase = new FindCampaignRequest($repository, $brandRepository); @@ -168,11 +168,11 @@ describe('find campaign request use case', function (): void { it('throws when the campaign request references a brand that no longer exists', function (): void { // GIVEN - $id = CampaignRequestId::generate(); - $code = new BrandCode('AAA01'); - $campaignRequest = CampaignRequest::hydrate( + $id = CampaignRequestId::generate(); + $brandCode = new BrandCode('AAA01'); + $campaignRequest = CampaignRequest::hydrate( $id, - $code, + $brandCode, new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, new PrintRules(maxPerCustomer: 3), @@ -184,7 +184,7 @@ describe('find campaign request use case', function (): void { $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); $brandRepository = \Mockery::mock(BrandRepository::class); - $brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(null); + $brandRepository->shouldReceive('findByCode')->once()->with($brandCode)->andReturn(null); $useCase = new FindCampaignRequest($repository, $brandRepository); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 483a049..cc93d70 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -96,7 +96,7 @@ describe('list campaigns request use case', function (): void { new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending) ] ); - $filters = new CampaignRequestFilters($campaignRequest2->code, CampaignRequestStatus::CREATED); + $filters = new CampaignRequestFilters($campaignRequest2->brandCode, CampaignRequestStatus::CREATED); $input = new Input(pagination: $pagination, sort: $sort, filters: $filters); $repository = \Mockery::mock(CampaignRequestRepository::class); @@ -116,7 +116,7 @@ describe('list campaigns request use case', function (): void { $items = $result->campaignsRequest->getItems(); expect($items)->toHaveCount(2) ->and($items[0])->toBeInstanceOf(CampaignRequestListItem::class) - ->and($items[0]->code)->toBe($campaignRequest1->code) + ->and($items[0]->brandCode)->toBe($campaignRequest1->brandCode) ->and($items[0]->applicant)->toBe($campaignRequest1->applicant) ->and($items[0]->campaignType)->toBe($campaignRequest1->campaignType) ->and($items[0]->status)->toBe($campaignRequest1->status) @@ -138,7 +138,7 @@ describe('list campaigns request use case', function (): void { ); $paginated = new Paginated($pagination, [$campaignRequest], 1); - $filters = new CampaignRequestFilters($campaignRequest->code, CampaignRequestStatus::CREATED); + $filters = new CampaignRequestFilters($campaignRequest->brandCode, CampaignRequestStatus::CREATED); $input = new Input(pagination: $pagination, filters: $filters); $repository = \Mockery::mock(CampaignRequestRepository::class); @@ -156,7 +156,7 @@ describe('list campaigns request use case', function (): void { $items = $result->campaignsRequest->getItems(); expect($items)->toHaveCount(1) ->and($items[0])->toBeInstanceOf(CampaignRequestListItem::class) - ->and($items[0]->code)->toBe($campaignRequest->code) + ->and($items[0]->brandCode)->toBe($campaignRequest->brandCode) ->and($items[0]->status)->toBe($campaignRequest->status) ->and($result->campaignsRequest->getTotal())->toBe(1) ->and($result->campaignsRequest->getPagination())->toBe($pagination); @@ -206,8 +206,8 @@ describe('list campaigns request use case', function (): void { // THEN $items = $result->campaignsRequest->getItems(); expect($items)->toHaveCount(2) - ->and($items[0]->code)->toBe($campaignRequest2->code) - ->and($items[1]->code)->toBe($campaignRequest1->code) + ->and($items[0]->brandCode)->toBe($campaignRequest2->brandCode) + ->and($items[1]->brandCode)->toBe($campaignRequest1->brandCode) ->and($result->campaignsRequest->getTotal())->toBe(2) ->and($result->campaignsRequest->getPagination())->toBe($pagination); }); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 2217f6e..d9dc6fb 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -77,7 +77,7 @@ describe('campaign request', function (): void { // THEN expect($campaignRequest->id())->toBe($id) - ->and($campaignRequest->code)->toBe($code) + ->and($campaignRequest->brandCode)->toBe($code) ->and($campaignRequest->applicant)->toBe($applicant) ->and($campaignRequest->campaignType)->toBe($campaignType) ->and($campaignRequest->printRules)->toBe($printRules) @@ -206,7 +206,7 @@ describe('campaign request', function (): void { expect($rejected->status)->toBe(CampaignRequestStatus::REJECTED) ->and($rejected->rejectionReason)->toBe($reason) ->and($rejected->id())->toBe($campaignRequest->id()) - ->and($rejected->code)->toBe($campaignRequest->code) + ->and($rejected->brandCode)->toBe($campaignRequest->brandCode) ->and($rejected->applicant)->toBe($campaignRequest->applicant) ->and($rejected->campaignType)->toBe($campaignRequest->campaignType) ->and($rejected->printRules)->toBe($campaignRequest->printRules) From f45bb119e017d1053d22001402fc62c6cb9114b7 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 29 Sep 2026 17:12:16 +0200 Subject: [PATCH 44/52] refactor: :recycle: update entities, related VOs and tests --- .../Find/FindCampaignRequest.php | 2 +- .../Find/Output/CampaignRequestFound.php | 8 +- .../List/CampaignRequestListItem.php | 1 + .../List/ListCampaignsRequest.php | 1 + src/Domain/Models/Entities/Campaign.php | 30 +- .../Models/Entities/CampaignRequest.php | 10 +- src/Domain/Models/Entities/Coupon.php | 89 +-- .../ValueObjects/Campaign/CampaignCoupon.php | 10 +- .../Models/ValueObjects/Coupon/CouponCode.php | 26 + .../Coupon/CouponLegalInformation.php | 9 +- .../ValueObjects/Coupon/CouponProduct.php | 22 - .../ValueObjects/Coupon/CouponProductInfo.php | 46 -- .../ValueObjects/Coupon/CouponQuantity.php | 16 + .../Coupon/CouponValidityInformation.php | 19 + .../Models/ValueObjects/CouponPicture.php | 15 +- .../Models/ValueObjects/CouponProductName.php | 16 - .../RequestedCoupon/RequestedCoupon.php | 25 +- .../Find/FindCampaignRequestTest.php | 35 +- .../List/ListCampaignsRequestTest.php | 48 +- .../Reject/RejectCampaignRequestTest.php | 39 +- .../Models/Entities/CampaignRequestTest.php | 76 ++- .../Domain/Models/Entities/CampaignTest.php | 84 +-- .../Domain/Models/Entities/CouponTest.php | 568 ++++++++++-------- .../Campaign/CampaignCouponTest.php | 82 ++- .../ValueObjects/Coupon/CouponCodeTest.php | 75 +++ .../Coupon/CouponLegalInformationTest.php | 2 +- .../Coupon/CouponProductInfoTest.php | 40 -- .../ValueObjects/Coupon/CouponProductTest.php | 36 -- .../Coupon/CouponQuantityTest.php | 39 ++ .../Coupon/CouponValidityInformationTest.php | 35 ++ .../ValueObjects/CouponProductNameTest.php | 30 - .../RequestedCoupon/RequestedCouponTest.php | 242 ++++++-- 32 files changed, 1054 insertions(+), 722 deletions(-) create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponCode.php delete mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponProduct.php delete mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponValidityInformation.php delete mode 100644 src/Domain/Models/ValueObjects/CouponProductName.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCodeTest.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/CouponProductNameTest.php diff --git a/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php index 55c207e..0acbbef 100644 --- a/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php +++ b/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php @@ -44,9 +44,9 @@ final readonly class FindCampaignRequest implements UseCase return new CampaignRequestFound( brand: $brand, + name: $campaignRequest->name, applicant: $campaignRequest->applicant, campaignType: $campaignRequest->campaignType, - printRules: $campaignRequest->printRules, requestedCoupon: $campaignRequest->requestedCoupon, status: $campaignRequest->status, rejectionReason: $campaignRequest->rejectionReason, diff --git a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php index 05af5aa..5100a7b 100644 --- a/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php +++ b/src/Application/UseCases/CampaignRequest/Find/Output/CampaignRequestFound.php @@ -4,21 +4,21 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\Find\Output; +use Domain\Models\Entities\Brand; +use Domain\Models\ValueObjects\Campaign\CampaignName; +use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; -use Domain\Models\Entities\Brand; -use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; -use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; final readonly class CampaignRequestFound implements Output { public function __construct( public Brand $brand, + public CampaignName $name, public Applicant $applicant, public CampaignRequestType $campaignType, - public ?PrintRules $printRules, public RequestedCoupon $requestedCoupon, public CampaignRequestStatus $status, public ?RejectionReason $rejectionReason, diff --git a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php index 1ae91e0..852118e 100644 --- a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php +++ b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php @@ -7,6 +7,7 @@ namespace Application\UseCases\CampaignRequest\List; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\Brand\BrandCode; +use Domain\Models\ValueObjects\Campaign\CampaignName; use Domain\Models\ValueObjects\CampaignRequest\Applicant; final readonly class CampaignRequestListItem diff --git a/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php b/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php index 1b1d3de..5c537bf 100644 --- a/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php +++ b/src/Application/UseCases/CampaignRequest/List/ListCampaignsRequest.php @@ -33,6 +33,7 @@ final readonly class ListCampaignsRequest implements UseCase $items = array_map( static fn (CampaignRequest $campaignRequest): CampaignRequestListItem => new CampaignRequestListItem( brandCode: $campaignRequest->brandCode, + name: $campaignRequest->name, applicant: $campaignRequest->applicant, campaignType: $campaignRequest->campaignType, status: $campaignRequest->status diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php index 197a7dd..9df15bc 100644 --- a/src/Domain/Models/Entities/Campaign.php +++ b/src/Domain/Models/Entities/Campaign.php @@ -11,24 +11,23 @@ use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; -use Shared\Enums\Country; /** * @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Campaign\CampaignId> */ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier { + /** + * @param array $coupons + */ 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 array $coupons */ public array $coupons, - public BrandCode $brand, + public BrandCode $brandCode, public ?\DateTimeInterface $updatedAt = null, ) { foreach ($this->coupons as $coupon) { @@ -54,59 +53,50 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier } /** - * @param array $coupons + * @param array $coupons */ public static function create( CampaignCode $code, - Tag $tag, CampaignName $name, bool $publication, bool $printAllAtOnce, - Country $availableIn, - /** @var array $coupons */ array $coupons, - BrandCode $brand, + BrandCode $brandCode, ?\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, + brandCode: $brandCode, updatedAt: $updatedAt, ); } /** - * @param array $coupons + * @param array $coupons */ public static function hydrate( CampaignId $id, CampaignCode $code, - Tag $tag, CampaignName $name, bool $publication, bool $printAllAtOnce, - Country $availableIn, array $coupons, - BrandCode $brand, + BrandCode $brandCode, ?\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, + brandCode: $brandCode, updatedAt: $updatedAt, ); } diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index 92be622..833b3cf 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -7,10 +7,10 @@ namespace Domain\Models\Entities; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\Brand\BrandCode; +use Domain\Models\ValueObjects\Campaign\CampaignName; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; -use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; @@ -24,9 +24,9 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent private function __construct( public CampaignRequestId $id, public BrandCode $brandCode, + public CampaignName $name, public Applicant $applicant, public CampaignRequestType $campaignType, - public ?PrintRules $printRules, public RequestedCoupon $requestedCoupon, public CampaignRequestStatus $status, public ?RejectionReason $rejectionReason = null, @@ -60,9 +60,9 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent public static function hydrate( CampaignRequestId $id, BrandCode $brandCode, + CampaignName $name, Applicant $applicant, CampaignRequestType $campaignType, - ?PrintRules $printRules, RequestedCoupon $requestedCoupon, CampaignRequestStatus $status, ?RejectionReason $rejectionReason = null, @@ -71,9 +71,9 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent return new self( id: $id, brandCode: $brandCode, + name: $name, applicant: $applicant, campaignType: $campaignType, - printRules: $printRules, requestedCoupon: $requestedCoupon, status: $status, rejectionReason: $rejectionReason, @@ -90,9 +90,9 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent return new self( id: $this->id, brandCode: $this->brandCode, + name: $this->name, applicant: $this->applicant, campaignType: $this->campaignType, - printRules: $this->printRules, requestedCoupon: $this->requestedCoupon, status: CampaignRequestStatus::REJECTED, rejectionReason: $reason, diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index 33052b9..c3075f7 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -7,15 +7,16 @@ namespace Domain\Models\Entities; use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar; +use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; use Domain\Models\ValueObjects\Coupon\CouponId; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; -use Domain\Models\ValueObjects\Coupon\CouponProduct; use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; -use Domain\Models\ValueObjects\CouponProductName; use Domain\Models\ValueObjects\GS1; +use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\ProcessingCode; use Shared\DDD\Models\AggregateRoot; @@ -26,27 +27,37 @@ use Shared\DDD\Models\HasTimestampIdentifier; */ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier { + /** + * @param array $couponValidityInformation + * @param array $couponLegalInformation + * @param array $products + */ private function __construct( public CouponId $id, + public CouponCode $code, public CouponName $name, - public bool $status, - public CouponProductName $productName, public MoneyAmount $amount, public CouponPicture $picture, - public CouponProduct $product, public CouponDiffusion $diffusion, - public CouponAvailabilityCalendar $availability, public CouponValidity $validity, - /** @var array $legalInformation */ - public array $legalInformation, - public ProcessingCode $code, + public array $couponValidityInformation, + public array $couponLegalInformation, + public ProcessingCode $processingCode, public BrandCode $brandCode, public ActorId $createdBy, + public array $products = [], + public ?CouponAvailabilityCalendar $availabilityCalendar = null, public ?GS1 $gtin = null, public ?\DateTimeInterface $updatedAt = null, ) { - foreach ($this->legalInformation as $legal) { - if (!$legal instanceof CouponLegalInformation) { + foreach ($this->couponValidityInformation as $validityInformation) { + if (!$validityInformation instanceof CouponValidityInformation) { + throw new \DomainException('Coupon validity information must be CouponValidityInformation instances.'); + } + } + + foreach ($this->couponLegalInformation as $legalInformation) { + if (!$legalInformation instanceof CouponLegalInformation) { throw new \DomainException('Coupon legal information must be CouponLegalInformation instances.'); } } @@ -68,81 +79,85 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier } /** - * @param array $legalInformation + * @param array $couponValidityInformation + * @param array $couponLegalInformation + * @param array $products */ public static function create( + CouponCode $code, CouponName $name, - bool $status, - CouponProductName $productName, MoneyAmount $amount, CouponPicture $picture, - CouponProduct $product, CouponDiffusion $diffusion, - CouponAvailabilityCalendar $availability, CouponValidity $validity, - array $legalInformation, - ProcessingCode $code, + array $couponValidityInformation, + array $couponLegalInformation, + ProcessingCode $processingCode, BrandCode $brandCode, ActorId $createdBy, + array $products = [], + ?CouponAvailabilityCalendar $availabilityCalendar = null, ?GS1 $gtin = null, ?\DateTimeInterface $updatedAt = null, ): Coupon { return new self( id: CouponId::generate(), + code: $code, name: $name, - status: $status, - productName: $productName, amount: $amount, picture: $picture, - product: $product, diffusion: $diffusion, - availability: $availability, validity: $validity, - legalInformation: $legalInformation, - code: $code, + couponValidityInformation: $couponValidityInformation, + couponLegalInformation: $couponLegalInformation, + processingCode: $processingCode, brandCode: $brandCode, createdBy: $createdBy, + products: $products, + availabilityCalendar: $availabilityCalendar, gtin: $gtin, updatedAt: $updatedAt, ); } /** - * @param array $legalInformation + * @param array $couponValidityInformation + * @param array $couponLegalInformation + * @param array $products */ public static function hydrate( CouponId $id, + CouponCode $code, CouponName $name, - bool $status, - CouponProductName $productName, MoneyAmount $amount, CouponPicture $picture, - CouponProduct $product, CouponDiffusion $diffusion, - CouponAvailabilityCalendar $availability, CouponValidity $validity, - array $legalInformation, - ProcessingCode $code, + array $couponValidityInformation, + array $couponLegalInformation, + ProcessingCode $processingCode, BrandCode $brandCode, ActorId $createdBy, + array $products = [], + ?CouponAvailabilityCalendar $availabilityCalendar = null, ?GS1 $gtin = null, ?\DateTimeInterface $updatedAt = null, ): self { return new self( id: $id, + code: $code, name: $name, - status: $status, - productName: $productName, amount: $amount, picture: $picture, - product: $product, diffusion: $diffusion, - availability: $availability, validity: $validity, - legalInformation: $legalInformation, - code: $code, + couponValidityInformation: $couponValidityInformation, + couponLegalInformation: $couponLegalInformation, + processingCode: $processingCode, brandCode: $brandCode, createdBy: $createdBy, + products: $products, + availabilityCalendar: $availabilityCalendar, gtin: $gtin, updatedAt: $updatedAt, ); diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php index a3ed524..713d13b 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php @@ -4,17 +4,19 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Campaign; +use Domain\Models\ValueObjects\CountryAuthorized; +use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Domain\Models\ValueObjects\Stock\StockId; use Domain\Models\ValueObjects\WarningThresholdQuantity; final readonly class CampaignCoupon { public function __construct( - public ProcessingCode $code, - public StockId $qty, - public WarningThresholdQuantity $warningThresholdQty, + public ProcessingCode $processingCode, + public CouponQuantity $quantity, + public CountryAuthorized $availableIn, + public ?WarningThresholdQuantity $warningThresholdQuantity = null, public ?PrintRules $printRules = null, ) { } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponCode.php b/src/Domain/Models/ValueObjects/Coupon/CouponCode.php new file mode 100644 index 0000000..09a09c4 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponCode.php @@ -0,0 +1,26 @@ +legalText === $other->legalText && $this->forLocale === $other->forLocale; - } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php b/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php deleted file mode 100644 index f297569..0000000 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProduct.php +++ /dev/null @@ -1,22 +0,0 @@ -name) === '') { - 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 deleted file mode 100644 index 2b00b78..0000000 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProductInfo.php +++ /dev/null @@ -1,46 +0,0 @@ -name); - - if ($trimmedName === '') { - throw new \DomainException('Product Info name cannot be empty.'); - } - - $trimmedRange = mb_trim($this->range); - - if ($trimmedRange === '') { - throw new \DomainException('Product Info range cannot be empty.'); - } - - $trimmedValidityText = mb_trim($this->validityText); - - if ($trimmedValidityText === '') { - throw new \DomainException('Product Info validity text cannot be empty.'); - } - } - - #[\Override] - public function equals(mixed $other): bool - { - return $other instanceof self - && $this->name === $other->name - && $this->range === $other->range - && $this->validityText === $other->validityText - && $this->forLocale === $other->forLocale; - } -} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php b/src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php new file mode 100644 index 0000000..d2f5bb0 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php @@ -0,0 +1,16 @@ +quantity <= 0) { + throw new \DomainException('CouponQuantity quantity must be positive.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformation.php b/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformation.php new file mode 100644 index 0000000..517b714 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformation.php @@ -0,0 +1,19 @@ +validityText) === '') { + throw new \DomainException('Validity text cannot be empty.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/CouponPicture.php b/src/Domain/Models/ValueObjects/CouponPicture.php index ec29916..a85f12f 100644 --- a/src/Domain/Models/ValueObjects/CouponPicture.php +++ b/src/Domain/Models/ValueObjects/CouponPicture.php @@ -4,19 +4,12 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects; -use Shared\DDD\Models\Equatable; - -final readonly class CouponPicture implements Equatable +final readonly class CouponPicture { public function __construct( - public PicturePath $product, - public PicturePath $thumbnail + public PicturePath $decoration, + public PicturePath $thumbnail, + public PicturePath $inactiveThumbnail ) { } - - #[\Override] - public function equals(mixed $other): bool - { - return $other instanceof self && $this->product->equals($other->product) && $this->thumbnail->equals($other->thumbnail); - } } diff --git a/src/Domain/Models/ValueObjects/CouponProductName.php b/src/Domain/Models/ValueObjects/CouponProductName.php deleted file mode 100644 index 826389d..0000000 --- a/src/Domain/Models/ValueObjects/CouponProductName.php +++ /dev/null @@ -1,16 +0,0 @@ -value) === '') { - throw new \DomainException('Coupon product name cannot be empty.'); - } - } -} diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php index 306e68b..683cd5c 100644 --- a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -5,28 +5,41 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\RequestedCoupon; use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; -use Domain\Models\ValueObjects\CouponProductName; use Domain\Models\ValueObjects\GS1; +use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; +use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\WarningThresholdQuantity; final readonly class RequestedCoupon { + /** + * @param array $couponValidityInformation + * @param array $products + */ public function __construct( public CouponName $name, - public CouponProductName $productName, public RequestedCouponQuantity $quantity, - public WarningThresholdQuantity $warningThresholdQuantity, - public Period $period, + public Period $diffusion, public MoneyAmount $moneyAmount, public ProcessingCode $processingCode, public CouponPicture $couponPicture, - public ?GS1 $gs1, - public CouponValidity $couponValidity + public CouponValidity $couponValidity, + public array $couponValidityInformation, + public array $products = [], + public ?PrintRules $printRules = null, + public ?GS1 $gs1 = null, + public ?WarningThresholdQuantity $warningThresholdQuantity = null, ) { + foreach ($this->couponValidityInformation as $validityInformation) { + if (!$validityInformation instanceof CouponValidityInformation) { + throw new \DomainException('Coupon validity information must be CouponValidityInformation instances.'); + } + } } } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 200ab32..9103821 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -9,36 +9,39 @@ use Application\UseCases\CampaignRequest\Find\Input; use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestFound; use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestNotFound; use Application\UseCases\CampaignRequest\Find\Output\Output; +use DateTime; use Domain\Enums\BarcodeFormat; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; +use Domain\Enums\Country; use Domain\Exceptions\BrandNotFoundException; use Domain\Models\Entities\Brand; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Brand\BrandId; use Domain\Models\ValueObjects\Brand\BrandName; +use Domain\Models\ValueObjects\Campaign\CampaignName; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; -use Domain\Models\ValueObjects\CouponProductName; use Domain\Models\ValueObjects\GS1; +use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Currency; -use DateTime; -use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; use Shared\ValueObjects\Rcs; @@ -49,15 +52,21 @@ describe('find campaign request use case', function (): void { beforeEach(function (): void { $this->requestedCoupon = new RequestedCoupon( new CouponName('Coupon A'), - new CouponProductName('Product A'), new RequestedCouponQuantity(100), - new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new CouponPicture( + new PicturePath('coupons/product.jpg'), + new PicturePath('coupons/thumbnail.jpg'), + new PicturePath('coupons/inactive_thumbnail.jpg') + ), + new CouponValidity(30, new DateTime('2026-02-01')), + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new IncoCode(35784), new IncoCode(15522)], + new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, new DateTime()), + new WarningThresholdQuantity(10) ); }); @@ -68,9 +77,9 @@ describe('find campaign request use case', function (): void { $campaignRequest = CampaignRequest::hydrate( $id, $brandCode, + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::CREATED, ); @@ -97,9 +106,9 @@ describe('find campaign request use case', function (): void { // THEN expect($result)->toBeInstanceOf(CampaignRequestFound::class) ->and($result->brand)->toBe($brand) + ->and($result->name)->toBe($campaignRequest->name) ->and($result->applicant)->toBe($campaignRequest->applicant) ->and($result->campaignType)->toBe($campaignRequest->campaignType) - ->and($result->printRules)->toBe($campaignRequest->printRules) ->and($result->requestedCoupon)->toBe($campaignRequest->requestedCoupon) ->and($result->status)->toBe($campaignRequest->status) ->and($result->rejectionReason)->toBeNull(); @@ -113,9 +122,9 @@ describe('find campaign request use case', function (): void { $campaignRequest = CampaignRequest::hydrate( $id, $code, + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::REJECTED, $rejectionReason, @@ -173,9 +182,9 @@ describe('find campaign request use case', function (): void { $campaignRequest = CampaignRequest::hydrate( $id, $brandCode, + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::CREATED, ); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index cc93d70..11da86f 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -4,11 +4,13 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\List; +use DateTime; use Domain\Enums\BarcodeFormat; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; +use Domain\Models\ValueObjects\Campaign\CampaignName; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestFilters; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; @@ -16,21 +18,21 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortClause; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField; use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; -use Domain\Models\ValueObjects\CouponProductName; use Domain\Models\ValueObjects\GS1; +use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\Enums\Currency; -use DateTime; -use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; use Shared\ValueObjects\Pagination\Paginated; @@ -42,28 +44,40 @@ describe('list campaigns request use case', function (): void { beforeEach(function (): void { $this->couponA = new RequestedCoupon( new CouponName('Coupon A'), - new CouponProductName('Product A'), new RequestedCouponQuantity(100), - new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new CouponPicture( + new PicturePath('coupons/product.jpg'), + new PicturePath('coupons/thumbnail.jpg'), + new PicturePath('coupons/inactive_thumbnail.jpg') + ), + new CouponValidity(30, new DateTime('2026-02-01')), + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new IncoCode(35784), new IncoCode(15522)], + new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, new DateTime()), + new WarningThresholdQuantity(10) ); $this->couponB = new RequestedCoupon( new CouponName('Coupon B'), - new CouponProductName('Product B'), new RequestedCouponQuantity(100), - new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC2'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new CouponPicture( + new PicturePath('coupons/product.jpg'), + new PicturePath('coupons/thumbnail.jpg'), + new PicturePath('coupons/inactive_thumbnail.jpg') + ), + new CouponValidity(30, new DateTime('2026-02-01')), + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new IncoCode(24514), new IncoCode(46259)], + new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, new DateTime()), + new WarningThresholdQuantity(10) ); }); @@ -73,18 +87,18 @@ describe('list campaigns request use case', function (): void { $campaignRequest1 = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->couponA, CampaignRequestStatus::CREATED, ); $campaignRequest2 = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA02'), + new CampaignName('Campaign B'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->couponB, CampaignRequestStatus::CREATED, ); @@ -117,6 +131,7 @@ describe('list campaigns request use case', function (): void { expect($items)->toHaveCount(2) ->and($items[0])->toBeInstanceOf(CampaignRequestListItem::class) ->and($items[0]->brandCode)->toBe($campaignRequest1->brandCode) + ->and($items[0]->name)->toBe($campaignRequest1->name) ->and($items[0]->applicant)->toBe($campaignRequest1->applicant) ->and($items[0]->campaignType)->toBe($campaignRequest1->campaignType) ->and($items[0]->status)->toBe($campaignRequest1->status) @@ -130,9 +145,9 @@ describe('list campaigns request use case', function (): void { $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->couponA, CampaignRequestStatus::CREATED, ); @@ -157,6 +172,7 @@ describe('list campaigns request use case', function (): void { expect($items)->toHaveCount(1) ->and($items[0])->toBeInstanceOf(CampaignRequestListItem::class) ->and($items[0]->brandCode)->toBe($campaignRequest->brandCode) + ->and($items[0]->name)->toBe($campaignRequest->name) ->and($items[0]->status)->toBe($campaignRequest->status) ->and($result->campaignsRequest->getTotal())->toBe(1) ->and($result->campaignsRequest->getPagination())->toBe($pagination); @@ -168,18 +184,18 @@ describe('list campaigns request use case', function (): void { $campaignRequest1 = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->couponA, CampaignRequestStatus::CREATED, ); $campaignRequest2 = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA02'), + new CampaignName('Campaign B'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->couponB, CampaignRequestStatus::CREATED, ); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 9ba1a97..0d95d6b 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -10,55 +10,64 @@ use Application\UseCases\CampaignRequest\Reject\Output\CampaignRequestRejected; use Application\UseCases\CampaignRequest\Reject\Output\Output; use Application\UseCases\CampaignRequest\Reject\RejectCampaignRequest; use Carbon\CarbonImmutable; +use DateTime; use Domain\Enums\BarcodeFormat; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; +use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; +use Domain\Models\ValueObjects\Campaign\CampaignName; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; -use Domain\Models\ValueObjects\CouponProductName; use Domain\Models\ValueObjects\GS1; +use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\Enums\Currency; -use DateTime; -use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; covers(RejectCampaignRequest::class, Input::class, CampaignRequestRejected::class, CampaignRequestNotFound::class); describe('reject campaign request use case', function (): void { - afterEach(function (): void { - CarbonImmutable::setTestNow(); - }); - beforeEach(function (): void { $this->requestedCoupon = new RequestedCoupon( new CouponName('Coupon A'), - new CouponProductName('Product A'), new RequestedCouponQuantity(100), - new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new CouponPicture( + new PicturePath('coupons/product.jpg'), + new PicturePath('coupons/thumbnail.jpg'), + new PicturePath('coupons/inactive_thumbnail.jpg') + ), + new CouponValidity(30, new DateTime('2026-02-01')), + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new IncoCode(35784), new IncoCode(15522)], + new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, new DateTime()), + new WarningThresholdQuantity(10) ); }); + afterEach(function (): void { + CarbonImmutable::setTestNow(); + }); + it('rejects a campaign request and persists it', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15 12:00:00'); @@ -66,9 +75,9 @@ describe('reject campaign request use case', function (): void { $campaignRequest = CampaignRequest::hydrate( $id, new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::CREATED, ); @@ -118,9 +127,9 @@ describe('reject campaign request use case', function (): void { $campaignRequest = CampaignRequest::hydrate( $id, new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::TRANSFORMED, ); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index d9dc6fb..a978935 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -4,33 +4,37 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; +use Carbon\CarbonImmutable; +use DateTime; use Domain\Enums\BarcodeFormat; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; +use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; +use Domain\Models\ValueObjects\Campaign\CampaignName; use Domain\Models\ValueObjects\CampaignRequest\Applicant; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; -use Domain\Models\ValueObjects\CouponProductName; use Domain\Models\ValueObjects\GS1; +use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Ramsey\Uuid\Uuid; -use Shared\Enums\Currency; -use DateTime; -use Shared\ValueObjects\Email; -use Shared\ValueObjects\Money; -use Carbon\CarbonImmutable; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; +use Ramsey\Uuid\Uuid; +use Shared\Enums\Currency; +use Shared\ValueObjects\Email; +use Shared\ValueObjects\Money; covers(CampaignRequest::class); @@ -42,15 +46,21 @@ describe('campaign request', function (): void { beforeEach(function (): void { $this->requestedCoupon = new RequestedCoupon( new CouponName('Coupon A'), - new CouponProductName('Product A'), new RequestedCouponQuantity(100), - new WarningThresholdQuantity(10), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), + new CouponPicture( + new PicturePath('coupons/product.jpg'), + new PicturePath('coupons/thumbnail.jpg'), + new PicturePath('coupons/inactive_thumbnail.jpg') + ), + new CouponValidity(30, new DateTime('2026-02-01')), + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new IncoCode(35784), new IncoCode(15522)], + new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), - new CouponValidity(30, new DateTime()), + new WarningThresholdQuantity(10) ); }); @@ -58,9 +68,9 @@ describe('campaign request', function (): void { // GIVEN $id = CampaignRequestId::generate(); $code = new BrandCode('AAA01'); + $name = new CampaignName('Campaign A'); $applicant = new Applicant('John', 'Doe', new Email('john.doe@example.com')); $campaignType = CampaignRequestType::CLUB; - $printRules = new PrintRules(maxPerCustomer: 3); $requestedCoupon = $this->requestedCoupon; $status = CampaignRequestStatus::CREATED; @@ -68,9 +78,9 @@ describe('campaign request', function (): void { $campaignRequest = CampaignRequest::hydrate( $id, $code, + $name, $applicant, $campaignType, - $printRules, $requestedCoupon, $status, ); @@ -78,31 +88,15 @@ describe('campaign request', function (): void { // THEN expect($campaignRequest->id())->toBe($id) ->and($campaignRequest->brandCode)->toBe($code) + ->and($campaignRequest->name)->toBe($name) ->and($campaignRequest->applicant)->toBe($applicant) ->and($campaignRequest->campaignType)->toBe($campaignType) - ->and($campaignRequest->printRules)->toBe($printRules) ->and($campaignRequest->requestedCoupon)->toBe($requestedCoupon) ->and($campaignRequest->status)->toBe($status) ->and($campaignRequest->updatedAt)->toBeNull() ->and($campaignRequest->updatedAt())->toBeNull(); }); - it('hydrates a campaign request with a null printRules', function (): void { - // WHEN - $campaignRequest = CampaignRequest::hydrate( - CampaignRequestId::generate(), - new BrandCode('AAA01'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, - null, - $this->requestedCoupon, - CampaignRequestStatus::CREATED, - ); - - // THEN - expect($campaignRequest->printRules)->toBeNull(); - }); - it('hydrates a campaign request with an explicit updatedAt date', function (): void { // GIVEN $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); @@ -111,9 +105,9 @@ describe('campaign request', function (): void { $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::CREATED, null, @@ -133,9 +127,9 @@ describe('campaign request', function (): void { $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::REJECTED, $rejectionReason, @@ -151,9 +145,9 @@ describe('campaign request', function (): void { $action = fn (): CampaignRequest => CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::REJECTED, ); @@ -170,9 +164,9 @@ describe('campaign request', function (): void { $action = fn (): CampaignRequest => CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::CREATED, new RejectionReason('Missing legal documents.'), @@ -191,9 +185,9 @@ describe('campaign request', function (): void { $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::CREATED, ); @@ -207,9 +201,9 @@ describe('campaign request', function (): void { ->and($rejected->rejectionReason)->toBe($reason) ->and($rejected->id())->toBe($campaignRequest->id()) ->and($rejected->brandCode)->toBe($campaignRequest->brandCode) + ->and($rejected->name)->toBe($campaignRequest->name) ->and($rejected->applicant)->toBe($campaignRequest->applicant) ->and($rejected->campaignType)->toBe($campaignRequest->campaignType) - ->and($rejected->printRules)->toBe($campaignRequest->printRules) ->and($rejected->requestedCoupon)->toBe($campaignRequest->requestedCoupon) ->and($rejected->updatedAt)->not->toBeNull() ->and($rejected->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00'); @@ -220,9 +214,9 @@ describe('campaign request', function (): void { $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::TRANSFORMED, ); @@ -239,9 +233,9 @@ describe('campaign request', function (): void { $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::REJECTED, new RejectionReason('First reason.'), @@ -261,9 +255,9 @@ describe('campaign request', function (): void { $campaignRequest = CampaignRequest::hydrate( $id, new BrandCode('AAA01'), + new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - new PrintRules(maxPerCustomer: 3), $this->requestedCoupon, CampaignRequestStatus::CREATED, ); diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index 25e96ba..d81b195 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -7,30 +7,21 @@ namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; use Shared\Enums\Country; use Domain\Models\Entities\Campaign; -use Domain\Models\Entities\Tag; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; +use Domain\Models\ValueObjects\CountryAuthorized; +use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Domain\Models\ValueObjects\Stock\StockId; -use Domain\Models\ValueObjects\Tag\TagId; -use Domain\Models\ValueObjects\Tag\TagName; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Ramsey\Uuid\Uuid; covers(Campaign::class); describe('campaign', function (): void { - beforeEach(function (): void { - $this->tag = Tag::hydrate( - TagId::generate(), - new TagName('Tag A'), - ); - }); - afterEach(function (): void { CarbonImmutable::setTestNow(); }); @@ -42,40 +33,36 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FRANCE; $coupons = [ new CampaignCoupon( new ProcessingCode('PC1'), - StockId::generate(), + new CouponQuantity(100), + new CountryAuthorized(Country::FRANCE), new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) ]; - $brand = new BrandCode('AAA01'); + $brandCode = new BrandCode('AAA01'); // WHEN $campaign = Campaign::hydrate( $id, $code, - $this->tag, $name, $publication, $printAllAtOnce, - $availableIn, $coupons, - $brand, + $brandCode, ); // THEN expect($campaign->id())->toBe($id) ->and($campaign->code)->toBe($code) - ->and($campaign->tag)->toBe($this->tag) ->and($campaign->name)->toBe($name) ->and($campaign->publication)->toBe($publication) ->and($campaign->printAllAtOnce)->toBe($printAllAtOnce) - ->and($campaign->availableIn)->toBe($availableIn) ->and($campaign->coupons)->toBe($coupons) - ->and($campaign->brand)->toBe($brand) + ->and($campaign->brandCode)->toBe($brandCode) ->and($campaign->updatedAt)->toBeNull() ->and($campaign->updatedAt())->toBeNull(); }); @@ -87,21 +74,18 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FRANCE; $coupons = []; - $brand = new BrandCode('AAA01'); + $brandCode = new BrandCode('AAA01'); // WHEN $campaign = Campaign::hydrate( $id, $code, - $this->tag, $name, $publication, $printAllAtOnce, - $availableIn, $coupons, - $brand, + $brandCode, ); // THEN @@ -115,29 +99,27 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FRANCE; $coupons = [ new CampaignCoupon( new ProcessingCode('PC1'), - StockId::generate(), + new CouponQuantity(100), + new CountryAuthorized(Country::FRANCE), new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) ]; - $brand = new BrandCode('AAA01'); + $brandCode = new BrandCode('AAA01'); $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); // WHEN $campaign = Campaign::hydrate( $id, $code, - $this->tag, $name, $publication, $printAllAtOnce, - $availableIn, $coupons, - $brand, + $brandCode, $updatedAt ); @@ -152,39 +134,35 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FRANCE; $coupons = [ new CampaignCoupon( new ProcessingCode('PC1'), - StockId::generate(), + new CouponQuantity(100), + new CountryAuthorized(Country::FRANCE), new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) ]; - $brand = new BrandCode('AAA01'); + $brandCode = new BrandCode('AAA01'); // WHEN $campaign = Campaign::create( $code, - $this->tag, $name, $publication, $printAllAtOnce, - $availableIn, $coupons, - $brand + $brandCode ); // THEN expect($campaign->id())->toBeInstanceOf(CampaignId::class) ->and($campaign->code)->toBe($code) - ->and($campaign->tag)->toBe($this->tag) ->and($campaign->name)->toBe($name) ->and($campaign->publication)->toBe($publication) ->and($campaign->printAllAtOnce)->toBe($printAllAtOnce) - ->and($campaign->availableIn)->toBe($availableIn) ->and($campaign->coupons)->toBe($coupons) - ->and($campaign->brand)->toBe($brand) + ->and($campaign->brandCode)->toBe($brandCode) ->and($campaign->updatedAt)->toBeNull() ->and($campaign->updatedAt())->toBeNull(); }); @@ -195,37 +173,33 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FRANCE; $coupons = [ new CampaignCoupon( new ProcessingCode('PC1'), - StockId::generate(), + new CouponQuantity(100), + new CountryAuthorized(Country::FRANCE), new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) ]; - $brand = new BrandCode('AAA01'); + $brandCode = new BrandCode('AAA01'); // WHEN $firstCampaign = Campaign::create( $code, - $this->tag, $name, $publication, $printAllAtOnce, - $availableIn, $coupons, - $brand + $brandCode ); $secondCampaign = Campaign::create( $code, - $this->tag, $name, $publication, $printAllAtOnce, - $availableIn, $coupons, - $brand + $brandCode ); // THEN @@ -239,15 +213,14 @@ describe('campaign', function (): void { $campaign = Campaign::hydrate( $id, new CampaignCode('Camp100'), - $this->tag, new CampaignName('Name'), \boolval(0), \boolval(1), - Country::FRANCE, [ new CampaignCoupon( new ProcessingCode('PC1'), - StockId::generate(), + new CouponQuantity(100), + new CountryAuthorized(Country::FRANCE), new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) @@ -269,21 +242,18 @@ describe('campaign', function (): void { $name = new CampaignName('Name'); $publication = \boolval(0); $printAllAtOnce = \boolval(1); - $availableIn = Country::FRANCE; $coupons = ['PC1']; - $brand = new BrandCode('AAA01'); + $brandCode = new BrandCode('AAA01'); // WHEN / THEN expect(fn (): Campaign => Campaign::hydrate( $id, $code, - $this->tag, $name, $publication, $printAllAtOnce, - $availableIn, $coupons, - $brand + $brandCode ))->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 97c243c..2ca903e 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -5,199 +5,323 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; +use DateTime; use Domain\Enums\BarcodeFormat; use Shared\Enums\Country; use Domain\Models\Entities\Coupon; use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar; +use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; use Domain\Models\ValueObjects\Coupon\CouponId; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; -use Domain\Models\ValueObjects\Coupon\CouponProduct; use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; -use Domain\Models\ValueObjects\CouponProductName; use Domain\Models\ValueObjects\GS1; +use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\ProcessingCode; use Ramsey\Uuid\Uuid; use Shared\Enums\Currency; -use DateTime; use Shared\ValueObjects\Money; covers(Coupon::class); describe('coupon', function (): void { + beforeEach(function (): void { + $this->code = new CouponCode('COU01'); + $this->name = new CouponName('Name'); + $this->amount = new MoneyAmount(new Money(Currency::EUR, 1)); + $this->picture = new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/inactive_thumbnail.jpg') + ); + $this->diffusion = new CouponDiffusion([new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))]); + $this->validity = new CouponValidity(30, new DateTime('2026-02-01')); + $this->couponValidityInformation = [ + new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + new CountryAuthorized(Country::FRANCE) + ) + ]; + $this->couponLegalInformation = [ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + new CountryAuthorized(Country::FRANCE) + ) + ]; + $this->processingCode = new ProcessingCode('5425003944130'); + $this->brandCode = new BrandCode('AAA01'); + $this->createdBy = ActorId::generate(); + $this->products = [new IncoCode(35784), new IncoCode(15522)]; + $this->availabilityCalendar = new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]); + $this->gtin = new GS1('110101010', BarcodeFormat::EAN13); + }); + afterEach(function (): void { CarbonImmutable::setTestNow(); }); 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([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::FRANCE - ) - ]; - $code = new ProcessingCode('5425003944130'); - $brandCode = new BrandCode('AAA01'); - $createdBy = ActorId::generate(); - $gtin = new GS1('110101010', BarcodeFormat::EAN13); + $id = CouponId::generate(); // WHEN $coupon = Coupon::hydrate( $id, - $name, - $status, - $productName, - $amount, - $picture, - $product, - $diffusion, - $availability, - $validity, - $legalInformation, - $code, - $brandCode, - $createdBy, - $gtin, + $this->code, + $this->name, + $this->amount, + $this->picture, + $this->diffusion, + $this->validity, + $this->couponValidityInformation, + $this->couponLegalInformation, + $this->processingCode, + $this->brandCode, + $this->createdBy, + $this->products, + $this->availabilityCalendar, + $this->gtin, ); // THEN expect($coupon->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->legalInformation)->toBe($legalInformation) - ->and($coupon->code)->toBe($code) - ->and($coupon->brandCode)->toBe($brandCode) - ->and($coupon->createdBy)->toBe($createdBy) - ->and($coupon->gtin)->toBe($gtin) + ->and($coupon->code)->toBe($this->code) + ->and($coupon->name)->toBe($this->name) + ->and($coupon->amount)->toBe($this->amount) + ->and($coupon->picture)->toBe($this->picture) + ->and($coupon->diffusion)->toBe($this->diffusion) + ->and($coupon->validity)->toBe($this->validity) + ->and($coupon->couponValidityInformation)->toBe($this->couponValidityInformation) + ->and($coupon->couponLegalInformation)->toBe($this->couponLegalInformation) + ->and($coupon->processingCode)->toBe($this->processingCode) + ->and($coupon->brandCode)->toBe($this->brandCode) + ->and($coupon->createdBy)->toBe($this->createdBy) + ->and($coupon->products)->toBe($this->products) + ->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar) + ->and($coupon->gtin)->toBe($this->gtin) ->and($coupon->updatedAt)->toBeNull() ->and($coupon->updatedAt())->toBeNull(); }); - it('creates a coupon without gs1', function (): void { + it('hydrates 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([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::FRANCE - ) - ]; - $code = new ProcessingCode('5425003944130'); - $brandCode = new BrandCode('AAA01'); - $createdBy = ActorId::generate(); + $id = CouponId::generate(); // WHEN $coupon = Coupon::hydrate( - $id, - $name, - $status, - $productName, - $amount, - $picture, - $product, - $diffusion, - $availability, - $validity, - $legalInformation, - $code, - $brandCode, - $createdBy, + id: $id, + code: $this->code, + name: $this->name, + amount: $this->amount, + picture: $this->picture, + diffusion: $this->diffusion, + validity: $this->validity, + couponValidityInformation: $this->couponValidityInformation, + couponLegalInformation: $this->couponLegalInformation, + processingCode: $this->processingCode, + brandCode: $this->brandCode, + createdBy: $this->createdBy, + products: $this->products, + availabilityCalendar: $this->availabilityCalendar + ); + + // THEN + expect($coupon->products)->toBe($this->products) + ->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar) + ->and($coupon->gtin)->toBeNull(); + }); + + it('hydrates a coupon without availability calendar', function (): void { + // GIVEN + $id = CouponId::generate(); + + // WHEN + $coupon = Coupon::hydrate( + id: $id, + code: $this->code, + name: $this->name, + amount: $this->amount, + picture: $this->picture, + diffusion: $this->diffusion, + validity: $this->validity, + couponValidityInformation: $this->couponValidityInformation, + couponLegalInformation: $this->couponLegalInformation, + processingCode: $this->processingCode, + brandCode: $this->brandCode, + createdBy: $this->createdBy, + products: $this->products, + gtin: $this->gtin + ); + + // THEN + expect($coupon->availabilityCalendar)->toBeNull() + ->and($coupon->products)->toBe($this->products) + ->and($coupon->gtin)->toBe($this->gtin); + }); + + it('hydrates a coupon without products', function (): void { + // GIVEN + $id = CouponId::generate(); + + // WHEN + $coupon = Coupon::hydrate( + id: $id, + code: $this->code, + name: $this->name, + amount: $this->amount, + picture: $this->picture, + diffusion: $this->diffusion, + validity: $this->validity, + couponValidityInformation: $this->couponValidityInformation, + couponLegalInformation: $this->couponLegalInformation, + processingCode: $this->processingCode, + brandCode: $this->brandCode, + createdBy: $this->createdBy, + availabilityCalendar: $this->availabilityCalendar, + gtin: $this->gtin + ); + + // THEN + expect($coupon->products)->toBe([]) + ->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar) + ->and($coupon->gtin)->toBe($this->gtin); + }); + + it('hydrates a coupon with only required fields', function (): void { + // GIVEN + $id = CouponId::generate(); + + // WHEN + $coupon = Coupon::hydrate( + id: $id, + code: $this->code, + name: $this->name, + amount: $this->amount, + picture: $this->picture, + diffusion: $this->diffusion, + validity: $this->validity, + couponValidityInformation: $this->couponValidityInformation, + couponLegalInformation: $this->couponLegalInformation, + processingCode: $this->processingCode, + brandCode: $this->brandCode, + createdBy: $this->createdBy, ); // THEN expect($coupon->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->legalInformation)->toBe($legalInformation) - ->and($coupon->code)->toBe($code) - ->and($coupon->brandCode)->toBe($brandCode) - ->and($coupon->createdBy)->toBe($createdBy) + ->and($coupon->products)->toBe([]) + ->and($coupon->availabilityCalendar)->toBeNull() + ->and($coupon->gtin)->toBeNull() ->and($coupon->updatedAt)->toBeNull() ->and($coupon->updatedAt())->toBeNull(); }); - it('creates a coupon with an explicit updatedAt date', function (): void { + it('accepts empty validity and legal information lists', 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([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::FRANCE - ) - ]; - $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(); // WHEN $coupon = Coupon::hydrate( $id, - $name, - $status, - $productName, - $amount, - $picture, - $product, - $diffusion, - $availability, - $validity, - $legalInformation, - $code, - $brandCode, - $createdBy, - $gtin, + $this->code, + $this->name, + $this->amount, + $this->picture, + $this->diffusion, + $this->validity, + [], + [], + $this->processingCode, + $this->brandCode, + $this->createdBy, + $this->products, + $this->availabilityCalendar, + $this->gtin, + ); + + // THEN + expect($coupon->couponValidityInformation)->toBe([]) + ->and($coupon->couponLegalInformation)->toBe([]); + }); + + it('throws when validity information contains an invalid item', function (): void { + // GIVEN + $id = CouponId::generate(); + + // WHEN / THEN + expect(fn () => Coupon::hydrate( + $id, + $this->code, + $this->name, + $this->amount, + $this->picture, + $this->diffusion, + $this->validity, + ['Not a CouponValidityInformation'], + $this->couponLegalInformation, + $this->processingCode, + $this->brandCode, + $this->createdBy, + $this->products, + $this->availabilityCalendar, + $this->gtin, + ))->toThrow(\DomainException::class, 'Coupon validity information must be CouponValidityInformation instances.'); + }); + + it('throws when legal information contains an invalid item', function (): void { + // GIVEN + $id = CouponId::generate(); + + // WHEN / THEN + expect(fn () => Coupon::hydrate( + $id, + $this->code, + $this->name, + $this->amount, + $this->picture, + $this->diffusion, + $this->validity, + $this->couponValidityInformation, + ['Not a CouponLegalInformation'], + $this->processingCode, + $this->brandCode, + $this->createdBy, + $this->products, + $this->availabilityCalendar, + $this->gtin, + ))->toThrow(\DomainException::class, 'Coupon legal information must be CouponLegalInformation instances.'); + }); + + it('hydrates a coupon with an explicit updatedAt date', function (): void { + // GIVEN + $id = CouponId::generate(); + $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); + + // WHEN + $coupon = Coupon::hydrate( + $id, + $this->code, + $this->name, + $this->amount, + $this->picture, + $this->diffusion, + $this->validity, + $this->couponValidityInformation, + $this->couponLegalInformation, + $this->processingCode, + $this->brandCode, + $this->createdBy, + $this->products, + $this->availabilityCalendar, + $this->gtin, $updatedAt ); @@ -207,119 +331,77 @@ 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([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::FRANCE - ) - ]; - $code = new ProcessingCode('5425003944130'); - $brandCode = new BrandCode('AAA01'); - $createdBy = ActorId::generate(); - $gtin = new GS1('110101010', BarcodeFormat::EAN13); - - // WHEN + // GIVEN / WHEN $coupon = Coupon::create( - $name, - $status, - $productName, - $amount, - $picture, - $product, - $diffusion, - $availability, - $validity, - $legalInformation, - $code, - $brandCode, - $createdBy, - $gtin, + $this->code, + $this->name, + $this->amount, + $this->picture, + $this->diffusion, + $this->validity, + $this->couponValidityInformation, + $this->couponLegalInformation, + $this->processingCode, + $this->brandCode, + $this->createdBy, + $this->products, + $this->availabilityCalendar, + $this->gtin, ); // 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->legalInformation)->toBe($legalInformation) - ->and($coupon->code)->toBe($code) - ->and($coupon->brandCode)->toBe($brandCode) - ->and($coupon->createdBy)->toBe($createdBy) - ->and($coupon->gtin)->toBe($gtin) + ->and($coupon->code)->toBe($this->code) + ->and($coupon->name)->toBe($this->name) + ->and($coupon->amount)->toBe($this->amount) + ->and($coupon->picture)->toBe($this->picture) + ->and($coupon->diffusion)->toBe($this->diffusion) + ->and($coupon->validity)->toBe($this->validity) + ->and($coupon->couponValidityInformation)->toBe($this->couponValidityInformation) + ->and($coupon->couponLegalInformation)->toBe($this->couponLegalInformation) + ->and($coupon->processingCode)->toBe($this->processingCode) + ->and($coupon->brandCode)->toBe($this->brandCode) + ->and($coupon->createdBy)->toBe($this->createdBy) + ->and($coupon->products)->toBe($this->products) + ->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar) + ->and($coupon->gtin)->toBe($this->gtin) ->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([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::FRANCE - ) - ]; - $code = new ProcessingCode('5425003944130'); - $brandCode = new BrandCode('AAA01'); - $createdBy = ActorId::generate(); - $gtin = new GS1('110101010', BarcodeFormat::EAN13); - - // WHEN + // GIVEN / WHEN $firstCoupon = Coupon::create( - $name, - $status, - $productName, - $amount, - $picture, - $product, - $diffusion, - $availability, - $validity, - $legalInformation, - $code, - $brandCode, - $createdBy, - $gtin + $this->code, + $this->name, + $this->amount, + $this->picture, + $this->diffusion, + $this->validity, + $this->couponValidityInformation, + $this->couponLegalInformation, + $this->processingCode, + $this->brandCode, + $this->createdBy, + $this->products, + $this->availabilityCalendar, + $this->gtin, ); $secondCoupon = Coupon::create( - $name, - $status, - $productName, - $amount, - $picture, - $product, - $diffusion, - $availability, - $validity, - $legalInformation, - $code, - $brandCode, - $createdBy, - $gtin + $this->code, + $this->name, + $this->amount, + $this->picture, + $this->diffusion, + $this->validity, + $this->couponValidityInformation, + $this->couponLegalInformation, + $this->processingCode, + $this->brandCode, + $this->createdBy, + $this->products, + $this->availabilityCalendar, + $this->gtin, ); // THEN @@ -332,19 +414,23 @@ describe('coupon', function (): void { $id = new CouponId(Uuid::uuid7($expected)); $coupon = Coupon::hydrate( $id, + new CouponCode('COU01'), 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 CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/inactive_thumbnail.jpg') + ), 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::FRANCE)], + new CouponValidity(30, new DateTime('2026-02-01')), + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], new ProcessingCode('5425003944130'), new BrandCode('AAA01'), ActorId::generate(), + [new IncoCode(35784), new IncoCode(15522)], + new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]), new GS1('110101010', BarcodeFormat::EAN13), ); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php index 340d03c..4608cca 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php @@ -4,10 +4,12 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\Campaign; +use Domain\Enums\Country; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; +use Domain\Models\ValueObjects\CountryAuthorized; +use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Domain\Models\ValueObjects\Stock\StockId; use Domain\Models\ValueObjects\WarningThresholdQuantity; covers(CampaignCoupon::class); @@ -15,33 +17,79 @@ covers(CampaignCoupon::class); describe('campaign coupon', function (): void { it('creates a valid campaign coupon', function (): void { // GIVEN - $code = new ProcessingCode('PC1'); - $qty = StockId::generate(); - $printRules = new PrintRules(maxPerCustomer: 3); - $warningThresholdQty = new WarningThresholdQuantity(100); + $processingCode = new ProcessingCode('PC1'); + $quantity = new CouponQuantity(10); + $availableIn = new CountryAuthorized(Country::FRANCE); + $warningThresholdQuantity = new WarningThresholdQuantity(100); + $printRules = new PrintRules(maxPerCustomer: 3); // WHEN - $campaignCoupon = new CampaignCoupon($code, $qty, $warningThresholdQty, $printRules); + $campaignCoupon = new CampaignCoupon($processingCode, $quantity, $availableIn, $warningThresholdQuantity, $printRules); // THEN - expect($campaignCoupon->code)->toBe($code) - ->and($campaignCoupon->qty)->toBe($qty) - ->and($campaignCoupon->printRules)->toBe($printRules) - ->and($campaignCoupon->warningThresholdQty)->toBe($warningThresholdQty); + expect($campaignCoupon->processingCode)->toBe($processingCode) + ->and($campaignCoupon->quantity)->toBe($quantity) + ->and($campaignCoupon->availableIn)->toBe($availableIn) + ->and($campaignCoupon->warningThresholdQuantity)->toBe($warningThresholdQuantity) + ->and($campaignCoupon->printRules)->toBe($printRules); + }); + + it('creates a valid campaign coupon whitout warning threshold quantity', function (): void { + // GIVEN + $processingCode = new ProcessingCode('PC1'); + $quantity = new CouponQuantity(10); + $availableIn = new CountryAuthorized(Country::FRANCE); + $printRules = new PrintRules(maxPerCustomer: 3); + + // WHEN + $campaignCoupon = new CampaignCoupon( + processingCode: $processingCode, + quantity: $quantity, + availableIn: $availableIn, + printRules: $printRules + ); + + // THEN + expect($campaignCoupon->processingCode)->toBe($processingCode) + ->and($campaignCoupon->quantity)->toBe($quantity) + ->and($campaignCoupon->availableIn)->toBe($availableIn) + ->and($campaignCoupon->printRules)->toBe($printRules); }); it('creates a valid campaign coupon whitout print rules', function (): void { // GIVEN - $code = new ProcessingCode('PC1'); - $qty = StockId::generate(); - $warningThresholdQty = new WarningThresholdQuantity(100); + $processingCode = new ProcessingCode('PC1'); + $quantity = new CouponQuantity(10); + $availableIn = new CountryAuthorized(Country::FRANCE); + $warningThresholdQuantity = new WarningThresholdQuantity(100); // WHEN - $campaignCoupon = new CampaignCoupon($code, $qty, $warningThresholdQty); + $campaignCoupon = new CampaignCoupon( + processingCode: $processingCode, + quantity: $quantity, + availableIn: $availableIn, + warningThresholdQuantity: $warningThresholdQuantity + ); // THEN - expect($campaignCoupon->code)->toBe($code) - ->and($campaignCoupon->qty)->toBe($qty) - ->and($campaignCoupon->warningThresholdQty)->toBe($warningThresholdQty); + expect($campaignCoupon->processingCode)->toBe($processingCode) + ->and($campaignCoupon->quantity)->toBe($quantity) + ->and($campaignCoupon->availableIn)->toBe($availableIn) + ->and($campaignCoupon->warningThresholdQuantity)->toBe($warningThresholdQuantity); + }); + + it('creates a valid campaign coupon whitout warning threshold quantity and print rules', function (): void { + // GIVEN + $processingCode = new ProcessingCode('PC1'); + $quantity = new CouponQuantity(10); + $availableIn = new CountryAuthorized(Country::FRANCE); + + // WHEN + $campaignCoupon = new CampaignCoupon(processingCode: $processingCode, quantity: $quantity, availableIn: $availableIn); + + // THEN + expect($campaignCoupon->processingCode)->toBe($processingCode) + ->and($campaignCoupon->quantity)->toBe($quantity) + ->and($campaignCoupon->availableIn)->toBe($availableIn); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCodeTest.php new file mode 100644 index 0000000..a139b96 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCodeTest.php @@ -0,0 +1,75 @@ +toBeInstanceOf(CouponCode::class) + ->and($code->value)->toBe($value); + }); +}); + +describe('coupon code validation rules', function (): void { + it('rejects invalid coupon codes', function (string $invalid): void { + // WHEN / THEN + expect(fn (): CouponCode => new CouponCode($invalid))->toThrow(\DomainException::class); + + // GIVEN + })->with([ + // rejects empty strings + '', + ' ', + "\t", + "\n", + // rejects numbers only + '12345', + '67890', + // rejects letters only + 'ABCDE', + 'FGHIJ', + // rejects codes with special characters + 'AB@12', + 'CD#34', + 'EF$56', + // rejects codes that have the same digit repeated + 'ABC11', + 'DEF22', + 'GHI33', + // rejects codes with lowercase letters + 'AbC12', + 'dEf34', + 'GhI56', + ]); + + it('rejects codes with more/less than 3 letters or more/less than 2 digits', function (string $invalid): void { + // WHEN / THEN + expect(fn (): CouponCode => new CouponCode($invalid))->toThrow(\DomainException::class); + + // GIVEN + })->with([ + 'ABCD12', + 'ABC123', + 'ABCDE123', + 'AB12', + 'A1', + 'AB1', + 'A12', + 'AB123', + 'A123', + 'ABC1', + 'AB12C', + ]); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php index 77fa49e..335caad 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\Unit\Domain\Models\ValueObjects; +namespace Tests\Unit\Domain\Models\ValueObjects\Coupon; use Shared\Enums\Country; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php deleted file mode 100644 index 293e8b6..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductInfoTest.php +++ /dev/null @@ -1,40 +0,0 @@ -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::FRANCE; - - // 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 deleted file mode 100644 index 16e2459..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductTest.php +++ /dev/null @@ -1,36 +0,0 @@ -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/CouponQuantityTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php new file mode 100644 index 0000000..f3aa565 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php @@ -0,0 +1,39 @@ +toBeInstanceOf(CouponQuantity::class) + ->and($couponQuantity->quantity)->toBe($quantity); + }); + + it('rejects a quantity of zero', function (): void { + // WHEN + $action = fn (): CouponQuantity => new CouponQuantity(0); + + // THEN + expect($action)->toThrow(\DomainException::class, 'CouponQuantity quantity must be positive.'); + }); + + it('rejects a negative quantity', function (): void { + // WHEN + $action = fn (): CouponQuantity => new CouponQuantity(-1); + + // THEN + expect($action)->toThrow(\DomainException::class, 'CouponQuantity quantity must be positive.'); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php new file mode 100644 index 0000000..3a43b39 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php @@ -0,0 +1,35 @@ +validityText)->toBe($validityText) + ->and($couponValidityInformation->forLocale)->toBe($forLocale); + }); + + it('rejects invalid coupon validity information', function (): void { + // GIVEN + $validityText = ''; + $forLocale = new CountryAuthorized(Country::FRANCE); + + // GIVEN / THEN + expect(fn (): CouponValidityInformation => new CouponValidityInformation($validityText, $forLocale))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/CouponProductNameTest.php b/tests/Unit/Domain/Models/ValueObjects/CouponProductNameTest.php deleted file mode 100644 index 0c96164..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/CouponProductNameTest.php +++ /dev/null @@ -1,30 +0,0 @@ -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/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index 2913dc3..267a401 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -5,67 +5,239 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\RequestedCoupon; use Carbon\CarbonImmutable; +use DateTime; use Domain\Enums\BarcodeFormat; +use Domain\Enums\Country; +use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; use Domain\Models\ValueObjects\CouponPicture; -use Domain\Models\ValueObjects\CouponProductName; use Domain\Models\ValueObjects\GS1; +use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; +use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Shared\Enums\Currency; -use DateTime; -use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Shared\ValueObjects\Money; covers(RequestedCoupon::class); describe('requested coupon', function (): void { + beforeEach(function (): void { + $this->name = new CouponName('Coupon A'); + $this->quantity = new RequestedCouponQuantity(100); + $this->diffusion = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); + $this->moneyAmount = new MoneyAmount(new Money(Currency::EUR, 300)); + $this->processingCode = new ProcessingCode('PC1'); + $this->couponPicture = new CouponPicture( + new PicturePath('coupons/product.jpg'), + new PicturePath('coupons/thumbnail.jpg'), + new PicturePath('coupons/inactive_thumbnail.jpg') + ); + $this->couponValidity = new CouponValidity(30, new DateTime('2026-02-01')); + $this->couponValidityInformation = [ + new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + new CountryAuthorized(Country::FRANCE) + ), + ]; + $this->products = [new IncoCode(35784), new IncoCode(15522)]; + $this->printRules = new PrintRules(maxPerCustomer: 3); + $this->gs1 = new GS1('01234567890128', BarcodeFormat::EAN13); + $this->warningThresholdQuantity = new WarningThresholdQuantity(10); + }); + afterEach(function (): void { CarbonImmutable::setTestNow(); }); it('hydrates a requested coupon with all its fields', function (): void { - // GIVEN - $name = new CouponName('Coupon A'); - $productName = new CouponProductName('Product A'); - $quantity = new RequestedCouponQuantity(100); - $warningThresholdQuantity = new WarningThresholdQuantity(10); - $period = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); - $moneyAmount = new MoneyAmount(new Money(Currency::EUR, 300)); - $processingCode = new ProcessingCode('PC1'); - $couponPicture = new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')); - $gs1 = new GS1('01234567890128', BarcodeFormat::EAN13); - $couponValidity = new CouponValidity(30, new DateTime()); - // WHEN + // GIVEN / WHEN $coupon = new RequestedCoupon( - $name, - $productName, - $quantity, - $warningThresholdQuantity, - $period, - $moneyAmount, - $processingCode, - $couponPicture, - $gs1, - $couponValidity, + $this->name, + $this->quantity, + $this->diffusion, + $this->moneyAmount, + $this->processingCode, + $this->couponPicture, + $this->couponValidity, + $this->couponValidityInformation, + $this->products, + $this->printRules, + $this->gs1, + $this->warningThresholdQuantity ); // THEN - expect($coupon->name)->toBe($name) - ->and($coupon->productName)->toBe($productName) - ->and($coupon->quantity)->toBe($quantity) - ->and($coupon->warningThresholdQuantity)->toBe($warningThresholdQuantity) - ->and($coupon->period)->toBe($period) - ->and($coupon->moneyAmount)->toBe($moneyAmount) - ->and($coupon->processingCode)->toBe($processingCode) - ->and($coupon->couponPicture)->toBe($couponPicture) - ->and($coupon->gs1)->toBe($gs1) - ->and($coupon->couponValidity)->toBe($couponValidity); + expect($coupon->name)->toBe($this->name) + ->and($coupon->quantity)->toBe($this->quantity) + ->and($coupon->diffusion)->toBe($this->diffusion) + ->and($coupon->moneyAmount)->toBe($this->moneyAmount) + ->and($coupon->processingCode)->toBe($this->processingCode) + ->and($coupon->couponPicture)->toBe($this->couponPicture) + ->and($coupon->couponValidity)->toBe($this->couponValidity) + ->and($coupon->couponValidityInformation)->toBe($this->couponValidityInformation) + ->and($coupon->products)->toBe($this->products) + ->and($coupon->printRules)->toBe($this->printRules) + ->and($coupon->gs1)->toBe($this->gs1) + ->and($coupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); + }); + + it('hydrates a requested coupon without products', function (): void { + // GIVEN / WHEN + $coupon = new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $this->moneyAmount, + processingCode: $this->processingCode, + couponPicture:$this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: $this->couponValidityInformation, + printRules: $this->printRules, + gs1: $this->gs1, + warningThresholdQuantity: $this->warningThresholdQuantity + ); + + // THEN + expect($coupon->products)->toBe([]) + ->and($coupon->printRules)->toBe($this->printRules) + ->and($coupon->gs1)->toBe($this->gs1) + ->and($coupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); + }); + + it('hydrates a requested coupon without print rules', function (): void { + // GIVEN / WHEN + $coupon = new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $this->moneyAmount, + processingCode: $this->processingCode, + couponPicture:$this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: $this->couponValidityInformation, + products: $this->products, + gs1: $this->gs1, + warningThresholdQuantity: $this->warningThresholdQuantity + ); + + // THEN + expect($coupon->printRules)->toBeNull() + ->and($coupon->products)->toBe($this->products) + ->and($coupon->gs1)->toBe($this->gs1) + ->and($coupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); + }); + + it('hydrates a requested coupon without gs1', function (): void { + // GIVEN / WHEN + $coupon = new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $this->moneyAmount, + processingCode: $this->processingCode, + couponPicture:$this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: $this->couponValidityInformation, + products: $this->products, + printRules: $this->printRules, + warningThresholdQuantity: $this->warningThresholdQuantity + ); + + // THEN + expect($coupon->gs1)->toBeNull() + ->and($coupon->products)->toBe($this->products) + ->and($coupon->printRules)->toBe($this->printRules) + ->and($coupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); + }); + + it('hydrates a requested coupon without warning threshold quantity', function (): void { + // GIVEN / WHEN + $coupon = new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $this->moneyAmount, + processingCode: $this->processingCode, + couponPicture:$this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: $this->couponValidityInformation, + products: $this->products, + printRules: $this->printRules, + gs1: $this->gs1, + ); + + // THEN + expect($coupon->warningThresholdQuantity)->toBeNull() + ->and($coupon->products)->toBe($this->products) + ->and($coupon->printRules)->toBe($this->printRules) + ->and($coupon->gs1)->toBe($this->gs1); + }); + + it('hydrates a requested coupon with only required fields', function (): void { + // GIVEN / WHEN + $coupon = new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $this->moneyAmount, + processingCode: $this->processingCode, + couponPicture:$this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: $this->couponValidityInformation, + ); + + // THEN + expect($coupon->products)->toBe([]) + ->and($coupon->printRules)->toBeNull() + ->and($coupon->gs1)->toBeNull() + ->and($coupon->warningThresholdQuantity)->toBeNull(); + }); + + it('accepts an empty validity information list', function (): void { + // GIVEN / WHEN + $coupon = new RequestedCoupon( + $this->name, + $this->quantity, + $this->diffusion, + $this->moneyAmount, + $this->processingCode, + $this->couponPicture, + $this->couponValidity, + [], + $this->products, + $this->printRules, + $this->gs1, + $this->warningThresholdQuantity + ); + + // THEN + expect($coupon->couponValidityInformation)->toBe([]); + }); + + it('throws when validity information contains an invalid item', function (): void { + // GIVEN / WHEN / THEN + expect(fn () => new RequestedCoupon( + $this->name, + $this->quantity, + $this->diffusion, + $this->moneyAmount, + $this->processingCode, + $this->couponPicture, + $this->couponValidity, + ['Not a CouponValidityInformation'], + $this->products, + $this->printRules, + $this->gs1, + $this->warningThresholdQuantity + ))->toThrow(\DomainException::class, 'Coupon validity information must be CouponValidityInformation instances.'); }); it('hydrates a requested coupon without a GS1', function (): void { From 7265d2fa5f615e13cf46f3b3d2fedd90157a2a78 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 29 Sep 2026 18:26:18 +0200 Subject: [PATCH 45/52] fix: :bug: resolve some tests --- .../List/CampaignRequestListItem.php | 1 + .../ValueObjects/Campaign/CampaignCoupon.php | 4 ++-- .../Coupon/CouponValidityInformation.php | 4 ++-- .../Find/FindCampaignRequestTest.php | 4 ++-- .../List/ListCampaignsRequestTest.php | 9 +++---- .../Reject/RejectCampaignRequestTest.php | 4 ++-- .../Models/Entities/CampaignRequestTest.php | 5 ++-- .../Domain/Models/Entities/CampaignTest.php | 11 ++++----- .../Domain/Models/Entities/CouponTest.php | 8 +++---- .../Campaign/CampaignCouponTest.php | 11 ++++----- .../Coupon/CouponValidityInformationTest.php | 7 +++--- .../RequestedCoupon/RequestedCouponTest.php | 24 ++----------------- 12 files changed, 35 insertions(+), 57 deletions(-) diff --git a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php index 852118e..baa9700 100644 --- a/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php +++ b/src/Application/UseCases/CampaignRequest/List/CampaignRequestListItem.php @@ -14,6 +14,7 @@ final readonly class CampaignRequestListItem { public function __construct( public BrandCode $brandCode, + public CampaignName $name, public Applicant $applicant, public CampaignRequestType $campaignType, public CampaignRequestStatus $status diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php index 713d13b..e6ae5f4 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php @@ -4,18 +4,18 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Campaign; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\WarningThresholdQuantity; +use Shared\Enums\Country; final readonly class CampaignCoupon { public function __construct( public ProcessingCode $processingCode, public CouponQuantity $quantity, - public CountryAuthorized $availableIn, + public Country $availableIn, public ?WarningThresholdQuantity $warningThresholdQuantity = null, public ?PrintRules $printRules = null, ) { diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformation.php b/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformation.php index 517b714..5ac7ff5 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformation.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformation.php @@ -4,13 +4,13 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Coupon; -use Domain\Models\ValueObjects\CountryAuthorized; +use Shared\Enums\Country; final readonly class CouponValidityInformation { public function __construct( public string $validityText, - public CountryAuthorized $forLocale + public Country $forLocale ) { if (mb_trim($this->validityText) === '') { throw new \DomainException('Validity text cannot be empty.'); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 9103821..606acd6 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -11,7 +11,6 @@ use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestNotFound; use Application\UseCases\CampaignRequest\Find\Output\Output; use DateTime; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Country; use Domain\Exceptions\BrandNotFoundException; use Domain\Models\Entities\Brand; use Domain\Models\Entities\CampaignRequest; @@ -41,6 +40,7 @@ use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; +use Shared\Enums\Country; use Shared\Enums\Currency; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; @@ -62,7 +62,7 @@ describe('find campaign request use case', function (): void { new PicturePath('coupons/inactive_thumbnail.jpg') ), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 11da86f..dd4f0fe 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -6,8 +6,6 @@ namespace Application\UseCases\CampaignRequest\List; use DateTime; use Domain\Enums\BarcodeFormat; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; -use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignName; @@ -17,6 +15,8 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortClause; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; +use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; @@ -32,6 +32,7 @@ use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; +use Shared\Enums\Country; use Shared\Enums\Currency; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; @@ -54,7 +55,7 @@ describe('list campaigns request use case', function (): void { new PicturePath('coupons/inactive_thumbnail.jpg') ), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), @@ -73,7 +74,7 @@ describe('list campaigns request use case', function (): void { new PicturePath('coupons/inactive_thumbnail.jpg') ), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], [new IncoCode(24514), new IncoCode(46259)], new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 0d95d6b..85f1bfa 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -12,7 +12,6 @@ use Application\UseCases\CampaignRequest\Reject\RejectCampaignRequest; use Carbon\CarbonImmutable; use DateTime; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignName; @@ -36,6 +35,7 @@ use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; +use Shared\Enums\Country; use Shared\Enums\Currency; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; @@ -56,7 +56,7 @@ describe('reject campaign request use case', function (): void { new PicturePath('coupons/inactive_thumbnail.jpg') ), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index a978935..8b9e532 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -7,7 +7,6 @@ namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; use DateTime; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Country; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignName; @@ -16,7 +15,6 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; @@ -32,6 +30,7 @@ use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; use Ramsey\Uuid\Uuid; +use Shared\Enums\Country; use Shared\Enums\Currency; use Shared\ValueObjects\Email; use Shared\ValueObjects\Money; @@ -56,7 +55,7 @@ describe('campaign request', function (): void { new PicturePath('coupons/inactive_thumbnail.jpg') ), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index d81b195..4952579 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -12,7 +12,6 @@ use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; @@ -37,7 +36,7 @@ describe('campaign', function (): void { new CampaignCoupon( new ProcessingCode('PC1'), new CouponQuantity(100), - new CountryAuthorized(Country::FRANCE), + Country::FRANCE, new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) @@ -103,7 +102,7 @@ describe('campaign', function (): void { new CampaignCoupon( new ProcessingCode('PC1'), new CouponQuantity(100), - new CountryAuthorized(Country::FRANCE), + Country::FRANCE, new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) @@ -138,7 +137,7 @@ describe('campaign', function (): void { new CampaignCoupon( new ProcessingCode('PC1'), new CouponQuantity(100), - new CountryAuthorized(Country::FRANCE), + Country::FRANCE, new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) @@ -177,7 +176,7 @@ describe('campaign', function (): void { new CampaignCoupon( new ProcessingCode('PC1'), new CouponQuantity(100), - new CountryAuthorized(Country::FRANCE), + Country::FRANCE, new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) @@ -220,7 +219,7 @@ describe('campaign', function (): void { new CampaignCoupon( new ProcessingCode('PC1'), new CouponQuantity(100), - new CountryAuthorized(Country::FRANCE), + Country::FRANCE, new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index 2ca903e..623ebc4 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -47,13 +47,13 @@ describe('coupon', function (): void { $this->couponValidityInformation = [ new CouponValidityInformation( 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ) ]; $this->couponLegalInformation = [ new CouponLegalInformation( 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ) ]; $this->processingCode = new ProcessingCode('5425003944130'); @@ -424,8 +424,8 @@ describe('coupon', function (): void { ), new CouponDiffusion([new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))]), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], - [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', new CountryAuthorized(Country::FRANCE))], + [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], + [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], new ProcessingCode('5425003944130'), new BrandCode('AAA01'), ActorId::generate(), diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php index 4608cca..00697fb 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php @@ -4,13 +4,12 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\Campaign; -use Domain\Enums\Country; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\WarningThresholdQuantity; +use Shared\Enums\Country; covers(CampaignCoupon::class); @@ -19,7 +18,7 @@ describe('campaign coupon', function (): void { // GIVEN $processingCode = new ProcessingCode('PC1'); $quantity = new CouponQuantity(10); - $availableIn = new CountryAuthorized(Country::FRANCE); + $availableIn = Country::FRANCE; $warningThresholdQuantity = new WarningThresholdQuantity(100); $printRules = new PrintRules(maxPerCustomer: 3); @@ -38,7 +37,7 @@ describe('campaign coupon', function (): void { // GIVEN $processingCode = new ProcessingCode('PC1'); $quantity = new CouponQuantity(10); - $availableIn = new CountryAuthorized(Country::FRANCE); + $availableIn = Country::FRANCE; $printRules = new PrintRules(maxPerCustomer: 3); // WHEN @@ -60,7 +59,7 @@ describe('campaign coupon', function (): void { // GIVEN $processingCode = new ProcessingCode('PC1'); $quantity = new CouponQuantity(10); - $availableIn = new CountryAuthorized(Country::FRANCE); + $availableIn = Country::FRANCE; $warningThresholdQuantity = new WarningThresholdQuantity(100); // WHEN @@ -82,7 +81,7 @@ describe('campaign coupon', function (): void { // GIVEN $processingCode = new ProcessingCode('PC1'); $quantity = new CouponQuantity(10); - $availableIn = new CountryAuthorized(Country::FRANCE); + $availableIn = Country::FRANCE; // WHEN $campaignCoupon = new CampaignCoupon(processingCode: $processingCode, quantity: $quantity, availableIn: $availableIn); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php index 3a43b39..46eedd2 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php @@ -4,9 +4,8 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\Coupon; -use Domain\Enums\Country; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; +use Shared\Enums\Country; covers(CouponValidityInformation::class); @@ -14,7 +13,7 @@ describe('coupon validity information', function (): void { it('creates a valid coupon validity information', function (): void { // GIVEN $validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; - $forLocale = new CountryAuthorized(Country::FRANCE); + $forLocale = Country::FRANCE; // WHEN $couponValidityInformation = new CouponValidityInformation($validityText, $forLocale); @@ -27,7 +26,7 @@ describe('coupon validity information', function (): void { it('rejects invalid coupon validity information', function (): void { // GIVEN $validityText = ''; - $forLocale = new CountryAuthorized(Country::FRANCE); + $forLocale = Country::FRANCE; // GIVEN / THEN expect(fn (): CouponValidityInformation => new CouponValidityInformation($validityText, $forLocale))->toThrow(\DomainException::class); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index 267a401..325ab50 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -7,8 +7,6 @@ namespace Tests\Unit\Domain\Models\ValueObjects\RequestedCoupon; use Carbon\CarbonImmutable; use DateTime; use Domain\Enums\BarcodeFormat; -use Domain\Enums\Country; -use Domain\Models\ValueObjects\CountryAuthorized; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; @@ -23,6 +21,7 @@ use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Models\ValueObjects\WarningThresholdQuantity; +use Shared\Enums\Country; use Shared\Enums\Currency; use Shared\ValueObjects\Money; @@ -44,7 +43,7 @@ describe('requested coupon', function (): void { $this->couponValidityInformation = [ new CouponValidityInformation( 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - new CountryAuthorized(Country::FRANCE) + Country::FRANCE ), ]; $this->products = [new IncoCode(35784), new IncoCode(15522)]; @@ -239,23 +238,4 @@ describe('requested coupon', function (): void { $this->warningThresholdQuantity ))->toThrow(\DomainException::class, 'Coupon validity information must be CouponValidityInformation instances.'); }); - - it('hydrates a requested coupon without a GS1', function (): void { - // WHEN - $coupon = new RequestedCoupon( - new CouponName('Coupon A'), - new CouponProductName('Product A'), - new RequestedCouponQuantity(100), - new WarningThresholdQuantity(10), - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC1'), - new CouponPicture(new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg')), - null, - new CouponValidity(30, new DateTime()), - ); - - // THEN - expect($coupon->gs1)->toBeNull(); - }); }); From 7136ce9ee64d2234ba8439ebd086d1d04f45ac64 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Thu, 1 Oct 2026 12:04:26 +0200 Subject: [PATCH 46/52] refactor: :recycle: centralize test variables in beforeEach --- .../Unit/Domain/Models/Entities/ActorTest.php | 23 +- .../Unit/Domain/Models/Entities/BrandTest.php | 174 +++++++------ .../Models/Entities/CampaignRequestTest.php | 151 ++++++------ .../Domain/Models/Entities/CampaignTest.php | 232 +++++++----------- .../Domain/Models/Entities/CouponTest.php | 32 ++- .../Unit/Domain/Models/Entities/StockTest.php | 87 +++---- tests/Unit/Domain/Models/Entities/TagTest.php | 32 ++- .../Actor/ActorDisplayNameTest.php | 25 +- .../ValueObjects/Brand/BrandNameTest.php | 25 +- .../Campaign/CampaignCodeTest.php | 23 +- .../Campaign/CampaignCouponTest.php | 102 ++++---- .../Campaign/CampaignNameTest.php | 23 +- .../CampaignRequest/ApplicantTest.php | 61 +++-- .../CampaignRequestFiltersTest.php | 42 ++-- .../CampaignRequestSortCriteriaTest.php | 54 ++-- .../CampaignRequest/RejectionReasonTest.php | 13 +- .../Coupon/CouponAvailabilityCalendarTest.php | 58 ++--- .../Coupon/CouponLegalInformationTest.php | 32 +-- .../Coupon/CouponQuantityTest.php | 25 +- .../Coupon/CouponValidityInformationTest.php | 32 +-- .../Coupon/CouponValidityTest.php | 32 +-- .../Models/ValueObjects/CouponNameTest.php | 25 +- .../Domain/Models/ValueObjects/GS1Test.php | 30 ++- .../Models/ValueObjects/IncoCodeTest.php | 25 +- .../Models/ValueObjects/MoneyAmountTest.php | 36 ++- .../Models/ValueObjects/PrintRulesTest.php | 67 +++-- .../ValueObjects/ProcessingCodeTest.php | 25 +- .../RequestedCoupon/PeriodTest.php | 20 +- .../RequestedCouponQuantityTest.php | 29 ++- .../Stock/StockAdjustmentTest.php | 47 +++- .../ValueObjects/Stock/StockAmountTest.php | 31 ++- .../Models/ValueObjects/Tag/TagNameTest.php | 25 +- .../WarningThresholdQuantityTest.php | 33 ++- 33 files changed, 902 insertions(+), 769 deletions(-) diff --git a/tests/Unit/Domain/Models/Entities/ActorTest.php b/tests/Unit/Domain/Models/Entities/ActorTest.php index 3d81e58..cda40c1 100644 --- a/tests/Unit/Domain/Models/Entities/ActorTest.php +++ b/tests/Unit/Domain/Models/Entities/ActorTest.php @@ -12,17 +12,23 @@ use Ramsey\Uuid\Uuid; covers(Actor::class); describe('actor', function (): void { + beforeEach(function (): void { + $this->displayName = new ActorDisplayName('John Doe'); + }); + it('hydrates an actor', function (): void { // GIVEN $id = ActorId::generate(); - $displayName = new ActorDisplayName('John Doe'); // WHEN - $actor = Actor::hydrate($id, $displayName); + $actor = Actor::hydrate( + $id, + $this->displayName + ); // THEN expect($actor->id())->toBe($id) - ->and($actor->displayName)->toBe($displayName) + ->and($actor->displayName)->toBe($this->displayName) ->and($actor->updatedAt)->toBeNull() ->and($actor->updatedAt())->toBeNull(); }); @@ -32,7 +38,11 @@ describe('actor', function (): void { $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); // WHEN - $actor = Actor::hydrate(ActorId::generate(), new ActorDisplayName('John Doe'), $updatedAt); + $actor = Actor::hydrate( + ActorId::generate(), + $this->displayName, + $updatedAt + ); // THEN expect($actor->updatedAt)->toBe($updatedAt) @@ -43,7 +53,10 @@ describe('actor', function (): void { // GIVEN $expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC')); $id = new ActorId(Uuid::uuid7($expected)); - $actor = Actor::hydrate($id, new ActorDisplayName('John Doe')); + $actor = Actor::hydrate( + $id, + $this->displayName + ); // WHEN $result = $actor->createdAt(); diff --git a/tests/Unit/Domain/Models/Entities/BrandTest.php b/tests/Unit/Domain/Models/Entities/BrandTest.php index efcbf99..1e3737b 100644 --- a/tests/Unit/Domain/Models/Entities/BrandTest.php +++ b/tests/Unit/Domain/Models/Entities/BrandTest.php @@ -17,29 +17,39 @@ use Shared\ValueObjects\Rcs; covers(Brand::class); describe('brand', function (): void { + beforeEach(function (): void { + $this->code = new BrandCode('AAA01'); + $this->name = new BrandName('Name'); + $this->logo = new PicturePath('files/picture.jpg'); + $this->rcs = new Rcs('RCS PARIS 111 222 333'); + $this->contact = new Email('contact@example.com'); + }); + afterEach(function (): void { CarbonImmutable::setTestNow(); }); it('creates a brand', function (): void { // GIVEN - $id = BrandId::generate(); - $code = new BrandCode('AAA01'); - $name = new BrandName('Name'); - $logo = new PicturePath('files/picture.jpg'); - $rcs = new Rcs('RCS PARIS 111 222 333'); - $contact = new Email('contact@example.com'); + $id = BrandId::generate(); // WHEN - $brand = Brand::hydrate($id, $code, $name, $logo, $rcs, $contact); + $brand = Brand::hydrate( + $id, + $this->code, + $this->name, + $this->logo, + $this->rcs, + $this->contact + ); // THEN expect($brand->id())->toBe($id) - ->and($brand->code)->toBe($code) - ->and($brand->name)->toBe($name) - ->and($brand->logo)->toBe($logo) - ->and($brand->rcs)->toBe($rcs) - ->and($brand->contact)->toBe($contact) + ->and($brand->code)->toBe($this->code) + ->and($brand->name)->toBe($this->name) + ->and($brand->logo)->toBe($this->logo) + ->and($brand->rcs)->toBe($this->rcs) + ->and($brand->contact)->toBe($this->contact) ->and($brand->updatedAt)->toBeNull() ->and($brand->updatedAt())->toBeNull(); }); @@ -47,15 +57,18 @@ describe('brand', function (): void { it('creates a brand with an explicit updatedAt date', function (): void { // GIVEN $id = BrandId::generate(); - $code = new BrandCode('AAA01'); - $name = new BrandName('Name'); - $logo = new PicturePath('files/picture.jpg'); - $rcs = new Rcs('RCS PARIS 111 222 333'); - $contact = new Email('contact@example.com'); $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); // WHEN - $brand = Brand::hydrate($id, $code, $name, $logo, $rcs, $contact, $updatedAt); + $brand = Brand::hydrate( + $id, + $this->code, + $this->name, + $this->logo, + $this->rcs, + $this->contact, + $updatedAt + ); // THEN expect($brand->updatedAt)->toBe($updatedAt) @@ -63,38 +76,42 @@ describe('brand', function (): void { }); it('creates a new brand with a freshly generated id', function (): void { - // GIVEN - $code = new BrandCode('AAA01'); - $name = new BrandName('Name'); - $logo = new PicturePath('files/picture.jpg'); - $rcs = new Rcs('RCS PARIS 111 222 333'); - $contact = new Email('contact@example.com'); - - // WHEN - $brand = Brand::create($code, $name, $logo, $rcs, $contact); + // GIVEN / WHEN + $brand = Brand::create( + $this->code, + $this->name, + $this->logo, + $this->rcs, + $this->contact + ); // THEN expect($brand->id())->toBeInstanceOf(BrandId::class) - ->and($brand->code)->toBe($code) - ->and($brand->name)->toBe($name) - ->and($brand->logo)->toBe($logo) - ->and($brand->rcs)->toBe($rcs) - ->and($brand->contact)->toBe($contact) + ->and($brand->code)->toBe($this->code) + ->and($brand->name)->toBe($this->name) + ->and($brand->logo)->toBe($this->logo) + ->and($brand->rcs)->toBe($this->rcs) + ->and($brand->contact)->toBe($this->contact) ->and($brand->updatedAt)->toBeNull() ->and($brand->updatedAt())->toBeNull(); }); it('generates a different id for each created brand', function (): void { - // GIVEN - $code = new BrandCode('AAA01'); - $name = new BrandName('Name'); - $logo = new PicturePath('files/picture.jpg'); - $rcs = new Rcs('RCS PARIS 111 222 333'); - $contact = new Email('contact@example.com'); - - // WHEN - $first = Brand::create($code, $name, $logo, $rcs, $contact); - $second = Brand::create($code, $name, $logo, $rcs, $contact); + // GIVEN / WHEN + $first = Brand::create( + $this->code, + $this->name, + $this->logo, + $this->rcs, + $this->contact + ); + $second = Brand::create( + $this->code, + $this->name, + $this->logo, + $this->rcs, + $this->contact + ); // THEN expect($first->id()->equals($second->id()))->toBeFalse(); @@ -104,24 +121,27 @@ describe('brand', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15 12:00:00'); $id = BrandId::generate(); - $code = new BrandCode('AAA01'); - $name = new BrandName('Name'); $logoOld = new PicturePath('files/picture-old.jpg'); $logoNew = new PicturePath('files/picture-new.jpg'); - $rcs = new Rcs('RCS PARIS 111 222 333'); - $contact = new Email('contact@example.com'); - $brand = Brand::hydrate($id, $code, $name, $logoOld, $rcs, $contact); + $brand = Brand::hydrate( + $id, + $this->code, + $this->name, + $logoOld, + $this->rcs, + $this->contact + ); // WHEN $brand = $brand->changeLogo($logoNew); // THEN expect($brand->id())->toBe($id) - ->and($brand->code)->toBe($code) - ->and($brand->name)->toBe($name) + ->and($brand->code)->toBe($this->code) + ->and($brand->name)->toBe($this->name) ->and($brand->logo)->toBe($logoNew) - ->and($brand->rcs)->toBe($rcs) - ->and($brand->contact)->toBe($contact) + ->and($brand->rcs)->toBe($this->rcs) + ->and($brand->contact)->toBe($this->contact) ->and($brand->updatedAt)->not->toBeNull() ->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00'); }); @@ -130,24 +150,27 @@ describe('brand', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15 14:30:00'); $id = BrandId::generate(); - $code = new BrandCode('AAA01'); - $name = new BrandName('Name'); - $logo = new PicturePath('files/picture.jpg'); $rcsOld = new Rcs('RCS PARIS 111 222 333'); $rcsNew = new Rcs('RCS NANTES 444 555 666'); - $contact = new Email('contact@example.com'); - $brand = Brand::hydrate($id, $code, $name, $logo, $rcsOld, $contact); + $brand = Brand::hydrate( + $id, + $this->code, + $this->name, + $this->logo, + $rcsOld, + $this->contact + ); // WHEN $brand = $brand->changeRcs($rcsNew); // THEN expect($brand->id())->toBe($id) - ->and($brand->code)->toBe($code) - ->and($brand->name)->toBe($name) - ->and($brand->logo)->toBe($logo) + ->and($brand->code)->toBe($this->code) + ->and($brand->name)->toBe($this->name) + ->and($brand->logo)->toBe($this->logo) ->and($brand->rcs)->toBe($rcsNew) - ->and($brand->contact)->toBe($contact) + ->and($brand->contact)->toBe($this->contact) ->and($brand->updatedAt)->not->toBeNull() ->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 14:30:00'); }); @@ -156,23 +179,26 @@ describe('brand', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15 16:45:00'); $id = BrandId::generate(); - $code = new BrandCode('AAA01'); - $name = new BrandName('Name'); - $logo = new PicturePath('files/picture.jpg'); - $rcs = new Rcs('RCS PARIS 111 222 333'); $contactOld = new Email('contact-old@example.com'); $contactNew = new Email('contact-new@example.com'); - $brand = Brand::hydrate($id, $code, $name, $logo, $rcs, $contactOld); + $brand = Brand::hydrate( + $id, + $this->code, + $this->name, + $this->logo, + $this->rcs, + $contactOld + ); // WHEN $brand = $brand->changeContact($contactNew); // THEN expect($brand->id())->toBe($id) - ->and($brand->code)->toBe($code) - ->and($brand->name)->toBe($name) - ->and($brand->logo)->toBe($logo) - ->and($brand->rcs)->toBe($rcs) + ->and($brand->code)->toBe($this->code) + ->and($brand->name)->toBe($this->name) + ->and($brand->logo)->toBe($this->logo) + ->and($brand->rcs)->toBe($this->rcs) ->and($brand->contact)->toBe($contactNew) ->and($brand->updatedAt)->not->toBeNull() ->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 16:45:00'); @@ -184,11 +210,11 @@ describe('brand', function (): void { $id = new BrandId(Uuid::uuid7($expected)); $brand = Brand::hydrate( $id, - new BrandCode('AAA01'), - new BrandName('Name'), - new PicturePath('files/picture.jpg'), - new Rcs('RCS PARIS 111 222 333'), - new Email('contact@example.com'), + $this->code, + $this->name, + $this->logo, + $this->rcs, + $this->contact ); // WHEN diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 8b9e532..1c2edbe 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -38,12 +38,8 @@ use Shared\ValueObjects\Money; covers(CampaignRequest::class); describe('campaign request', function (): void { - afterEach(function (): void { - CarbonImmutable::setTestNow(); - }); - beforeEach(function (): void { - $this->requestedCoupon = new RequestedCoupon( + $this->requestedCoupon = new RequestedCoupon( new CouponName('Coupon A'), new RequestedCouponQuantity(100), new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), @@ -61,37 +57,44 @@ describe('campaign request', function (): void { new GS1('01234567890128', BarcodeFormat::EAN13), new WarningThresholdQuantity(10) ); + $this->code = new BrandCode('AAA01'); + $this->name = new CampaignName('Campaign A'); + $this->applicant = new Applicant('John', 'Doe', new Email('john.doe@example.com')); + $this->campaignType = CampaignRequestType::CLUB; + $this->requestedCoupon = $this->requestedCoupon; + $this->status = CampaignRequestStatus::CREATED; + $this->transformedStatus = CampaignRequestStatus::TRANSFORMED; + $this->rejectedStatus = CampaignRequestStatus::REJECTED; + $this->rejectionReason = new RejectionReason('Missing legal documents.'); + }); + + afterEach(function (): void { + CarbonImmutable::setTestNow(); }); it('hydrates a campaign request with all its fields', function (): void { // GIVEN $id = CampaignRequestId::generate(); - $code = new BrandCode('AAA01'); - $name = new CampaignName('Campaign A'); - $applicant = new Applicant('John', 'Doe', new Email('john.doe@example.com')); - $campaignType = CampaignRequestType::CLUB; - $requestedCoupon = $this->requestedCoupon; - $status = CampaignRequestStatus::CREATED; // WHEN $campaignRequest = CampaignRequest::hydrate( $id, - $code, - $name, - $applicant, - $campaignType, - $requestedCoupon, - $status, + $this->code, + $this->name, + $this->applicant, + $this->campaignType, + $this->requestedCoupon, + $this->status, ); // THEN expect($campaignRequest->id())->toBe($id) - ->and($campaignRequest->brandCode)->toBe($code) - ->and($campaignRequest->name)->toBe($name) - ->and($campaignRequest->applicant)->toBe($applicant) - ->and($campaignRequest->campaignType)->toBe($campaignType) - ->and($campaignRequest->requestedCoupon)->toBe($requestedCoupon) - ->and($campaignRequest->status)->toBe($status) + ->and($campaignRequest->brandCode)->toBe($this->code) + ->and($campaignRequest->name)->toBe($this->name) + ->and($campaignRequest->applicant)->toBe($this->applicant) + ->and($campaignRequest->campaignType)->toBe($this->campaignType) + ->and($campaignRequest->requestedCoupon)->toBe($this->requestedCoupon) + ->and($campaignRequest->status)->toBe($this->status) ->and($campaignRequest->updatedAt)->toBeNull() ->and($campaignRequest->updatedAt())->toBeNull(); }); @@ -103,12 +106,12 @@ describe('campaign request', function (): void { // WHEN $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), - new BrandCode('AAA01'), - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, + $this->code, + $this->name, + $this->applicant, + $this->campaignType, $this->requestedCoupon, - CampaignRequestStatus::CREATED, + $this->status, null, $updatedAt, ); @@ -119,36 +122,33 @@ describe('campaign request', function (): void { }); it('hydrates a rejected campaign request with a rejection reason', function (): void { - // GIVEN - $rejectionReason = new RejectionReason('Missing legal documents.'); - - // WHEN + // GIVEN / WHEN $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), - new BrandCode('AAA01'), - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, + $this->code, + $this->name, + $this->applicant, + $this->campaignType, $this->requestedCoupon, - CampaignRequestStatus::REJECTED, - $rejectionReason, + $this->rejectedStatus, + $this->rejectionReason, ); // THEN - expect($campaignRequest->status)->toBe(CampaignRequestStatus::REJECTED) - ->and($campaignRequest->rejectionReason)->toBe($rejectionReason); + expect($campaignRequest->status)->toBe($this->rejectedStatus) + ->and($campaignRequest->rejectionReason)->toBe($this->rejectionReason); }); it('rejects a rejected campaign request without a rejection reason', function (): void { - // WHEN + // GIVEN / WHEN $action = fn (): CampaignRequest => CampaignRequest::hydrate( CampaignRequestId::generate(), - new BrandCode('AAA01'), - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, + $this->code, + $this->name, + $this->applicant, + $this->campaignType, $this->requestedCoupon, - CampaignRequestStatus::REJECTED, + $this->rejectedStatus, ); // THEN @@ -162,13 +162,13 @@ describe('campaign request', function (): void { // WHEN $action = fn (): CampaignRequest => CampaignRequest::hydrate( CampaignRequestId::generate(), - new BrandCode('AAA01'), - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, + $this->code, + $this->name, + $this->applicant, + $this->campaignType, $this->requestedCoupon, - CampaignRequestStatus::CREATED, - new RejectionReason('Missing legal documents.'), + $this->status, + $this->rejectionReason, ); // THEN @@ -183,21 +183,20 @@ describe('campaign request', function (): void { CarbonImmutable::setTestNow('2026-06-15 12:00:00'); $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), - new BrandCode('AAA01'), - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, + $this->code, + $this->name, + $this->applicant, + $this->campaignType, $this->requestedCoupon, - CampaignRequestStatus::CREATED, + $this->status, ); - $reason = new RejectionReason('Missing legal documents.'); // WHEN - $rejected = $campaignRequest->reject($reason); + $rejected = $campaignRequest->reject($this->rejectionReason); // THEN expect($rejected->status)->toBe(CampaignRequestStatus::REJECTED) - ->and($rejected->rejectionReason)->toBe($reason) + ->and($rejected->rejectionReason)->toBe($this->rejectionReason) ->and($rejected->id())->toBe($campaignRequest->id()) ->and($rejected->brandCode)->toBe($campaignRequest->brandCode) ->and($rejected->name)->toBe($campaignRequest->name) @@ -212,16 +211,16 @@ describe('campaign request', function (): void { // GIVEN $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), - new BrandCode('AAA01'), - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, + $this->code, + $this->name, + $this->applicant, + $this->campaignType, $this->requestedCoupon, - CampaignRequestStatus::TRANSFORMED, + $this->transformedStatus, ); // WHEN - $action = fn (): CampaignRequest => $campaignRequest->reject(new RejectionReason('Missing legal documents.')); + $action = fn (): CampaignRequest => $campaignRequest->reject($this->rejectionReason); // THEN expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.'); @@ -231,12 +230,12 @@ describe('campaign request', function (): void { // GIVEN $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), - new BrandCode('AAA01'), - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, + $this->code, + $this->name, + $this->applicant, + $this->campaignType, $this->requestedCoupon, - CampaignRequestStatus::REJECTED, + $this->rejectedStatus, new RejectionReason('First reason.'), ); @@ -253,12 +252,12 @@ describe('campaign request', function (): void { $id = new CampaignRequestId(Uuid::uuid7($expected)); $campaignRequest = CampaignRequest::hydrate( $id, - new BrandCode('AAA01'), - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, + $this->code, + $this->name, + $this->applicant, + $this->campaignType, $this->requestedCoupon, - CampaignRequestStatus::CREATED, + $this->status, ); // WHEN diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index 4952579..35eb58a 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -21,18 +21,12 @@ use Ramsey\Uuid\Uuid; covers(Campaign::class); describe('campaign', function (): void { - afterEach(function (): void { - CarbonImmutable::setTestNow(); - }); - - it('creates a campaign', function (): void { - // GIVEN - $id = CampaignId::generate(); - $code = new CampaignCode('Camp100'); - $name = new CampaignName('Name'); - $publication = \boolval(0); - $printAllAtOnce = \boolval(1); - $coupons = [ + beforeEach(function (): void { + $this->code = new CampaignCode('Camp100'); + $this->name = new CampaignName('Name'); + $this->publication = \boolval(0); + $this->printAllAtOnce = \boolval(1); + $this->coupons = [ new CampaignCoupon( new ProcessingCode('PC1'), new CouponQuantity(100), @@ -41,50 +35,53 @@ describe('campaign', function (): void { new PrintRules(maxPerCustomer: 3) ) ]; - $brandCode = new BrandCode('AAA01'); + $this->brandCode = new BrandCode('AAA01'); + }); + + afterEach(function (): void { + CarbonImmutable::setTestNow(); + }); + + it('creates a campaign', function (): void { + // GIVEN + $id = CampaignId::generate(); // WHEN $campaign = Campaign::hydrate( $id, - $code, - $name, - $publication, - $printAllAtOnce, - $coupons, - $brandCode, + $this->code, + $this->name, + $this->publication, + $this->printAllAtOnce, + $this->coupons, + $this->brandCode, ); // THEN expect($campaign->id())->toBe($id) - ->and($campaign->code)->toBe($code) - ->and($campaign->name)->toBe($name) - ->and($campaign->publication)->toBe($publication) - ->and($campaign->printAllAtOnce)->toBe($printAllAtOnce) - ->and($campaign->coupons)->toBe($coupons) - ->and($campaign->brandCode)->toBe($brandCode) + ->and($campaign->code)->toBe($this->code) + ->and($campaign->name)->toBe($this->name) + ->and($campaign->publication)->toBe($this->publication) + ->and($campaign->printAllAtOnce)->toBe($this->printAllAtOnce) + ->and($campaign->coupons)->toBe($this->coupons) + ->and($campaign->brandCode)->toBe($this->brandCode) ->and($campaign->updatedAt)->toBeNull() ->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); - $coupons = []; - $brandCode = new BrandCode('AAA01'); + $id = CampaignId::generate(); // WHEN $campaign = Campaign::hydrate( $id, - $code, - $name, - $publication, - $printAllAtOnce, - $coupons, - $brandCode, + $this->code, + $this->name, + $this->publication, + $this->printAllAtOnce, + [], + $this->brandCode, ); // THEN @@ -93,32 +90,18 @@ describe('campaign', function (): void { it('creates a coupon with an explicit updatedAt date', function (): void { // GIVEN - $id = CampaignId::generate(); - $code = new CampaignCode('Camp100'); - $name = new CampaignName('Name'); - $publication = \boolval(0); - $printAllAtOnce = \boolval(1); - $coupons = [ - new CampaignCoupon( - new ProcessingCode('PC1'), - new CouponQuantity(100), - Country::FRANCE, - new WarningThresholdQuantity(100), - new PrintRules(maxPerCustomer: 3) - ) - ]; - $brandCode = new BrandCode('AAA01'); - $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); + $id = CampaignId::generate(); + $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); // WHEN $campaign = Campaign::hydrate( $id, - $code, - $name, - $publication, - $printAllAtOnce, - $coupons, - $brandCode, + $this->code, + $this->name, + $this->publication, + $this->printAllAtOnce, + $this->coupons, + $this->brandCode, $updatedAt ); @@ -128,77 +111,45 @@ describe('campaign', function (): void { }); it('creates a new campaign with a freshly generated id', function (): void { - // GIVEN - $code = new CampaignCode('Camp100'); - $name = new CampaignName('Name'); - $publication = \boolval(0); - $printAllAtOnce = \boolval(1); - $coupons = [ - new CampaignCoupon( - new ProcessingCode('PC1'), - new CouponQuantity(100), - Country::FRANCE, - new WarningThresholdQuantity(100), - new PrintRules(maxPerCustomer: 3) - ) - ]; - $brandCode = new BrandCode('AAA01'); - - // WHEN + // GIVEN / WHEN $campaign = Campaign::create( - $code, - $name, - $publication, - $printAllAtOnce, - $coupons, - $brandCode + $this->code, + $this->name, + $this->publication, + $this->printAllAtOnce, + $this->coupons, + $this->brandCode ); // THEN expect($campaign->id())->toBeInstanceOf(CampaignId::class) - ->and($campaign->code)->toBe($code) - ->and($campaign->name)->toBe($name) - ->and($campaign->publication)->toBe($publication) - ->and($campaign->printAllAtOnce)->toBe($printAllAtOnce) - ->and($campaign->coupons)->toBe($coupons) - ->and($campaign->brandCode)->toBe($brandCode) + ->and($campaign->code)->toBe($this->code) + ->and($campaign->name)->toBe($this->name) + ->and($campaign->publication)->toBe($this->publication) + ->and($campaign->printAllAtOnce)->toBe($this->printAllAtOnce) + ->and($campaign->coupons)->toBe($this->coupons) + ->and($campaign->brandCode)->toBe($this->brandCode) ->and($campaign->updatedAt)->toBeNull() ->and($campaign->updatedAt())->toBeNull(); }); it('generates a different id for each created campaign', function (): void { - // GIVEN - $code = new CampaignCode('Camp100'); - $name = new CampaignName('Name'); - $publication = \boolval(0); - $printAllAtOnce = \boolval(1); - $coupons = [ - new CampaignCoupon( - new ProcessingCode('PC1'), - new CouponQuantity(100), - Country::FRANCE, - new WarningThresholdQuantity(100), - new PrintRules(maxPerCustomer: 3) - ) - ]; - $brandCode = new BrandCode('AAA01'); - - // WHEN + // GIVEN / WHEN $firstCampaign = Campaign::create( - $code, - $name, - $publication, - $printAllAtOnce, - $coupons, - $brandCode + $this->code, + $this->name, + $this->publication, + $this->printAllAtOnce, + $this->coupons, + $this->brandCode ); $secondCampaign = Campaign::create( - $code, - $name, - $publication, - $printAllAtOnce, - $coupons, - $brandCode + $this->code, + $this->name, + $this->publication, + $this->printAllAtOnce, + $this->coupons, + $this->brandCode ); // THEN @@ -207,24 +158,16 @@ describe('campaign', function (): void { 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 CampaignId(Uuid::uuid7($expected)); - $campaign = Campaign::hydrate( + $expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC')); + $id = new CampaignId(Uuid::uuid7($expected)); + $campaign = Campaign::hydrate( $id, - new CampaignCode('Camp100'), - new CampaignName('Name'), - \boolval(0), - \boolval(1), - [ - new CampaignCoupon( - new ProcessingCode('PC1'), - new CouponQuantity(100), - Country::FRANCE, - new WarningThresholdQuantity(100), - new PrintRules(maxPerCustomer: 3) - ) - ], - new BrandCode('AAA01') + $this->code, + $this->name, + $this->publication, + $this->printAllAtOnce, + $this->coupons, + $this->brandCode ); // WHEN @@ -236,23 +179,18 @@ describe('campaign', function (): void { 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); - $coupons = ['PC1']; - $brandCode = new BrandCode('AAA01'); + $id = CampaignId::generate(); + $coupons = ['PC1']; // WHEN / THEN expect(fn (): Campaign => Campaign::hydrate( $id, - $code, - $name, - $publication, - $printAllAtOnce, + $this->code, + $this->name, + $this->publication, + $this->printAllAtOnce, $coupons, - $brandCode + $this->brandCode ))->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 623ebc4..2e03c7d 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -414,24 +414,20 @@ describe('coupon', function (): void { $id = new CouponId(Uuid::uuid7($expected)); $coupon = Coupon::hydrate( $id, - new CouponCode('COU01'), - new CouponName('Name'), - new MoneyAmount(new Money(Currency::EUR, 1)), - new CouponPicture( - new PicturePath('files/product.jpg'), - new PicturePath('files/thumbnail.jpg'), - new PicturePath('files/inactive_thumbnail.jpg') - ), - new CouponDiffusion([new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))]), - new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], - [new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], - new ProcessingCode('5425003944130'), - new BrandCode('AAA01'), - ActorId::generate(), - [new IncoCode(35784), new IncoCode(15522)], - new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]), - new GS1('110101010', BarcodeFormat::EAN13), + $this->code, + $this->name, + $this->amount, + $this->picture, + $this->diffusion, + $this->validity, + $this->couponValidityInformation, + $this->couponLegalInformation, + $this->processingCode, + $this->brandCode, + $this->createdBy, + $this->products, + $this->availabilityCalendar, + $this->gtin, ); // WHEN diff --git a/tests/Unit/Domain/Models/Entities/StockTest.php b/tests/Unit/Domain/Models/Entities/StockTest.php index 15da8fc..a30b6d7 100644 --- a/tests/Unit/Domain/Models/Entities/StockTest.php +++ b/tests/Unit/Domain/Models/Entities/StockTest.php @@ -16,23 +16,32 @@ use Ramsey\Uuid\Uuid; covers(Stock::class); describe('stock', function (): void { + beforeEach(function (): void { + $this->stockAmount = new StockAmount(10); + $this->campaignCode = new CampaignCode('AAA01'); + $this->processingCode = new ProcessingCode('PC1'); + $this->stockAdjustment = [new StockAdjustment(5, 'Inventory correction', ActorId::generate())]; + }); + it('hydrates a stock', 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())]; + $id = StockId::generate(); // WHEN - $stock = Stock::hydrate($id, $stockAmount, $campaignCode, $processingCode, $stockAdjustment); + $stock = Stock::hydrate( + $id, + $this->stockAmount, + $this->campaignCode, + $this->processingCode, + $this->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->stockAmount)->toBe($this->stockAmount) + ->and($stock->campaignCode)->toBe($this->campaignCode) + ->and($stock->processingCode)->toBe($this->processingCode) + ->and($stock->stockAdjustment)->toBe($this->stockAdjustment) ->and($stock->updatedAt)->toBeNull() ->and($stock->updatedAt())->toBeNull(); }); @@ -40,9 +49,6 @@ describe('stock', function (): void { 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()), @@ -51,17 +57,17 @@ describe('stock', function (): void { // WHEN $stock = Stock::hydrate( $id, - $stockAmount, - $campaignCode, - $processingCode, + $this->stockAmount, + $this->campaignCode, + $this->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->stockAmount)->toBe($this->stockAmount) + ->and($stock->campaignCode)->toBe($this->campaignCode) + ->and($stock->processingCode)->toBe($this->processingCode) ->and($stock->stockAdjustment)->toBe($stockAdjustment) ->and($stock->updatedAt)->toBeNull() ->and($stock->updatedAt())->toBeNull(); @@ -69,19 +75,21 @@ describe('stock', function (): void { 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'); + $id = StockId::generate(); // WHEN - $stock = Stock::hydrate($id, $stockAmount, $campaignCode, $processingCode); + $stock = Stock::hydrate( + $id, + $this->stockAmount, + $this->campaignCode, + $this->processingCode + ); // THEN expect($stock->id())->toBe($id) - ->and($stock->stockAmount)->toBe($stockAmount) - ->and($stock->campaignCode)->toBe($campaignCode) - ->and($stock->processingCode)->toBe($processingCode) + ->and($stock->stockAmount)->toBe($this->stockAmount) + ->and($stock->campaignCode)->toBe($this->campaignCode) + ->and($stock->processingCode)->toBe($this->processingCode) ->and($stock->stockAdjustment)->toBe([]) ->and($stock->updatedAt)->toBeNull() ->and($stock->updatedAt())->toBeNull(); @@ -90,17 +98,14 @@ describe('stock', function (): void { 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, + $this->stockAmount, + $this->campaignCode, + $this->processingCode, $stockAdjustment, ))->toThrow(\DomainException::class, 'Stock adjustment must be StockAdjustment instances.'); }); @@ -112,10 +117,10 @@ describe('stock', function (): void { // WHEN $stock = Stock::hydrate( StockId::generate(), - new StockAmount(10), - new CampaignCode('AAA01'), - new ProcessingCode('PC1'), - [new StockAdjustment(5, 'Inventory correction', ActorId::generate())], + $this->stockAmount, + $this->campaignCode, + $this->processingCode, + $this->stockAdjustment, $updatedAt, ); @@ -130,10 +135,10 @@ describe('stock', function (): void { $id = new StockId(Uuid::uuid7($expected)); $stock = Stock::hydrate( $id, - new StockAmount(10), - new CampaignCode('AAA01'), - new ProcessingCode('PC1'), - [new StockAdjustment(5, 'Inventory correction', ActorId::generate())], + $this->stockAmount, + $this->campaignCode, + $this->processingCode, + $this->stockAdjustment ); // WHEN diff --git a/tests/Unit/Domain/Models/Entities/TagTest.php b/tests/Unit/Domain/Models/Entities/TagTest.php index 1ade199..b7c3d71 100644 --- a/tests/Unit/Domain/Models/Entities/TagTest.php +++ b/tests/Unit/Domain/Models/Entities/TagTest.php @@ -13,6 +13,10 @@ use Ramsey\Uuid\Uuid; covers(Tag::class); describe('tag', function (): void { + beforeEach(function (): void { + $this->value = new TagName('Name'); + }); + afterEach(function (): void { CarbonImmutable::setTestNow(); }); @@ -20,17 +24,16 @@ describe('tag', function (): void { it('creates a tag', function (): void { // GIVEN $id = TagId::generate(); - $value = new TagName('Name'); // WHEN $tag = Tag::hydrate( $id, - $value, + $this->value, ); // THEN expect($tag->id())->toBe($id) - ->and($tag->value)->toBe($value) + ->and($tag->value)->toBe($this->value) ->and($tag->updatedAt)->toBeNull() ->and($tag->updatedAt())->toBeNull(); }); @@ -38,13 +41,12 @@ describe('tag', function (): void { it('creates a tag with an explicit updatedAt date', function (): void { // GIVEN $id = TagId::generate(); - $name = new TagName('Name'); $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); // WHEN $tag = Tag::hydrate( $id, - $name, + $this->value, $updatedAt ); @@ -54,31 +56,25 @@ describe('tag', function (): void { }); it('creates a new tag with a freshly generated id', function (): void { - // GIVEN - $value = new TagName('Name'); - - // WHEN + // GIVEN / WHEN $tag = Tag::create( - $value, + $this->value, ); // THEN expect($tag->id())->toBeInstanceOf(TagId::class) - ->and($tag->value)->toBe($value) + ->and($tag->value)->toBe($this->value) ->and($tag->updatedAt)->toBeNull() ->and($tag->updatedAt())->toBeNull(); }); it('generates a different id for each created tag', function (): void { - // GIVEN - $value = new TagName('Name'); - - // WHEN + // GIVEN / WHEN $firstTag = Tag::create( - $value + $this->value ); $secondTag = Tag::create( - $value + $this->value ); // THEN @@ -91,7 +87,7 @@ describe('tag', function (): void { $id = new TagId(Uuid::uuid7($expected)); $tag = Tag::hydrate( $id, - new TagName('Name'), + $this->value, ); // WHEN diff --git a/tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php index 8ea9a6f..82b09a3 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php @@ -9,22 +9,25 @@ use Domain\Models\ValueObjects\Actor\ActorDisplayName; covers(ActorDisplayName::class); describe('actor displayname', function (): void { - it('creates a valid actor displayname', function (): void { - // GIVEN - $value = 'Johnny'; + beforeEach(function (): void { + $this->value = 'Johnny'; + $this->emptyValue = ''; + }); - // WHEN - $name = new ActorDisplayName($value); + it('creates a valid actor displayname', function (): void { + // GIVEN / WHEN + $name = new ActorDisplayName( + $this->value + ); // THEN - expect($name->value)->toBe($value); + expect($name->value)->toBe($this->value); }); it('rejects invalid actor displayname', function (): void { - // GIVEN - $value = ''; - - // GIVEN / THEN - expect(fn (): ActorDisplayName => new ActorDisplayName($value))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): ActorDisplayName => new ActorDisplayName( + $this->emptyValue + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Brand/BrandNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Brand/BrandNameTest.php index 2cc6013..41a3762 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Brand/BrandNameTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Brand/BrandNameTest.php @@ -9,23 +9,26 @@ use Domain\Models\ValueObjects\Brand\BrandName; covers(BrandName::class); describe('brand name', function (): void { - it('creates a valid brand name', function (): void { - // GIVEN - $value = 'Name'; + beforeEach(function (): void { + $this->value = 'Name'; + $this->emptyValue = ''; + }); - // WHEN - $name = new BrandName($value); + it('creates a valid brand name', function (): void { + // GIVEN / WHEN + $name = new BrandName( + $this->value + ); // THEN - expect($name->value)->toBe('Name'); + expect($name->value)->toBe($this->value); }); it('rejects invalid brand name', function (): void { - // GIVEN - $value = ''; - - // GIVEN / THEN - expect(fn (): BrandName => new BrandName($value))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): BrandName => new BrandName( + $this->emptyValue + ))->toThrow(\DomainException::class); }); it('rejects a name that exceeds the maximum allowed length', function (): void { diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php index 84b229c..939cefd 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php @@ -9,22 +9,23 @@ use Domain\Models\ValueObjects\Campaign\CampaignCode; covers(CampaignCode::class); describe('campaign code', function (): void { - it('creates a valid campaign code', function (): void { - // GIVEN - $code = 'Name'; + beforeEach(function (): void { + $this->code = 'Name'; + $this->emptyCode = ''; + }); - // WHEN - $name = new CampaignCode($code); + it('creates a valid campaign code', function (): void { + // GIVEN / WHEN + $name = new CampaignCode($this->code); // THEN - expect($name->code)->toBe($code); + expect($name->code)->toBe($this->code); }); it('rejects invalid campaign code', function (): void { - // GIVEN - $code = ''; - - // GIVEN / THEN - expect(fn (): CampaignCode => new CampaignCode($code))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): CampaignCode => new CampaignCode( + $this->emptyCode + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php index 00697fb..be7fc49 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php @@ -14,81 +14,75 @@ use Shared\Enums\Country; covers(CampaignCoupon::class); describe('campaign coupon', function (): void { - it('creates a valid campaign coupon', function (): void { - // GIVEN - $processingCode = new ProcessingCode('PC1'); - $quantity = new CouponQuantity(10); - $availableIn = Country::FRANCE; - $warningThresholdQuantity = new WarningThresholdQuantity(100); - $printRules = new PrintRules(maxPerCustomer: 3); + beforeEach(function (): void { + $this->processingCode = new ProcessingCode('PC1'); + $this->quantity = new CouponQuantity(10); + $this->availableIn = Country::FRANCE; + $this->warningThresholdQuantity = new WarningThresholdQuantity(100); + $this->printRules = new PrintRules(maxPerCustomer: 3); + }); - // WHEN - $campaignCoupon = new CampaignCoupon($processingCode, $quantity, $availableIn, $warningThresholdQuantity, $printRules); + it('creates a valid campaign coupon', function (): void { + // GIVEN / WHEN + $campaignCoupon = new CampaignCoupon( + $this->processingCode, + $this->quantity, + $this->availableIn, + $this->warningThresholdQuantity, + $this->printRules + ); // THEN - expect($campaignCoupon->processingCode)->toBe($processingCode) - ->and($campaignCoupon->quantity)->toBe($quantity) - ->and($campaignCoupon->availableIn)->toBe($availableIn) - ->and($campaignCoupon->warningThresholdQuantity)->toBe($warningThresholdQuantity) - ->and($campaignCoupon->printRules)->toBe($printRules); + expect($campaignCoupon->processingCode)->toBe($this->processingCode) + ->and($campaignCoupon->quantity)->toBe($this->quantity) + ->and($campaignCoupon->availableIn)->toBe($this->availableIn) + ->and($campaignCoupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity) + ->and($campaignCoupon->printRules)->toBe($this->printRules); }); it('creates a valid campaign coupon whitout warning threshold quantity', function (): void { - // GIVEN - $processingCode = new ProcessingCode('PC1'); - $quantity = new CouponQuantity(10); - $availableIn = Country::FRANCE; - $printRules = new PrintRules(maxPerCustomer: 3); - - // WHEN + // GIVEN / WHEN $campaignCoupon = new CampaignCoupon( - processingCode: $processingCode, - quantity: $quantity, - availableIn: $availableIn, - printRules: $printRules + processingCode: $this->processingCode, + quantity: $this->quantity, + availableIn: $this->availableIn, + printRules: $this->printRules ); // THEN - expect($campaignCoupon->processingCode)->toBe($processingCode) - ->and($campaignCoupon->quantity)->toBe($quantity) - ->and($campaignCoupon->availableIn)->toBe($availableIn) - ->and($campaignCoupon->printRules)->toBe($printRules); + expect($campaignCoupon->processingCode)->toBe($this->processingCode) + ->and($campaignCoupon->quantity)->toBe($this->quantity) + ->and($campaignCoupon->availableIn)->toBe($this->availableIn) + ->and($campaignCoupon->printRules)->toBe($this->printRules); }); it('creates a valid campaign coupon whitout print rules', function (): void { - // GIVEN - $processingCode = new ProcessingCode('PC1'); - $quantity = new CouponQuantity(10); - $availableIn = Country::FRANCE; - $warningThresholdQuantity = new WarningThresholdQuantity(100); - - // WHEN + // GIVEN / WHEN $campaignCoupon = new CampaignCoupon( - processingCode: $processingCode, - quantity: $quantity, - availableIn: $availableIn, - warningThresholdQuantity: $warningThresholdQuantity + processingCode: $this->processingCode, + quantity: $this->quantity, + availableIn: $this->availableIn, + warningThresholdQuantity: $this->warningThresholdQuantity ); // THEN - expect($campaignCoupon->processingCode)->toBe($processingCode) - ->and($campaignCoupon->quantity)->toBe($quantity) - ->and($campaignCoupon->availableIn)->toBe($availableIn) - ->and($campaignCoupon->warningThresholdQuantity)->toBe($warningThresholdQuantity); + expect($campaignCoupon->processingCode)->toBe($this->processingCode) + ->and($campaignCoupon->quantity)->toBe($this->quantity) + ->and($campaignCoupon->availableIn)->toBe($this->availableIn) + ->and($campaignCoupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); }); it('creates a valid campaign coupon whitout warning threshold quantity and print rules', function (): void { - // GIVEN - $processingCode = new ProcessingCode('PC1'); - $quantity = new CouponQuantity(10); - $availableIn = Country::FRANCE; - - // WHEN - $campaignCoupon = new CampaignCoupon(processingCode: $processingCode, quantity: $quantity, availableIn: $availableIn); + // GIVEN / WHEN + $campaignCoupon = new CampaignCoupon( + processingCode: $this->processingCode, + quantity: $this->quantity, + availableIn: $this->availableIn + ); // THEN - expect($campaignCoupon->processingCode)->toBe($processingCode) - ->and($campaignCoupon->quantity)->toBe($quantity) - ->and($campaignCoupon->availableIn)->toBe($availableIn); + expect($campaignCoupon->processingCode)->toBe($this->processingCode) + ->and($campaignCoupon->quantity)->toBe($this->quantity) + ->and($campaignCoupon->availableIn)->toBe($this->availableIn); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php index ffbf82f..ad4af96 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php @@ -9,22 +9,23 @@ use Domain\Models\ValueObjects\Campaign\CampaignName; covers(CampaignName::class); describe('campaign name', function (): void { - it('creates a valid campaign name', function (): void { - // GIVEN - $value = 'Name'; + beforeEach(function (): void { + $this->value = 'Name'; + $this->emptyValue = ''; + }); - // WHEN - $name = new CampaignName($value); + it('creates a valid campaign name', function (): void { + // GIVEN / WHEN + $name = new CampaignName($this->value); // THEN - expect($name->value)->toBe($value); + expect($name->value)->toBe($this->value); }); it('rejects invalid campaign name', function (): void { - // GIVEN - $value = ''; - - // GIVEN / THEN - expect(fn (): CampaignName => new CampaignName($value))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): CampaignName => new CampaignName( + $this->emptyValue + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/ApplicantTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/ApplicantTest.php index e46349c..730b5c6 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/ApplicantTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/ApplicantTest.php @@ -10,25 +10,34 @@ use Shared\ValueObjects\Email; covers(Applicant::class); describe('applicant', function (): void { - it('creates a valid applicant', function (): void { - // GIVEN - $firstname = 'John'; - $lastname = 'Doe'; - $email = new Email('john.doe@example.com'); + beforeEach(function (): void { + $this->firstname = 'John'; + $this->lastname = 'Doe'; + $this->email = new Email('john.doe@example.com'); + }); - // WHEN - $applicant = new Applicant($firstname, $lastname, $email); + it('creates a valid applicant', function (): void { + // GIVEN / WHEN + $applicant = new Applicant( + $this->firstname, + $this->lastname, + $this->email + ); // THEN expect($applicant)->toBeInstanceOf(Applicant::class) - ->and($applicant->firstname)->toBe($firstname) - ->and($applicant->lastname)->toBe($lastname) - ->and($applicant->email)->toBe($email); + ->and($applicant->firstname)->toBe($this->firstname) + ->and($applicant->lastname)->toBe($this->lastname) + ->and($applicant->email)->toBe($this->email); }); it('rejects an empty or whitespace-only firstname', function (string $firstname): void { // WHEN - $action = fn (): Applicant => new Applicant($firstname, 'Doe', new Email('john.doe@example.com')); + $action = fn (): Applicant => new Applicant( + $firstname, + $this->lastname, + $this->email + ); // THEN expect($action)->toThrow(\DomainException::class, 'Applicant firstname cannot be empty.'); @@ -42,7 +51,11 @@ describe('applicant', function (): void { $firstname = str_repeat('a', 201); // WHEN - $action = fn (): Applicant => new Applicant($firstname, 'Doe', new Email('john.doe@example.com')); + $action = fn (): Applicant => new Applicant( + $firstname, + $this->lastname, + $this->email + ); // THEN expect($action)->toThrow(\DomainException::class, 'Applicant firstname exceeds the maximum allowed length.'); @@ -53,7 +66,11 @@ describe('applicant', function (): void { $firstname = str_repeat('a', 200); // WHEN - $applicant = new Applicant($firstname, 'Doe', new Email('john.doe@example.com')); + $applicant = new Applicant( + $firstname, + $this->lastname, + $this->email + ); // THEN expect(mb_strlen($applicant->firstname))->toBe(200); @@ -61,7 +78,11 @@ describe('applicant', function (): void { it('rejects an empty or whitespace-only lastname', function (string $lastname): void { // WHEN - $action = fn (): Applicant => new Applicant('John', $lastname, new Email('john.doe@example.com')); + $action = fn (): Applicant => new Applicant( + $this->firstname, + $lastname, + $this->email + ); // THEN expect($action)->toThrow(\DomainException::class, 'Applicant lastname cannot be empty.'); @@ -75,7 +96,11 @@ describe('applicant', function (): void { $lastname = str_repeat('a', 201); // WHEN - $action = fn (): Applicant => new Applicant('John', $lastname, new Email('john.doe@example.com')); + $action = fn (): Applicant => new Applicant( + $this->firstname, + $lastname, + $this->email + ); // THEN expect($action)->toThrow(\DomainException::class, 'Applicant lastname exceeds the maximum allowed length.'); @@ -86,7 +111,11 @@ describe('applicant', function (): void { $lastname = str_repeat('a', 200); // WHEN - $applicant = new Applicant('John', $lastname, new Email('john.doe@example.com')); + $applicant = new Applicant( + $this->firstname, + $lastname, + $this->email + ); // THEN expect(mb_strlen($applicant->lastname))->toBe(200); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php index 6083d0b..b85bdb3 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php @@ -11,41 +11,43 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; covers(CampaignRequestFilters::class); describe('campaign request filters', function (): void { - it('creates filters with both fields set', function (): void { - // GIVEN - $brandCode = new BrandCode('AAA01'); - $status = CampaignRequestStatus::CREATED; + beforeEach(function (): void { + $this->brandCode = new BrandCode('AAA01'); + $this->status = CampaignRequestStatus::CREATED; + }); - // WHEN - $filters = new CampaignRequestFilters(brandCode: $brandCode, status: $status); + it('creates filters with both fields set', function (): void { + // GIVEN / WHEN + $filters = new CampaignRequestFilters( + brandCode: $this->brandCode, + status: $this->status + ); // THEN - expect($filters->brandCode)->toBe($brandCode) - ->and($filters->status)->toBe($status); + expect($filters->brandCode)->toBe($this->brandCode) + ->and($filters->status)->toBe($this->status); }); it('creates filters with only brandCode set', function (): void { - // GIVEN - $brandCode = new BrandCode('AAA01'); - - // WHEN - $filters = new CampaignRequestFilters(brandCode: $brandCode); + // GIVEN / WHEN + $filters = new CampaignRequestFilters( + brandCode: $this->brandCode + ); // THEN - expect($filters->brandCode)->toBe($brandCode) + expect($filters->brandCode)->toBe($this->brandCode) ->and($filters->status)->toBeNull(); }); it('creates filters with only status set', function (): void { - // GIVEN - $status = CampaignRequestStatus::CREATED; - - // WHEN - $filters = new CampaignRequestFilters(status: $status); + // GIVEN / WHEN + $filters = new CampaignRequestFilters( + status: $this->status + ); // THEN expect($filters->brandCode)->toBeNull() - ->and($filters->status)->toBe($status); + ->and($filters->status)->toBe($this->status); }); it('rejects filters without any field set', function (): void { diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php index 2f7b8b8..af31c54 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php @@ -11,48 +11,48 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField; covers(CampaignRequestSortCriteria::class); describe('campaign request sort criteria', function (): void { - it('creates a valid campaign request sort criteria with one clause', function (): void { - // GIVEN - $clauses = [ + beforeEach(function (): void { + $this->singleClauses = [ new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending), ]; - - // WHEN - $campaignRequestSortCriteria = new CampaignRequestSortCriteria($clauses); - - // THEN - expect($campaignRequestSortCriteria->clauses)->toBe($clauses); - }); - - it('creates a valid campaign request sort criteria with several clauses', function (): void { - // GIVEN - $clauses = [ + $this->clauses = [ new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending), new CampaignRequestSortClause(field: CampaignRequestSortField::Status, direction: \SortDirection::Descending), ]; + $this->invalidClauses = [CampaignRequestSortField::BrandCode]; + $this->emptyClauses = []; + }); - // WHEN - $campaignRequestSortCriteria = new CampaignRequestSortCriteria($clauses); + + it('creates a valid campaign request sort criteria with one clause', function (): void { + // GIVEN / WHEN + $campaignRequestSortCriteria = new CampaignRequestSortCriteria( + $this->singleClauses + ); // THEN - expect($campaignRequestSortCriteria->clauses)->toBe($clauses); + expect($campaignRequestSortCriteria->clauses)->toBe($this->singleClauses); + }); + + it('creates a valid campaign request sort criteria with several clauses', function (): void { + // GIVEN / WHEN + $campaignRequestSortCriteria = new CampaignRequestSortCriteria( + $this->clauses + ); + + // THEN + expect($campaignRequestSortCriteria->clauses)->toBe($this->clauses); }); it('rejects a campaign request sort criteria without clause', function (): void { - // GIVEN - $clauses = []; - - // WHEN / THEN - expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($clauses)) + // GIVEN / WHEN / THEN + expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($this->emptyClauses)) ->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)) + // GIVEN / WHEN / THEN + expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($this->invalidClauses)) ->toThrow(\DomainException::class, 'CampaignRequestSortCriteria clauses must be CampaignRequestSortClause instances.'); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/RejectionReasonTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/RejectionReasonTest.php index 817d894..f72656f 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/RejectionReasonTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/RejectionReasonTest.php @@ -9,16 +9,17 @@ use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; covers(RejectionReason::class); describe('rejection reason', function (): void { - it('creates a valid rejection reason', function (): void { - // GIVEN - $value = 'Missing legal documents.'; + beforeEach(function (): void { + $this->value = 'Missing legal documents.'; + }); - // WHEN - $reason = new RejectionReason($value); + it('creates a valid rejection reason', function (): void { + // GIVEN / WHEN + $reason = new RejectionReason($this->value); // THEN expect($reason)->toBeInstanceOf(RejectionReason::class) - ->and($reason->value)->toBe($value); + ->and($reason->value)->toBe($this->value); }); it('rejects an empty or whitespace-only reason', function (string $value): void { diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php index a663ef3..829915c 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php @@ -11,47 +11,49 @@ use DateTime; covers(CouponAvailabilityCalendar::class); describe('coupon availability calendar', function (): void { - it('creates a valid coupon availability calendar with one period', function (): void { - // GIVEN - $periods = [ + beforeEach(function (): void { + $this->singlePeriods = [ new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), ]; - - // WHEN - $couponAvailabilityCalendar = new CouponAvailabilityCalendar($periods); - - // THEN - expect($couponAvailabilityCalendar->periods)->toBe($periods); - }); - - it('creates a valid coupon availability calendar with several periods', function (): void { - // GIVEN - $periods = [ + $this->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')), ]; + $this->invalidPeriods = ['2026-08-25', '2026-10-31']; + $this->emptyPeriods = []; + }); - // WHEN - $couponAvailabilityCalendar = new CouponAvailabilityCalendar($periods); + it('creates a valid coupon availability calendar with one period', function (): void { + // GIVEN / WHEN + $couponAvailabilityCalendar = new CouponAvailabilityCalendar( + $this->singlePeriods + ); // THEN - expect($couponAvailabilityCalendar->periods)->toBe($periods); + expect($couponAvailabilityCalendar->periods)->toBe($this->singlePeriods); + }); + + it('creates a valid coupon availability calendar with several periods', function (): void { + // GIVEN / WHEN + $couponAvailabilityCalendar = new CouponAvailabilityCalendar( + $this->periods + ); + + // THEN + expect($couponAvailabilityCalendar->periods)->toBe($this->periods); }); it('rejects a coupon availability calendar without period', function (): void { - // GIVEN - $periods = []; - - // GIVEN / THEN - expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar($periods))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar( + $this->emptyPeriods + ))->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.'); + // GIVEN / WHEN / THEN + expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar( + $this->invalidPeriods + ))->toThrow(\DomainException::class, 'CouponAvailabilityCalendar periods must be Period instances.'); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php index 335caad..007b7d6 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php @@ -10,25 +10,29 @@ use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; covers(CouponLegalInformation::class); describe('coupon legal information', function (): void { - it('creates a valid coupon legal information', function (): void { - // GIVEN - $legalText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; - $forLocale = Country::FRANCE; + beforeEach(function (): void { + $this->legalText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; + $this->emptyLegalText = ''; + $this->forLocale = Country::FRANCE; + }); - // WHEN - $couponLegalInformation = new CouponLegalInformation($legalText, $forLocale); + it('creates a valid coupon legal information', function (): void { + // GIVEN / WHEN + $couponLegalInformation = new CouponLegalInformation( + $this->legalText, + $this->forLocale + ); // THEN - expect($couponLegalInformation->legalText)->toBe($legalText) - ->and($couponLegalInformation->forLocale)->toBe($forLocale); + expect($couponLegalInformation->legalText)->toBe($this->legalText) + ->and($couponLegalInformation->forLocale)->toBe($this->forLocale); }); it('rejects invalid coupon legal information', function (): void { - // GIVEN - $legalText = ''; - $forLocale = Country::FRANCE; - - // GIVEN / THEN - expect(fn (): CouponLegalInformation => new CouponLegalInformation($legalText, $forLocale))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): CouponLegalInformation => new CouponLegalInformation( + $this->emptyLegalText, + $this->forLocale + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php index f3aa565..35dd4b3 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php @@ -9,21 +9,28 @@ use Domain\Models\ValueObjects\Coupon\CouponQuantity; covers(CouponQuantity::class); describe('coupon quantity', function (): void { - it('creates a valid coupon quantity', function (): void { - // GIVEN - $quantity = 10; + beforeEach(function (): void { + $this->quantity = 10; + $this->zeroQuantity = 0; + $this->negativeQuantity = -1; + }); - // WHEN - $couponQuantity = new CouponQuantity($quantity); + it('creates a valid coupon quantity', function (): void { + // GIVEN / WHEN + $couponQuantity = new CouponQuantity( + $this->quantity + ); // THEN expect($couponQuantity)->toBeInstanceOf(CouponQuantity::class) - ->and($couponQuantity->quantity)->toBe($quantity); + ->and($couponQuantity->quantity)->toBe($this->quantity); }); it('rejects a quantity of zero', function (): void { // WHEN - $action = fn (): CouponQuantity => new CouponQuantity(0); + $action = fn (): CouponQuantity => new CouponQuantity( + $this->zeroQuantity + ); // THEN expect($action)->toThrow(\DomainException::class, 'CouponQuantity quantity must be positive.'); @@ -31,7 +38,9 @@ describe('coupon quantity', function (): void { it('rejects a negative quantity', function (): void { // WHEN - $action = fn (): CouponQuantity => new CouponQuantity(-1); + $action = fn (): CouponQuantity => new CouponQuantity( + $this->negativeQuantity + ); // THEN expect($action)->toThrow(\DomainException::class, 'CouponQuantity quantity must be positive.'); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php index 46eedd2..952a9b2 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php @@ -10,25 +10,29 @@ use Shared\Enums\Country; covers(CouponValidityInformation::class); describe('coupon validity information', function (): void { - it('creates a valid coupon validity information', function (): void { - // GIVEN - $validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; - $forLocale = Country::FRANCE; + beforeEach(function (): void { + $this->validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; + $this->emptyValidityText = ''; + $this->forLocale = Country::FRANCE; + }); - // WHEN - $couponValidityInformation = new CouponValidityInformation($validityText, $forLocale); + it('creates a valid coupon validity information', function (): void { + // GIVEN / WHEN + $couponValidityInformation = new CouponValidityInformation( + $this->validityText, + $this->forLocale + ); // THEN - expect($couponValidityInformation->validityText)->toBe($validityText) - ->and($couponValidityInformation->forLocale)->toBe($forLocale); + expect($couponValidityInformation->validityText)->toBe($this->validityText) + ->and($couponValidityInformation->forLocale)->toBe($this->forLocale); }); it('rejects invalid coupon validity information', function (): void { - // GIVEN - $validityText = ''; - $forLocale = Country::FRANCE; - - // GIVEN / THEN - expect(fn (): CouponValidityInformation => new CouponValidityInformation($validityText, $forLocale))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): CouponValidityInformation => new CouponValidityInformation( + $this->emptyValidityText, + $this->forLocale + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php index cd0db59..57c85c9 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php @@ -10,25 +10,29 @@ use DateTime; covers(CouponValidity::class); describe('coupon validity', function (): void { - it('creates a valid coupon validity', function (): void { - // GIVEN - $days = 30; - $until = new DateTime('2026-11-30'); + beforeEach(function (): void { + $this->days = 30; + $this->zeroDays = 0; + $this->until = new DateTime('2026-11-30'); + }); - // WHEN - $couponValidity = new CouponValidity($days, $until); + it('creates a valid coupon validity', function (): void { + // GIVEN / WHEN + $couponValidity = new CouponValidity( + $this->days, + $this->until + ); // THEN - expect($couponValidity->days)->toBe($days) - ->and($couponValidity->until)->toBe($until); + expect($couponValidity->days)->toBe($this->days) + ->and($couponValidity->until)->toBe($this->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); + // GIVEN / WHEN / THEN + expect(fn (): CouponValidity => new CouponValidity( + $this->zeroDays, + $this->until + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/CouponNameTest.php b/tests/Unit/Domain/Models/ValueObjects/CouponNameTest.php index 8125316..f1b0524 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CouponNameTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CouponNameTest.php @@ -9,22 +9,25 @@ use Domain\Models\ValueObjects\CouponName; covers(CouponName::class); describe('coupon name', function (): void { - it('creates a valid coupon name', function (): void { - // GIVEN - $value = 'Name'; + beforeEach(function (): void { + $this->value = 'Name'; + $this->emptyValue = ''; + }); - // WHEN - $name = new CouponName($value); + it('creates a valid coupon name', function (): void { + // GIVEN / WHEN + $name = new CouponName( + $this->value + ); // THEN - expect($name->value)->toBe($value); + expect($name->value)->toBe($this->value); }); it('rejects invalid coupon name', function (): void { - // GIVEN - $value = ''; - - // GIVEN / THEN - expect(fn (): CouponName => new CouponName($value))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): CouponName => new CouponName( + $this->emptyValue + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/GS1Test.php b/tests/Unit/Domain/Models/ValueObjects/GS1Test.php index 84e8d16..1e56044 100644 --- a/tests/Unit/Domain/Models/ValueObjects/GS1Test.php +++ b/tests/Unit/Domain/Models/ValueObjects/GS1Test.php @@ -10,25 +10,29 @@ use Domain\Models\ValueObjects\GS1; covers(GS1::class); describe('gs1', function (): void { - it('creates a valid gs1', function (): void { - // GIVEN - $value = '1010100101'; - $format = BarcodeFormat::EAN13; + beforeEach(function (): void { + $this->value = '1010100101'; + $this->emptyValue = ''; + $this->format = BarcodeFormat::EAN13; + }); + it('creates a valid gs1', function (): void { // WHEN - $GS1 = new GS1($value, $format); + $GS1 = new GS1( + $this->value, + $this->format + ); // THEN - expect($GS1->value)->toBe($value) - ->and($GS1->format)->toBe($format); + expect($GS1->value)->toBe($this->value) + ->and($GS1->format)->toBe($this->format); }); it('rejects invalid gs1', function (): void { - // GIVEN - $value = ''; - $format = BarcodeFormat::EAN13; - - // GIVEN / THEN - expect(fn (): GS1 => new GS1($value, $format))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): GS1 => new GS1( + $this->emptyValue, + $this->format + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php index c7fe5cb..54b03ea 100644 --- a/tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php @@ -9,22 +9,25 @@ use Domain\Models\ValueObjects\IncoCode; covers(IncoCode::class); describe('inco code', function (): void { - it('creates a valid inco code', function (): void { - // GIVEN - $value = 1010100101; + beforeEach(function (): void { + $this->value = 1010100101; + $this->zeroValue = 0; + }); - // WHEN - $incoCode = new IncoCode($value); + it('creates a valid inco code', function (): void { + // GIVEN / WHEN + $incoCode = new IncoCode( + $this->value + ); // THEN - expect($incoCode->value)->toBe($value); + expect($incoCode->value)->toBe($this->value); }); it('rejects invalid inco code', function (): void { - // GIVEN - $value = 0; - - // GIVEN / THEN - expect(fn (): IncoCode => new IncoCode($value))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): IncoCode => new IncoCode( + $this->zeroValue + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/MoneyAmountTest.php b/tests/Unit/Domain/Models/ValueObjects/MoneyAmountTest.php index 089ee70..dbd4190 100644 --- a/tests/Unit/Domain/Models/ValueObjects/MoneyAmountTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/MoneyAmountTest.php @@ -11,23 +11,33 @@ use Shared\ValueObjects\Money; covers(MoneyAmount::class); describe('money amount', function (): void { - it('wraps a Money value', function (): void { - // GIVEN - $money = new Money(Currency::EUR, 300); + beforeEach(function (): void { + $this->money = new Money(Currency::EUR, 300); + $this->secondMoney = new Money(Currency::EUR, 500); + }); - // WHEN - $amount = new MoneyAmount($money); + it('wraps a Money value', function (): void { + // GIVEN / WHEN + $amount = new MoneyAmount( + $this->money + ); // THEN expect($amount)->toBeInstanceOf(MoneyAmount::class) - ->and($amount->value)->toBe($money); + ->and($amount->value)->toBe($this->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)); + // GIVEN / WHEN + $amount1 = new MoneyAmount( + $this->money + ); + $amount2 = new MoneyAmount( + $this->money + ); + $amount3 = new MoneyAmount( + $this->secondMoney + ); // THEN expect($amount1->equals($amount2))->toBeTrue() @@ -36,8 +46,10 @@ describe('money amount', function (): void { }); it('converts to string by delegating to the wrapped Money', function (): void { - // GIVEN - $amount = new MoneyAmount(new Money(Currency::EUR, 300)); + // GIVEN / WHEN + $amount = new MoneyAmount( + $this->money + ); // THEN expect((string) $amount)->toBe((string) $amount->value); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php index 10286c9..d45e709 100644 --- a/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php @@ -9,32 +9,48 @@ use Domain\Models\ValueObjects\PrintRules; covers(PrintRules::class); describe('print rules', function (): void { + beforeEach(function (): void { + $this->maxPerCustomer = 3; + $this->minMaxPerCustomer = 1; + $this->zeroMaxPerCustomer = 0; + $this->cooldownDays = 30; + $this->minCooldownDays = 1; + $this->zeroCooldownDays = 0; + }); + it('creates print rules with both fields set', function (): void { - // WHEN - $printRules = new PrintRules(maxPerCustomer: 3, cooldownDays: 30); + // GIVEN / WHEN + $printRules = new PrintRules( + maxPerCustomer: $this->maxPerCustomer, + cooldownDays: $this->cooldownDays + ); // THEN expect($printRules)->toBeInstanceOf(PrintRules::class) - ->and($printRules->maxPerCustomer)->toBe(3) - ->and($printRules->cooldownDays)->toBe(30); + ->and($printRules->maxPerCustomer)->toBe($this->maxPerCustomer) + ->and($printRules->cooldownDays)->toBe($this->cooldownDays); }); it('creates print rules with only maxPerCustomer set', function (): void { - // WHEN - $printRules = new PrintRules(maxPerCustomer: 5); + // GIVEN / WHEN + $printRules = new PrintRules( + maxPerCustomer: $this->maxPerCustomer + ); // THEN - expect($printRules->maxPerCustomer)->toBe(5) + expect($printRules->maxPerCustomer)->toBe($this->maxPerCustomer) ->and($printRules->cooldownDays)->toBeNull(); }); it('creates print rules with only cooldownDays set', function (): void { - // WHEN - $printRules = new PrintRules(cooldownDays: 45); + // GIVEN / WHEN + $printRules = new PrintRules( + cooldownDays: $this->cooldownDays + ); // THEN expect($printRules->maxPerCustomer)->toBeNull() - ->and($printRules->cooldownDays)->toBe(45); + ->and($printRules->cooldownDays)->toBe($this->cooldownDays); }); it('rejects print rules without any field set', function (): void { @@ -44,29 +60,28 @@ describe('print rules', function (): void { }); it('accepts the minimum value of 1 for both fields', function (): void { - // WHEN - $printRules = new PrintRules(maxPerCustomer: 1, cooldownDays: 1); + // GIVEN / WHEN + $printRules = new PrintRules( + maxPerCustomer: $this->minMaxPerCustomer, + cooldownDays: $this->minCooldownDays + ); // THEN - expect($printRules->maxPerCustomer)->toBe(1) - ->and($printRules->cooldownDays)->toBe(1); + expect($printRules->maxPerCustomer)->toBe($this->minMaxPerCustomer) + ->and($printRules->cooldownDays)->toBe($this->minCooldownDays); }); 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.'); + // GIVEN / WHEN / THEN + expect(fn (): PrintRules => new PrintRules( + maxPerCustomer: $this->zeroMaxPerCustomer + ))->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.'); + // GIVEN / WHEN / THEN + expect(fn (): PrintRules => new PrintRules( + cooldownDays: $this->zeroCooldownDays + ))->toThrow(\InvalidArgumentException::class, 'Cooldown days must be greater than 0.'); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/ProcessingCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/ProcessingCodeTest.php index ae020bc..2946cd1 100644 --- a/tests/Unit/Domain/Models/ValueObjects/ProcessingCodeTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/ProcessingCodeTest.php @@ -9,22 +9,25 @@ use Domain\Models\ValueObjects\ProcessingCode; covers(ProcessingCode::class); describe('processing code', function (): void { - it('creates a valid processing code', function (): void { - // GIVEN - $value = '1010100101'; + beforeEach(function (): void { + $this->value = '1010100101'; + $this->emptyValue = ''; + }); - // WHEN - $processingCode = new ProcessingCode($value); + it('creates a valid processing code', function (): void { + // GIVEN / WHEN + $processingCode = new ProcessingCode( + $this->value + ); // THEN - expect($processingCode->value)->toBe($value); + expect($processingCode->value)->toBe($this->value); }); it('rejects invalid processing code', function (): void { - // GIVEN - $value = ''; - - // GIVEN / THEN - expect(fn (): ProcessingCode => new ProcessingCode($value))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): ProcessingCode => new ProcessingCode( + $this->emptyValue + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php index 0602074..5320c51 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php @@ -10,17 +10,21 @@ use DateTime; covers(Period::class); describe('period', function (): void { - it('creates a valid period', function (): void { - // GIVEN - $start = new DateTime('2026-01-01'); - $end = new DateTime('2026-01-31'); + beforeEach(function (): void { + $this->start = new DateTime('2026-01-01'); + $this->end = new DateTime('2026-01-31'); + }); - // WHEN - $period = new Period($start, $end); + it('creates a valid period', function (): void { + // GIVEN / WHEN + $period = new Period( + $this->start, + $this->end + ); // THEN expect($period)->toBeInstanceOf(Period::class) - ->and($period->start)->toBe($start) - ->and($period->end)->toBe($end); + ->and($period->start)->toBe($this->start) + ->and($period->end)->toBe($this->end); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php index 5197b8d..e5f835c 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php @@ -9,29 +9,38 @@ use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; covers(RequestedCouponQuantity::class); describe('quantity', function (): void { - it('creates a valid quantity', function (): void { - // GIVEN - $value = 10; + beforeEach(function (): void { + $this->value = 10; + $this->zeroValue = 0; + $this->negativeValue = -1; + }); - // WHEN - $quantity = new RequestedCouponQuantity($value); + it('creates a valid quantity', function (): void { + // GIVEN / WHEN + $quantity = new RequestedCouponQuantity( + $this->value + ); // THEN expect($quantity)->toBeInstanceOf(RequestedCouponQuantity::class) - ->and($quantity->value)->toBe($value); + ->and($quantity->value)->toBe($this->value); }); it('rejects a quantity of zero', function (): void { - // WHEN - $action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity(0); + // GIVEN / WHEN + $action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity( + $this->zeroValue + ); // THEN expect($action)->toThrow(\DomainException::class, 'Quantity should be more than 0.'); }); it('rejects a negative quantity', function (): void { - // WHEN - $action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity(-1); + // GIVEN / WHEN + $action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity( + $this->negativeValue + ); // THEN expect($action)->toThrow(\DomainException::class, 'Quantity should be more than 0.'); diff --git a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php index 02515f0..42441f1 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php @@ -10,45 +10,74 @@ use Domain\Models\ValueObjects\Stock\StockAdjustment; covers(StockAdjustment::class); describe('stock adjustment', function (): void { + beforeEach(function (): void { + $this->quantity = 10; + $this->zeroQuantity = 0; + $this->negativeQuantity = -5; + $this->reason = 'Inventory correction'; + }); + it('creates a valid stock adjustment', function (): void { // GIVEN - $quantity = 10; - $reason = 'Inventory correction'; - $actorId = ActorId::generate(); + $actorId = ActorId::generate(); // WHEN - $adjustment = new StockAdjustment($quantity, $reason, $actorId); + $adjustment = new StockAdjustment( + $this->quantity, + $this->reason, + $actorId + ); // THEN expect($adjustment)->toBeInstanceOf(StockAdjustment::class) - ->and($adjustment->quantity)->toBe($quantity) - ->and($adjustment->reason)->toBe($reason) + ->and($adjustment->quantity)->toBe($this->quantity) + ->and($adjustment->reason)->toBe($this->reason) ->and($adjustment->actorId)->toBe($actorId) ->and($adjustment->createdAt)->toBeNull(); }); it('creates a stock adjustment with an explicit createdAt date', function (): void { // GIVEN + $actorId = ActorId::generate(); $createdAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); // WHEN - $adjustment = new StockAdjustment(10, 'Inventory correction', ActorId::generate(), $createdAt); + $adjustment = new StockAdjustment( + $this->quantity, + $this->reason, + $actorId, + $createdAt + ); // THEN expect($adjustment->createdAt)->toBe($createdAt); }); it('rejects a quantity of zero', function (): void { + // GIVEN + $actorId = ActorId::generate(); + // WHEN - $action = fn (): StockAdjustment => new StockAdjustment(0, 'Inventory correction', ActorId::generate()); + $action = fn (): StockAdjustment => new StockAdjustment( + $this->zeroQuantity, + $this->reason, + $actorId + ); // THEN expect($action)->toThrow(\DomainException::class, 'Stock Amount should be more than 0.'); }); it('rejects a negative quantity', function (): void { + // GIVEN + $actorId = ActorId::generate(); + // WHEN - $action = fn (): StockAdjustment => new StockAdjustment(-5, 'Inventory correction', ActorId::generate()); + $action = fn (): StockAdjustment => new StockAdjustment( + $this->negativeQuantity, + $this->reason, + $actorId + ); // THEN expect($action)->toThrow(\DomainException::class, 'Stock Amount should be more than 0.'); diff --git a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php index ae988fb..59a7753 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php @@ -9,29 +9,38 @@ use Domain\Models\ValueObjects\Stock\StockAmount; covers(StockAmount::class); describe('stock amount', function (): void { - it('creates a valid stock amount', function (): void { - // GIVEN - $value = 10; + beforeEach(function (): void { + $this->value = 10; + $this->zeroValue = 0; + $this->negativeValue = -1; + }); - // WHEN - $amount = new StockAmount($value); + it('creates a valid stock amount', function (): void { + // GIVEN / WHEN + $amount = new StockAmount( + $this->value + ); // THEN expect($amount)->toBeInstanceOf(StockAmount::class) - ->and($amount->value)->toBe($value); + ->and($amount->value)->toBe($this->value); }); it('accepts a zero value', function (): void { - // WHEN - $amount = new StockAmount(0); + // GIVEN / WHEN + $amount = new StockAmount( + $this->zeroValue + ); // THEN - expect($amount->value)->toBe(0); + expect($amount->value)->toBe($this->zeroValue); }); it('rejects a negative value', function (): void { - // WHEN - $action = fn (): StockAmount => new StockAmount(-1); + // GIVEN / WHEN + $action = fn (): StockAmount => new StockAmount( + $this->negativeValue + ); // THEN expect($action)->toThrow(\DomainException::class, 'Stock Amount should be positive.'); diff --git a/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php index be3f6b4..2a236b4 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php @@ -9,22 +9,25 @@ use Domain\Models\ValueObjects\Tag\TagName; covers(TagName::class); describe('tag name', function (): void { - it('creates a valid tag name', function (): void { - // GIVEN - $value = 'Name'; + beforeEach(function (): void { + $this->value = 'Name'; + $this->emptyValue = ''; + }); - // WHEN - $name = new TagName($value); + it('creates a valid tag name', function (): void { + // GIVEN / WHEN + $name = new TagName( + $this->value + ); // THEN - expect($name->value)->toBe($value); + expect($name->value)->toBe($this->value); }); it('rejects invalid tag name', function (): void { - // GIVEN - $value = ''; - - // GIVEN / THEN - expect(fn (): TagName => new TagName($value))->toThrow(\DomainException::class); + // GIVEN / WHEN / THEN + expect(fn (): TagName => new TagName( + $this->emptyValue + ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php b/tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php index 318c519..de80f52 100644 --- a/tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php @@ -9,29 +9,38 @@ use Domain\Models\ValueObjects\WarningThresholdQuantity; covers(WarningThresholdQuantity::class); describe('warning threshold quantity', function (): void { - it('creates a valid warning threshold quantity', function (): void { - // GIVEN - $value = 10; + beforeEach(function (): void { + $this->value = 10; + $this->zeroValue = 0; + $this->negativeValue = -1; + }); - // WHEN - $threshold = new WarningThresholdQuantity($value); + it('creates a valid warning threshold quantity', function (): void { + // GIVEN / WHEN + $warningThresholdQuantity = new WarningThresholdQuantity( + $this->value + ); // THEN - expect($threshold)->toBeInstanceOf(WarningThresholdQuantity::class) - ->and($threshold->value)->toBe($value); + expect($warningThresholdQuantity)->toBeInstanceOf(WarningThresholdQuantity::class) + ->and($warningThresholdQuantity->value)->toBe($this->value); }); it('accepts a zero value, meaning no warning', function (): void { - // WHEN - $threshold = new WarningThresholdQuantity(0); + // GIVEN / WHEN + $warningThresholdQuantity = new WarningThresholdQuantity( + $this->zeroValue + ); // THEN - expect($threshold->value)->toBe(0); + expect($warningThresholdQuantity->value)->toBe($this->zeroValue); }); it('rejects a negative value', function (): void { - // WHEN - $action = fn (): WarningThresholdQuantity => new WarningThresholdQuantity(-1); + // GIVEN / WHEN + $action = fn (): WarningThresholdQuantity => new WarningThresholdQuantity( + $this->negativeValue + ); // THEN expect($action)->toThrow(\DomainException::class, 'Warning threshold quantity cannot be negative.'); From 237e2c62be8b0106e8a3fed999e392576a1cfdc8 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Mon, 28 Sep 2026 12:18:27 +0200 Subject: [PATCH 47/52] feat: :sparkles: create campaign, coupon and stock repositories --- .../Ports/Repositories/CampaignRepository.php | 14 ++++++++++++++ src/Domain/Ports/Repositories/CouponRepository.php | 14 ++++++++++++++ src/Domain/Ports/Repositories/StockRepository.php | 14 ++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/Domain/Ports/Repositories/CampaignRepository.php create mode 100644 src/Domain/Ports/Repositories/CouponRepository.php create mode 100644 src/Domain/Ports/Repositories/StockRepository.php diff --git a/src/Domain/Ports/Repositories/CampaignRepository.php b/src/Domain/Ports/Repositories/CampaignRepository.php new file mode 100644 index 0000000..a042630 --- /dev/null +++ b/src/Domain/Ports/Repositories/CampaignRepository.php @@ -0,0 +1,14 @@ + + */ +interface CampaignRepository extends Repository +{ +} diff --git a/src/Domain/Ports/Repositories/CouponRepository.php b/src/Domain/Ports/Repositories/CouponRepository.php new file mode 100644 index 0000000..458302c --- /dev/null +++ b/src/Domain/Ports/Repositories/CouponRepository.php @@ -0,0 +1,14 @@ + + */ +interface CouponRepository extends Repository +{ +} diff --git a/src/Domain/Ports/Repositories/StockRepository.php b/src/Domain/Ports/Repositories/StockRepository.php new file mode 100644 index 0000000..ab7962e --- /dev/null +++ b/src/Domain/Ports/Repositories/StockRepository.php @@ -0,0 +1,14 @@ + + */ +interface StockRepository extends Repository +{ +} From 028f0a675d329929e9321763a963f8d7461ca301 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 30 Sep 2026 09:29:01 +0200 Subject: [PATCH 48/52] refactor: :recycle: delete stock repository --- src/Domain/Ports/Repositories/StockRepository.php | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 src/Domain/Ports/Repositories/StockRepository.php diff --git a/src/Domain/Ports/Repositories/StockRepository.php b/src/Domain/Ports/Repositories/StockRepository.php deleted file mode 100644 index ab7962e..0000000 --- a/src/Domain/Ports/Repositories/StockRepository.php +++ /dev/null @@ -1,14 +0,0 @@ - - */ -interface StockRepository extends Repository -{ -} From 567a5cd48e3e0df6668c57c01c823d5c4d0c171d Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 30 Sep 2026 12:18:56 +0200 Subject: [PATCH 49/52] feat: :sparkles: add campaign request creation use case --- .../Create/CreateCampaignRequest.php | 55 +++++ .../UseCases/CampaignRequest/Create/Input.php | 24 +++ .../CampaignRequest/Create/Output.php | 29 +++ .../Models/Entities/CampaignRequest.php | 26 ++- src/Domain/Models/ValueObjects/Period.php | 3 + .../RequestedCoupon/RequestedCoupon.php | 39 +++- .../CampaignRequestRepository.php | 2 +- .../Create/CreateCampaignRequestTest.php | 106 ++++++++++ .../Find/FindCampaignRequestTest.php | 5 +- .../List/ListCampaignsRequestTest.php | 10 +- .../Reject/RejectCampaignRequestTest.php | 5 +- .../Models/Entities/CampaignRequestTest.php | 5 +- .../RequestedCoupon/PeriodTest.php | 19 ++ .../RequestedCoupon/RequestedCouponTest.php | 200 ++++++++++++++---- 14 files changed, 475 insertions(+), 53 deletions(-) create mode 100644 src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php create mode 100644 src/Application/UseCases/CampaignRequest/Create/Input.php create mode 100644 src/Application/UseCases/CampaignRequest/Create/Output.php create mode 100644 tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php diff --git a/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php new file mode 100644 index 0000000..9dc1b30 --- /dev/null +++ b/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php @@ -0,0 +1,55 @@ + */ +final readonly class CreateCampaignRequest implements UseCase, Transactional +{ + public function __construct( + private CampaignRequestRepository $campaignRequestRepository, + private BrandRepository $brandRepository + ) { + } + + #[\Override] + public function execute(BaseInput $input): Output + { + Assert::isInstanceOf($input, Input::class); + + if (!$this->brandRepository->existsByCode($input->brandCode)) { + throw new BrandNotFoundException($input->brandCode); + } + + $campaignRequest = CampaignRequest::create( + brandCode: $input->brandCode, + name: $input->name, + applicant: $input->applicant, + campaignType: $input->campaignType, + requestedCoupon: $input->requestedCoupon, + ); + + $this->campaignRequestRepository->save($campaignRequest); + + return new Output( + id: $campaignRequest->id(), + brandCode: $campaignRequest->brandCode, + name: $campaignRequest->name, + applicant: $campaignRequest->applicant, + campaignType: $campaignRequest->campaignType, + requestedCoupon: $campaignRequest->requestedCoupon, + status: $campaignRequest->status, + createdAt: $campaignRequest->createdAt(), + ); + } +} diff --git a/src/Application/UseCases/CampaignRequest/Create/Input.php b/src/Application/UseCases/CampaignRequest/Create/Input.php new file mode 100644 index 0000000..bffc422 --- /dev/null +++ b/src/Application/UseCases/CampaignRequest/Create/Input.php @@ -0,0 +1,24 @@ +updatedAt; } + public static function create( + BrandCode $brandCode, + CampaignName $name, + Applicant $applicant, + CampaignRequestType $campaignType, + RequestedCoupon $requestedCoupon, + ?\DateTimeInterface $updatedAt = null, + ): self { + return new self( + id: CampaignRequestId::generate(), + brandCode: $brandCode, + name: $name, + applicant: $applicant, + campaignType: $campaignType, + requestedCoupon: $requestedCoupon, + status: CampaignRequestStatus::CREATED, + rejectionReason: null, + updatedAt: $updatedAt, + ); + } public static function hydrate( CampaignRequestId $id, diff --git a/src/Domain/Models/ValueObjects/Period.php b/src/Domain/Models/ValueObjects/Period.php index da672a4..c189079 100644 --- a/src/Domain/Models/ValueObjects/Period.php +++ b/src/Domain/Models/ValueObjects/Period.php @@ -12,5 +12,8 @@ final readonly class Period public DateTime $start, public DateTime $end, ) { + if ($this->end < $this->start) { + throw new \DomainException('Period end date cannot be before its start date.'); + } } } diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php index 683cd5c..c85f37d 100644 --- a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -15,9 +15,16 @@ use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\WarningThresholdQuantity; +use Shared\Enums\Country; final readonly class RequestedCoupon { + private const int DEFAULT_WARNING_THRESHOLD_QUANTITY = 100; + private const int MIN_FACE_VALUE_IN_CENTS = 1; + private const int MAX_FACE_VALUE_IN_CENTS = 300; + + public WarningThresholdQuantity $warningThresholdQuantity; + /** * @param array $couponValidityInformation * @param array $products @@ -34,12 +41,42 @@ final readonly class RequestedCoupon public array $products = [], public ?PrintRules $printRules = null, public ?GS1 $gs1 = null, - public ?WarningThresholdQuantity $warningThresholdQuantity = null, + ?WarningThresholdQuantity $warningThresholdQuantity = null, ) { + $money = $this->moneyAmount->value; + foreach ($this->couponValidityInformation as $validityInformation) { if (!$validityInformation instanceof CouponValidityInformation) { throw new \DomainException('Coupon validity information must be CouponValidityInformation instances.'); } } + + if ($money->value < self::MIN_FACE_VALUE_IN_CENTS || $money->value > self::MAX_FACE_VALUE_IN_CENTS) { + throw new \DomainException(sprintf( + 'Coupon face value must be between 0.01€ and 3.00€, %s given.', + $money, + )); + } + + if (!$this->hasValidityInformationFor(Country::FRANCE)) { + throw new \DomainException('A coupon requires validity information for France.'); + } + + if ($this->gs1 instanceof GS1 && !$this->hasValidityInformationFor(Country::BENELUX)) { + throw new \DomainException('A GS1 coupon requires validity information for Benelux.'); + } + + $this->warningThresholdQuantity = $warningThresholdQuantity ?? new WarningThresholdQuantity(self::DEFAULT_WARNING_THRESHOLD_QUANTITY); + } + + private function hasValidityInformationFor(Country $country): bool + { + foreach ($this->couponValidityInformation as $validityInformation) { + if ($validityInformation->forLocale === $country) { + return true; + } + } + + return false; } } diff --git a/src/Domain/Ports/Repositories/CampaignRequestRepository.php b/src/Domain/Ports/Repositories/CampaignRequestRepository.php index f8c6cbe..909202a 100644 --- a/src/Domain/Ports/Repositories/CampaignRequestRepository.php +++ b/src/Domain/Ports/Repositories/CampaignRequestRepository.php @@ -13,7 +13,7 @@ use Shared\ValueObjects\Pagination\Paginated; use Shared\ValueObjects\Pagination\Pagination; /** - * @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\Brand\BrandId, \Domain\Models\Entities\Brand> + * @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId, \Domain\Models\Entities\CampaignRequest> */ interface CampaignRequestRepository extends Repository { diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php new file mode 100644 index 0000000..af5ea21 --- /dev/null +++ b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php @@ -0,0 +1,106 @@ +input = new Input( + brandCode: new BrandCode('AAA01'), + name: new CampaignName('Campaign name'), + applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')), + campaignType: CampaignRequestType::CLUB, + requestedCoupon: new RequestedCoupon( + name: new CouponName('Coupon A'), + quantity: new RequestedCouponQuantity(100), + diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)), + processingCode: new ProcessingCode('PC1'), + couponPicture: new CouponPicture( + new PicturePath('coupons/product.jpg'), + new PicturePath('coupons/thumbnail.jpg'), + new PicturePath('coupons/inactive_thumbnail.jpg') + ), + couponValidity: new CouponValidity(30, new DateTime('2026-02-01')), + couponValidityInformation: [ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + ], + products: [new IncoCode(35784), new IncoCode(15522)], + printRules: new PrintRules(maxPerCustomer: 3), + warningThresholdQuantity: new WarningThresholdQuantity(10), + ), + ); +}); + +describe('create campaign request use case', function (): void { + it('creates a campaign request when the brand exists', function (): void { + // GIVEN + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('existsByCode')->once()->with($this->input->brandCode)->andReturn(true); + + $campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class); + $campaignRequestRepository->shouldReceive('save')->once()->with(\Mockery::type(CampaignRequest::class)); + + $useCase = new CreateCampaignRequest($campaignRequestRepository, $brandRepository); + + // WHEN + $result = $useCase->execute($this->input); + + // THEN + expect($result)->toBeInstanceOf(Output::class) + ->and($result->status)->toBe(CampaignRequestStatus::CREATED) + ->and($result->brandCode)->toBe($this->input->brandCode); + }); +}); + +describe('create campaign request use case validation rules', function (): void { + it('throws when the brand does not exist', function (): void { + // GIVEN + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('existsByCode')->once()->with($this->input->brandCode)->andReturn(false); + + $campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class); + $campaignRequestRepository->shouldNotReceive('save'); + + $useCase = new CreateCampaignRequest($campaignRequestRepository, $brandRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($this->input); + + // THEN + expect($action)->toThrow(BrandNotFoundException::class); + }); +}); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 606acd6..715a98a 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -62,7 +62,10 @@ describe('find campaign request use case', function (): void { new PicturePath('coupons/inactive_thumbnail.jpg') ), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], + [ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index dd4f0fe..119c54f 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -55,7 +55,10 @@ describe('list campaigns request use case', function (): void { new PicturePath('coupons/inactive_thumbnail.jpg') ), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], + [ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), @@ -74,7 +77,10 @@ describe('list campaigns request use case', function (): void { new PicturePath('coupons/inactive_thumbnail.jpg') ), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], + [ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ], [new IncoCode(24514), new IncoCode(46259)], new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 85f1bfa..780085d 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -56,7 +56,10 @@ describe('reject campaign request use case', function (): void { new PicturePath('coupons/inactive_thumbnail.jpg') ), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], + [ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 1c2edbe..0dbcba0 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -51,7 +51,10 @@ describe('campaign request', function (): void { new PicturePath('coupons/inactive_thumbnail.jpg') ), new CouponValidity(30, new DateTime('2026-02-01')), - [new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE)], + [ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), new GS1('01234567890128', BarcodeFormat::EAN13), diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php index 5320c51..e161db9 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php @@ -27,4 +27,23 @@ describe('period', function (): void { ->and($period->start)->toBe($this->start) ->and($period->end)->toBe($this->end); }); + + it('accepts a period whose end equals its start', function (): void { + // GIVEN + $start = new DateTime('2026-01-01'); + $end = new DateTime('2026-01-01'); + + // WHEN + $period = new Period($start, $end); + + // THEN + expect($period->start)->toBe($start) + ->and($period->end)->toBe($end); + }); + + it('throws when the end is before the start', function (): void { + // GIVEN / WHEN / THEN + expect(fn () => new Period(new DateTime('2026-02-01'), new DateTime('2026-01-01'))) + ->toThrow(\DomainException::class, 'Period end date cannot be before its start date.'); + }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index 325ab50..c467cb1 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -29,27 +29,33 @@ covers(RequestedCoupon::class); describe('requested coupon', function (): void { beforeEach(function (): void { - $this->name = new CouponName('Coupon A'); - $this->quantity = new RequestedCouponQuantity(100); - $this->diffusion = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); - $this->moneyAmount = new MoneyAmount(new Money(Currency::EUR, 300)); - $this->processingCode = new ProcessingCode('PC1'); - $this->couponPicture = new CouponPicture( + $this->name = new CouponName('Coupon A'); + $this->quantity = new RequestedCouponQuantity(100); + $this->diffusion = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); + $this->moneyAmount = new MoneyAmount(new Money(Currency::EUR, 300)); + $this->processingCode = new ProcessingCode('PC1'); + $this->couponPicture = new CouponPicture( new PicturePath('coupons/product.jpg'), new PicturePath('coupons/thumbnail.jpg'), new PicturePath('coupons/inactive_thumbnail.jpg') ); - $this->couponValidity = new CouponValidity(30, new DateTime('2026-02-01')); - $this->couponValidityInformation = [ - new CouponValidityInformation( - 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - Country::FRANCE - ), + $this->couponValidity = new CouponValidity(30, new DateTime('2026-02-01')); + $this->franceValidityInformation = new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE + ); + $this->beneluxValidityInformation = new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::BENELUX + ); + $this->couponValidityInformation = [ + $this->franceValidityInformation, + $this->beneluxValidityInformation, ]; - $this->products = [new IncoCode(35784), new IncoCode(15522)]; - $this->printRules = new PrintRules(maxPerCustomer: 3); - $this->gs1 = new GS1('01234567890128', BarcodeFormat::EAN13); - $this->warningThresholdQuantity = new WarningThresholdQuantity(10); + $this->products = [new IncoCode(35784), new IncoCode(15522)]; + $this->printRules = new PrintRules(maxPerCustomer: 3); + $this->gs1 = new GS1('01234567890128', BarcodeFormat::EAN13); + $this->warningThresholdQuantity = new WarningThresholdQuantity(10); }); afterEach(function (): void { @@ -96,7 +102,7 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture:$this->couponPicture, + couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, printRules: $this->printRules, @@ -119,7 +125,7 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture:$this->couponPicture, + couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, products: $this->products, @@ -142,7 +148,7 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture:$this->couponPicture, + couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, products: $this->products, @@ -157,7 +163,7 @@ describe('requested coupon', function (): void { ->and($coupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); }); - it('hydrates a requested coupon without warning threshold quantity', function (): void { + it('defaults the warning threshold quantity to 100 when not provided', function (): void { // GIVEN / WHEN $coupon = new RequestedCoupon( name: $this->name, @@ -165,7 +171,7 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture:$this->couponPicture, + couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, products: $this->products, @@ -174,7 +180,7 @@ describe('requested coupon', function (): void { ); // THEN - expect($coupon->warningThresholdQuantity)->toBeNull() + expect($coupon->warningThresholdQuantity)->toEqual(new WarningThresholdQuantity(100)) ->and($coupon->products)->toBe($this->products) ->and($coupon->printRules)->toBe($this->printRules) ->and($coupon->gs1)->toBe($this->gs1); @@ -188,7 +194,7 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture:$this->couponPicture, + couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, ); @@ -197,28 +203,21 @@ describe('requested coupon', function (): void { expect($coupon->products)->toBe([]) ->and($coupon->printRules)->toBeNull() ->and($coupon->gs1)->toBeNull() - ->and($coupon->warningThresholdQuantity)->toBeNull(); + ->and($coupon->warningThresholdQuantity)->toEqual(new WarningThresholdQuantity(100)); }); - it('accepts an empty validity information list', function (): void { - // GIVEN / WHEN - $coupon = new RequestedCoupon( - $this->name, - $this->quantity, - $this->diffusion, - $this->moneyAmount, - $this->processingCode, - $this->couponPicture, - $this->couponValidity, - [], - $this->products, - $this->printRules, - $this->gs1, - $this->warningThresholdQuantity - ); - - // THEN - expect($coupon->couponValidityInformation)->toBe([]); + it('throws when there is no validity information for france', function (): void { + // GIVEN / WHEN / THEN + expect(fn () => new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $this->moneyAmount, + processingCode: $this->processingCode, + couponPicture: $this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: [], + ))->toThrow(\DomainException::class, 'A coupon requires validity information for France.'); }); it('throws when validity information contains an invalid item', function (): void { @@ -238,4 +237,119 @@ describe('requested coupon', function (): void { $this->warningThresholdQuantity ))->toThrow(\DomainException::class, 'Coupon validity information must be CouponValidityInformation instances.'); }); + + it('accepts the minimum face value of 0.01€', function (): void { + // GIVEN + $moneyAmount = new MoneyAmount(new Money(Currency::EUR, 1)); + + // WHEN + $coupon = new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $moneyAmount, + processingCode: $this->processingCode, + couponPicture: $this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: $this->couponValidityInformation, + ); + + // THEN + expect($coupon->moneyAmount)->toBe($moneyAmount); + }); + + it('accepts the maximum face value of 3.00€', function (): void { + // GIVEN + $moneyAmount = new MoneyAmount(new Money(Currency::EUR, 300)); + + // WHEN + $coupon = new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $moneyAmount, + processingCode: $this->processingCode, + couponPicture: $this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: $this->couponValidityInformation, + ); + + // THEN + expect($coupon->moneyAmount)->toBe($moneyAmount); + }); + + it('throws when the face value is zero', function (): void { + // GIVEN / WHEN / THEN + expect(fn () => new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: new MoneyAmount(new Money(Currency::EUR, 0)), + processingCode: $this->processingCode, + couponPicture: $this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: $this->couponValidityInformation, + ))->toThrow(\DomainException::class, 'Coupon face value must be between 0.01€ and 3.00€'); + }); + + it('throws when the face value is above 3.00€', function (): void { + // GIVEN / WHEN / THEN + expect(fn () => new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: new MoneyAmount(new Money(Currency::EUR, 301)), + processingCode: $this->processingCode, + couponPicture: $this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: $this->couponValidityInformation, + ))->toThrow(\DomainException::class, 'Coupon face value must be between 0.01€ and 3.00€'); + }); + + it('throws when a gs1 coupon has no benelux validity information', function (): void { + // GIVEN / WHEN / THEN + expect(fn () => new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $this->moneyAmount, + processingCode: $this->processingCode, + couponPicture: $this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: [$this->franceValidityInformation], + gs1: $this->gs1, + ))->toThrow(\DomainException::class, 'A GS1 coupon requires validity information for Benelux.'); + }); + + it('throws when a gs1 coupon has only benelux validity information', function (): void { + // GIVEN / WHEN / THEN + expect(fn () => new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $this->moneyAmount, + processingCode: $this->processingCode, + couponPicture: $this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: [$this->beneluxValidityInformation], + gs1: $this->gs1, + ))->toThrow(\DomainException::class, 'A coupon requires validity information for France.'); + }); + + it('accepts a coupon without gs1 and without benelux validity information', function (): void { + // GIVEN / WHEN + $coupon = new RequestedCoupon( + name: $this->name, + quantity: $this->quantity, + diffusion: $this->diffusion, + moneyAmount: $this->moneyAmount, + processingCode: $this->processingCode, + couponPicture: $this->couponPicture, + couponValidity: $this->couponValidity, + couponValidityInformation: [$this->franceValidityInformation], + ); + + // THEN + expect($coupon->gs1)->toBeNull(); + }); }); From a9147e27c72d07dd320d902e0ad6ef41a86a317e Mon Sep 17 00:00:00 2001 From: K Muller Date: Fri, 2 Oct 2026 11:23:41 +0200 Subject: [PATCH 50/52] fix: :bug: some fixes on the structure in sprint2 --- src/Domain/Models/Entities/Campaign.php | 6 ++ src/Domain/Models/Entities/Coupon.php | 10 +- src/Domain/Models/Entities/Tag.php | 61 ------------ .../ValueObjects/Coupon/CouponDiffusion.php | 12 +-- .../RequestedCoupon/RequestedCoupon.php | 2 - src/Domain/Models/ValueObjects/Tag/TagId.php | 16 --- .../Models/ValueObjects/Tag/TagName.php | 16 --- .../Create/CreateCampaignRequestTest.php | 7 -- .../Find/FindCampaignRequestTest.php | 6 -- .../List/ListCampaignsRequestTest.php | 12 --- .../Reject/RejectCampaignRequestTest.php | 7 -- .../Models/Entities/CampaignRequestTest.php | 7 -- .../Domain/Models/Entities/CampaignTest.php | 20 +++- .../Domain/Models/Entities/CouponTest.php | 38 +++---- tests/Unit/Domain/Models/Entities/TagTest.php | 99 ------------------- .../Coupon/CouponDiffusionTest.php | 34 ++----- .../RequestedCoupon/RequestedCouponTest.php | 23 ----- .../Models/ValueObjects/Tag/TagIdTest.php | 28 ------ .../Models/ValueObjects/Tag/TagNameTest.php | 33 ------- 19 files changed, 52 insertions(+), 385 deletions(-) delete mode 100644 src/Domain/Models/Entities/Tag.php delete mode 100644 src/Domain/Models/ValueObjects/Tag/TagId.php delete mode 100644 src/Domain/Models/ValueObjects/Tag/TagName.php delete mode 100644 tests/Unit/Domain/Models/Entities/TagTest.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/Tag/TagIdTest.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php index 9df15bc..a52ae3a 100644 --- a/src/Domain/Models/Entities/Campaign.php +++ b/src/Domain/Models/Entities/Campaign.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Domain\Models\Entities; +use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; @@ -28,6 +29,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier public bool $printAllAtOnce, public array $coupons, public BrandCode $brandCode, + public ActorId $actorId, public ?\DateTimeInterface $updatedAt = null, ) { foreach ($this->coupons as $coupon) { @@ -62,6 +64,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier bool $printAllAtOnce, array $coupons, BrandCode $brandCode, + ActorId $actorId, ?\DateTimeInterface $updatedAt = null, ): Campaign { return new self( @@ -72,6 +75,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier printAllAtOnce: $printAllAtOnce, coupons: $coupons, brandCode: $brandCode, + actorId: $actorId, updatedAt: $updatedAt, ); } @@ -87,6 +91,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier bool $printAllAtOnce, array $coupons, BrandCode $brandCode, + ActorId $actorId, ?\DateTimeInterface $updatedAt = null, ): self { return new self( @@ -97,6 +102,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier printAllAtOnce: $printAllAtOnce, coupons: $coupons, brandCode: $brandCode, + actorId: $actorId, updatedAt: $updatedAt, ); } diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index c3075f7..f879cd9 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -47,7 +47,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier public ActorId $createdBy, public array $products = [], public ?CouponAvailabilityCalendar $availabilityCalendar = null, - public ?GS1 $gtin = null, + public ?GS1 $gs1 = null, public ?\DateTimeInterface $updatedAt = null, ) { foreach ($this->couponValidityInformation as $validityInformation) { @@ -97,7 +97,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier ActorId $createdBy, array $products = [], ?CouponAvailabilityCalendar $availabilityCalendar = null, - ?GS1 $gtin = null, + ?GS1 $gs1 = null, ?\DateTimeInterface $updatedAt = null, ): Coupon { return new self( @@ -115,7 +115,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier createdBy: $createdBy, products: $products, availabilityCalendar: $availabilityCalendar, - gtin: $gtin, + gs1: $gs1, updatedAt: $updatedAt, ); } @@ -140,7 +140,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier ActorId $createdBy, array $products = [], ?CouponAvailabilityCalendar $availabilityCalendar = null, - ?GS1 $gtin = null, + ?GS1 $gs1 = null, ?\DateTimeInterface $updatedAt = null, ): self { return new self( @@ -158,7 +158,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier createdBy: $createdBy, products: $products, availabilityCalendar: $availabilityCalendar, - gtin: $gtin, + gs1: $gs1, updatedAt: $updatedAt, ); } diff --git a/src/Domain/Models/Entities/Tag.php b/src/Domain/Models/Entities/Tag.php deleted file mode 100644 index bb8e084..0000000 --- a/src/Domain/Models/Entities/Tag.php +++ /dev/null @@ -1,61 +0,0 @@ - - */ -final readonly class Tag implements AggregateRoot, HasTimestampIdentifier -{ - private function __construct( - public TagId $id, - public TagName $value, - public ?\DateTimeInterface $updatedAt = null, - ) { - } - - public function id(): TagId - { - return $this->id; - } - - public function createdAt(): \DateTimeInterface - { - return $this->id->toDateTime(); - } - - public function updatedAt(): ?\DateTimeInterface - { - return $this->updatedAt; - } - - public static function create( - TagName $value, - ?\DateTimeInterface $updatedAt = null, - ): Tag { - return new self( - id: TagId::generate(), - value: $value, - updatedAt: $updatedAt, - ); - } - - public static function hydrate( - TagId $id, - TagName $value, - ?\DateTimeInterface $updatedAt = null, - ): self { - return new self( - id: $id, - value: $value, - updatedAt: $updatedAt, - ); - } -} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php b/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php index 2fef55a..14f5690 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php @@ -9,17 +9,7 @@ use Domain\Models\ValueObjects\Period; final readonly class CouponDiffusion { public function __construct( - /** @var Period[] */ - public array $periods, + public Period $period, ) { - 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/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php index c85f37d..1e2b565 100644 --- a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -7,7 +7,6 @@ namespace Domain\Models\ValueObjects\RequestedCoupon; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\CouponPicture; use Domain\Models\ValueObjects\GS1; use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; @@ -35,7 +34,6 @@ final readonly class RequestedCoupon public Period $diffusion, public MoneyAmount $moneyAmount, public ProcessingCode $processingCode, - public CouponPicture $couponPicture, public CouponValidity $couponValidity, public array $couponValidityInformation, public array $products = [], diff --git a/src/Domain/Models/ValueObjects/Tag/TagId.php b/src/Domain/Models/ValueObjects/Tag/TagId.php deleted file mode 100644 index 9ae28e2..0000000 --- a/src/Domain/Models/ValueObjects/Tag/TagId.php +++ /dev/null @@ -1,16 +0,0 @@ -value) === '') { - throw new \DomainException('Tag name cannot be empty.'); - } - } -} diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php index af5ea21..e7d52ea 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php @@ -18,11 +18,9 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\CouponPicture; use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; -use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; @@ -49,11 +47,6 @@ beforeEach(function (): void { diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)), processingCode: new ProcessingCode('PC1'), - couponPicture: new CouponPicture( - new PicturePath('coupons/product.jpg'), - new PicturePath('coupons/thumbnail.jpg'), - new PicturePath('coupons/inactive_thumbnail.jpg') - ), couponValidity: new CouponValidity(30, new DateTime('2026-02-01')), couponValidityInformation: [ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 715a98a..2173f21 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -26,7 +26,6 @@ use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\CouponPicture; use Domain\Models\ValueObjects\GS1; use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; @@ -56,11 +55,6 @@ describe('find campaign request use case', function (): void { new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), - new CouponPicture( - new PicturePath('coupons/product.jpg'), - new PicturePath('coupons/thumbnail.jpg'), - new PicturePath('coupons/inactive_thumbnail.jpg') - ), new CouponValidity(30, new DateTime('2026-02-01')), [ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 119c54f..7699b81 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -20,12 +20,10 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\CouponPicture; use Domain\Models\ValueObjects\GS1; use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; -use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; @@ -49,11 +47,6 @@ describe('list campaigns request use case', function (): void { new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), - new CouponPicture( - new PicturePath('coupons/product.jpg'), - new PicturePath('coupons/thumbnail.jpg'), - new PicturePath('coupons/inactive_thumbnail.jpg') - ), new CouponValidity(30, new DateTime('2026-02-01')), [ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -71,11 +64,6 @@ describe('list campaigns request use case', function (): void { new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC2'), - new CouponPicture( - new PicturePath('coupons/product.jpg'), - new PicturePath('coupons/thumbnail.jpg'), - new PicturePath('coupons/inactive_thumbnail.jpg') - ), new CouponValidity(30, new DateTime('2026-02-01')), [ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 780085d..d0cf988 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -23,12 +23,10 @@ use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\CouponPicture; use Domain\Models\ValueObjects\GS1; use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; -use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; @@ -50,11 +48,6 @@ describe('reject campaign request use case', function (): void { new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), - new CouponPicture( - new PicturePath('coupons/product.jpg'), - new PicturePath('coupons/thumbnail.jpg'), - new PicturePath('coupons/inactive_thumbnail.jpg') - ), new CouponValidity(30, new DateTime('2026-02-01')), [ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 0dbcba0..1e18321 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -18,12 +18,10 @@ use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\CouponPicture; use Domain\Models\ValueObjects\GS1; use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; -use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; @@ -45,11 +43,6 @@ describe('campaign request', function (): void { new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), new MoneyAmount(new Money(Currency::EUR, 300)), new ProcessingCode('PC1'), - new CouponPicture( - new PicturePath('coupons/product.jpg'), - new PicturePath('coupons/thumbnail.jpg'), - new PicturePath('coupons/inactive_thumbnail.jpg') - ), new CouponValidity(30, new DateTime('2026-02-01')), [ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index 35eb58a..0718b78 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -7,6 +7,7 @@ namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; use Shared\Enums\Country; use Domain\Models\Entities\Campaign; +use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; @@ -36,6 +37,7 @@ describe('campaign', function (): void { ) ]; $this->brandCode = new BrandCode('AAA01'); + $this->actorId = ActorId::generate(); }); afterEach(function (): void { @@ -55,6 +57,7 @@ describe('campaign', function (): void { $this->printAllAtOnce, $this->coupons, $this->brandCode, + $this->actorId ); // THEN @@ -82,6 +85,7 @@ describe('campaign', function (): void { $this->printAllAtOnce, [], $this->brandCode, + $this->actorId ); // THEN @@ -102,6 +106,7 @@ describe('campaign', function (): void { $this->printAllAtOnce, $this->coupons, $this->brandCode, + $this->actorId, $updatedAt ); @@ -118,7 +123,8 @@ describe('campaign', function (): void { $this->publication, $this->printAllAtOnce, $this->coupons, - $this->brandCode + $this->brandCode, + $this->actorId ); // THEN @@ -141,7 +147,8 @@ describe('campaign', function (): void { $this->publication, $this->printAllAtOnce, $this->coupons, - $this->brandCode + $this->brandCode, + $this->actorId ); $secondCampaign = Campaign::create( $this->code, @@ -149,7 +156,8 @@ describe('campaign', function (): void { $this->publication, $this->printAllAtOnce, $this->coupons, - $this->brandCode + $this->brandCode, + $this->actorId ); // THEN @@ -167,7 +175,8 @@ describe('campaign', function (): void { $this->publication, $this->printAllAtOnce, $this->coupons, - $this->brandCode + $this->brandCode, + $this->actorId ); // WHEN @@ -190,7 +199,8 @@ describe('campaign', function (): void { $this->publication, $this->printAllAtOnce, $coupons, - $this->brandCode + $this->brandCode, + $this->actorId ))->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 2e03c7d..6b54f95 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -42,7 +42,7 @@ describe('coupon', function (): void { new PicturePath('files/thumbnail.jpg'), new PicturePath('files/inactive_thumbnail.jpg') ); - $this->diffusion = new CouponDiffusion([new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))]); + $this->diffusion = new CouponDiffusion(new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))); $this->validity = new CouponValidity(30, new DateTime('2026-02-01')); $this->couponValidityInformation = [ new CouponValidityInformation( @@ -61,7 +61,7 @@ describe('coupon', function (): void { $this->createdBy = ActorId::generate(); $this->products = [new IncoCode(35784), new IncoCode(15522)]; $this->availabilityCalendar = new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]); - $this->gtin = new GS1('110101010', BarcodeFormat::EAN13); + $this->gs1 = new GS1('110101010', BarcodeFormat::EAN13); }); afterEach(function (): void { @@ -88,7 +88,7 @@ describe('coupon', function (): void { $this->createdBy, $this->products, $this->availabilityCalendar, - $this->gtin, + $this->gs1, ); // THEN @@ -106,7 +106,7 @@ describe('coupon', function (): void { ->and($coupon->createdBy)->toBe($this->createdBy) ->and($coupon->products)->toBe($this->products) ->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar) - ->and($coupon->gtin)->toBe($this->gtin) + ->and($coupon->gs1)->toBe($this->gs1) ->and($coupon->updatedAt)->toBeNull() ->and($coupon->updatedAt())->toBeNull(); }); @@ -136,7 +136,7 @@ describe('coupon', function (): void { // THEN expect($coupon->products)->toBe($this->products) ->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar) - ->and($coupon->gtin)->toBeNull(); + ->and($coupon->gs1)->toBeNull(); }); it('hydrates a coupon without availability calendar', function (): void { @@ -158,13 +158,13 @@ describe('coupon', function (): void { brandCode: $this->brandCode, createdBy: $this->createdBy, products: $this->products, - gtin: $this->gtin + gs1: $this->gs1 ); // THEN expect($coupon->availabilityCalendar)->toBeNull() ->and($coupon->products)->toBe($this->products) - ->and($coupon->gtin)->toBe($this->gtin); + ->and($coupon->gs1)->toBe($this->gs1); }); it('hydrates a coupon without products', function (): void { @@ -186,13 +186,13 @@ describe('coupon', function (): void { brandCode: $this->brandCode, createdBy: $this->createdBy, availabilityCalendar: $this->availabilityCalendar, - gtin: $this->gtin + gs1: $this->gs1 ); // THEN expect($coupon->products)->toBe([]) ->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar) - ->and($coupon->gtin)->toBe($this->gtin); + ->and($coupon->gs1)->toBe($this->gs1); }); it('hydrates a coupon with only required fields', function (): void { @@ -219,7 +219,7 @@ describe('coupon', function (): void { expect($coupon->id())->toBe($id) ->and($coupon->products)->toBe([]) ->and($coupon->availabilityCalendar)->toBeNull() - ->and($coupon->gtin)->toBeNull() + ->and($coupon->gs1)->toBeNull() ->and($coupon->updatedAt)->toBeNull() ->and($coupon->updatedAt())->toBeNull(); }); @@ -244,7 +244,7 @@ describe('coupon', function (): void { $this->createdBy, $this->products, $this->availabilityCalendar, - $this->gtin, + $this->gs1, ); // THEN @@ -272,7 +272,7 @@ describe('coupon', function (): void { $this->createdBy, $this->products, $this->availabilityCalendar, - $this->gtin, + $this->gs1, ))->toThrow(\DomainException::class, 'Coupon validity information must be CouponValidityInformation instances.'); }); @@ -296,7 +296,7 @@ describe('coupon', function (): void { $this->createdBy, $this->products, $this->availabilityCalendar, - $this->gtin, + $this->gs1, ))->toThrow(\DomainException::class, 'Coupon legal information must be CouponLegalInformation instances.'); }); @@ -321,7 +321,7 @@ describe('coupon', function (): void { $this->createdBy, $this->products, $this->availabilityCalendar, - $this->gtin, + $this->gs1, $updatedAt ); @@ -346,7 +346,7 @@ describe('coupon', function (): void { $this->createdBy, $this->products, $this->availabilityCalendar, - $this->gtin, + $this->gs1, ); // THEN @@ -364,7 +364,7 @@ describe('coupon', function (): void { ->and($coupon->createdBy)->toBe($this->createdBy) ->and($coupon->products)->toBe($this->products) ->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar) - ->and($coupon->gtin)->toBe($this->gtin) + ->and($coupon->gs1)->toBe($this->gs1) ->and($coupon->updatedAt)->toBeNull() ->and($coupon->updatedAt())->toBeNull(); }); @@ -385,7 +385,7 @@ describe('coupon', function (): void { $this->createdBy, $this->products, $this->availabilityCalendar, - $this->gtin, + $this->gs1, ); $secondCoupon = Coupon::create( $this->code, @@ -401,7 +401,7 @@ describe('coupon', function (): void { $this->createdBy, $this->products, $this->availabilityCalendar, - $this->gtin, + $this->gs1, ); // THEN @@ -427,7 +427,7 @@ describe('coupon', function (): void { $this->createdBy, $this->products, $this->availabilityCalendar, - $this->gtin, + $this->gs1, ); // WHEN diff --git a/tests/Unit/Domain/Models/Entities/TagTest.php b/tests/Unit/Domain/Models/Entities/TagTest.php deleted file mode 100644 index b7c3d71..0000000 --- a/tests/Unit/Domain/Models/Entities/TagTest.php +++ /dev/null @@ -1,99 +0,0 @@ -value = new TagName('Name'); - }); - - afterEach(function (): void { - CarbonImmutable::setTestNow(); - }); - - it('creates a tag', function (): void { - // GIVEN - $id = TagId::generate(); - - // WHEN - $tag = Tag::hydrate( - $id, - $this->value, - ); - - // THEN - expect($tag->id())->toBe($id) - ->and($tag->value)->toBe($this->value) - ->and($tag->updatedAt)->toBeNull() - ->and($tag->updatedAt())->toBeNull(); - }); - - it('creates a tag with an explicit updatedAt date', function (): void { - // GIVEN - $id = TagId::generate(); - $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); - - // WHEN - $tag = Tag::hydrate( - $id, - $this->value, - $updatedAt - ); - - // THEN - expect($tag->updatedAt)->toBe($updatedAt) - ->and($tag->updatedAt())->toBe($updatedAt); - }); - - it('creates a new tag with a freshly generated id', function (): void { - // GIVEN / WHEN - $tag = Tag::create( - $this->value, - ); - - // THEN - expect($tag->id())->toBeInstanceOf(TagId::class) - ->and($tag->value)->toBe($this->value) - ->and($tag->updatedAt)->toBeNull() - ->and($tag->updatedAt())->toBeNull(); - }); - - it('generates a different id for each created tag', function (): void { - // GIVEN / WHEN - $firstTag = Tag::create( - $this->value - ); - $secondTag = Tag::create( - $this->value - ); - - // THEN - expect($firstTag->id()->equals($secondTag->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 TagId(Uuid::uuid7($expected)); - $tag = Tag::hydrate( - $id, - $this->value, - ); - - // WHEN - $result = $tag->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/CouponDiffusionTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php index 15f0745..f9e19dc 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php @@ -13,45 +13,23 @@ covers(CouponDiffusion::class); describe('coupon diffusion', function (): void { it('creates a valid coupon diffusion with one period', function (): void { // GIVEN - $periods = [ - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - ]; + $period = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); // WHEN - $couponDiffusion = new CouponDiffusion($periods); + $couponDiffusion = new CouponDiffusion($period); // THEN - expect($couponDiffusion->periods)->toBe($periods); + expect($couponDiffusion->period)->toBe($period); }); 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')), - ]; + $period = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); // WHEN - $couponDiffusion = new CouponDiffusion($periods); + $couponDiffusion = new CouponDiffusion($period); // 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.'); + expect($couponDiffusion->period)->toBe($period); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index c467cb1..e3d3efc 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -10,12 +10,10 @@ use Domain\Enums\BarcodeFormat; use Domain\Models\ValueObjects\Coupon\CouponValidity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\CouponPicture; use Domain\Models\ValueObjects\GS1; use Domain\Models\ValueObjects\IncoCode; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; -use Domain\Models\ValueObjects\PicturePath; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; @@ -34,11 +32,6 @@ describe('requested coupon', function (): void { $this->diffusion = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); $this->moneyAmount = new MoneyAmount(new Money(Currency::EUR, 300)); $this->processingCode = new ProcessingCode('PC1'); - $this->couponPicture = new CouponPicture( - new PicturePath('coupons/product.jpg'), - new PicturePath('coupons/thumbnail.jpg'), - new PicturePath('coupons/inactive_thumbnail.jpg') - ); $this->couponValidity = new CouponValidity(30, new DateTime('2026-02-01')); $this->franceValidityInformation = new CouponValidityInformation( 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', @@ -70,7 +63,6 @@ describe('requested coupon', function (): void { $this->diffusion, $this->moneyAmount, $this->processingCode, - $this->couponPicture, $this->couponValidity, $this->couponValidityInformation, $this->products, @@ -85,7 +77,6 @@ describe('requested coupon', function (): void { ->and($coupon->diffusion)->toBe($this->diffusion) ->and($coupon->moneyAmount)->toBe($this->moneyAmount) ->and($coupon->processingCode)->toBe($this->processingCode) - ->and($coupon->couponPicture)->toBe($this->couponPicture) ->and($coupon->couponValidity)->toBe($this->couponValidity) ->and($coupon->couponValidityInformation)->toBe($this->couponValidityInformation) ->and($coupon->products)->toBe($this->products) @@ -102,7 +93,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, printRules: $this->printRules, @@ -125,7 +115,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, products: $this->products, @@ -148,7 +137,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, products: $this->products, @@ -171,7 +159,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, products: $this->products, @@ -194,7 +181,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, ); @@ -214,7 +200,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: [], ))->toThrow(\DomainException::class, 'A coupon requires validity information for France.'); @@ -228,7 +213,6 @@ describe('requested coupon', function (): void { $this->diffusion, $this->moneyAmount, $this->processingCode, - $this->couponPicture, $this->couponValidity, ['Not a CouponValidityInformation'], $this->products, @@ -249,7 +233,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, ); @@ -269,7 +252,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, ); @@ -286,7 +268,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: new MoneyAmount(new Money(Currency::EUR, 0)), processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, ))->toThrow(\DomainException::class, 'Coupon face value must be between 0.01€ and 3.00€'); @@ -300,7 +281,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: new MoneyAmount(new Money(Currency::EUR, 301)), processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, ))->toThrow(\DomainException::class, 'Coupon face value must be between 0.01€ and 3.00€'); @@ -314,7 +294,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: [$this->franceValidityInformation], gs1: $this->gs1, @@ -329,7 +308,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: [$this->beneluxValidityInformation], gs1: $this->gs1, @@ -344,7 +322,6 @@ describe('requested coupon', function (): void { diffusion: $this->diffusion, moneyAmount: $this->moneyAmount, processingCode: $this->processingCode, - couponPicture: $this->couponPicture, couponValidity: $this->couponValidity, couponValidityInformation: [$this->franceValidityInformation], ); diff --git a/tests/Unit/Domain/Models/ValueObjects/Tag/TagIdTest.php b/tests/Unit/Domain/Models/ValueObjects/Tag/TagIdTest.php deleted file mode 100644 index 298d233..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/Tag/TagIdTest.php +++ /dev/null @@ -1,28 +0,0 @@ -toBeInstanceOf(TagId::class); - }); - - it('generates different ids on each call', function (): void { - // WHEN - $first = TagId::generate(); - $second = TagId::generate(); - - // THEN - expect((string) $first)->not->toBe((string) $second); - }); -}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php deleted file mode 100644 index 2a236b4..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/Tag/TagNameTest.php +++ /dev/null @@ -1,33 +0,0 @@ -value = 'Name'; - $this->emptyValue = ''; - }); - - it('creates a valid tag name', function (): void { - // GIVEN / WHEN - $name = new TagName( - $this->value - ); - - // THEN - expect($name->value)->toBe($this->value); - }); - - it('rejects invalid tag name', function (): void { - // GIVEN / WHEN / THEN - expect(fn (): TagName => new TagName( - $this->emptyValue - ))->toThrow(\DomainException::class); - }); -}); From 871ceade05f0154dc6ade6afba42b0349ef75b7e Mon Sep 17 00:00:00 2001 From: K Muller Date: Fri, 2 Oct 2026 11:33:03 +0200 Subject: [PATCH 51/52] fix: :bug: new structures and fix linters --- .../ValueObjects/Campaign/CampaignCoupon.php | 2 - .../RequestedCoupon/RequestedCoupon.php | 7 --- .../ValueObjects/WarningThresholdQuantity.php | 16 ------ .../Create/CreateCampaignRequestTest.php | 4 +- .../Find/FindCampaignRequestTest.php | 4 +- .../List/ListCampaignsRequestTest.php | 7 +-- .../Reject/RejectCampaignRequestTest.php | 4 +- .../Models/Entities/CampaignRequestTest.php | 4 +- .../Domain/Models/Entities/CampaignTest.php | 2 - .../Campaign/CampaignCouponTest.php | 8 +-- .../RequestedCoupon/RequestedCouponTest.php | 52 ++++--------------- .../WarningThresholdQuantityTest.php | 48 ----------------- 12 files changed, 16 insertions(+), 142 deletions(-) delete mode 100644 src/Domain/Models/ValueObjects/WarningThresholdQuantity.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php index e6ae5f4..ba56632 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php @@ -7,7 +7,6 @@ namespace Domain\Models\ValueObjects\Campaign; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Domain\Models\ValueObjects\WarningThresholdQuantity; use Shared\Enums\Country; final readonly class CampaignCoupon @@ -16,7 +15,6 @@ final readonly class CampaignCoupon public ProcessingCode $processingCode, public CouponQuantity $quantity, public Country $availableIn, - public ?WarningThresholdQuantity $warningThresholdQuantity = null, public ?PrintRules $printRules = null, ) { } diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php index 1e2b565..8fc0c3c 100644 --- a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -13,17 +13,13 @@ use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Domain\Models\ValueObjects\WarningThresholdQuantity; use Shared\Enums\Country; final readonly class RequestedCoupon { - private const int DEFAULT_WARNING_THRESHOLD_QUANTITY = 100; private const int MIN_FACE_VALUE_IN_CENTS = 1; private const int MAX_FACE_VALUE_IN_CENTS = 300; - public WarningThresholdQuantity $warningThresholdQuantity; - /** * @param array $couponValidityInformation * @param array $products @@ -39,7 +35,6 @@ final readonly class RequestedCoupon public array $products = [], public ?PrintRules $printRules = null, public ?GS1 $gs1 = null, - ?WarningThresholdQuantity $warningThresholdQuantity = null, ) { $money = $this->moneyAmount->value; @@ -63,8 +58,6 @@ final readonly class RequestedCoupon if ($this->gs1 instanceof GS1 && !$this->hasValidityInformationFor(Country::BENELUX)) { throw new \DomainException('A GS1 coupon requires validity information for Benelux.'); } - - $this->warningThresholdQuantity = $warningThresholdQuantity ?? new WarningThresholdQuantity(self::DEFAULT_WARNING_THRESHOLD_QUANTITY); } private function hasValidityInformationFor(Country $country): bool diff --git a/src/Domain/Models/ValueObjects/WarningThresholdQuantity.php b/src/Domain/Models/ValueObjects/WarningThresholdQuantity.php deleted file mode 100644 index 682608e..0000000 --- a/src/Domain/Models/ValueObjects/WarningThresholdQuantity.php +++ /dev/null @@ -1,16 +0,0 @@ -value < 0) { - throw new \DomainException('Warning threshold quantity cannot be negative.'); - } - } -} diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php index e7d52ea..efd146e 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php @@ -25,7 +25,6 @@ use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; -use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\Enums\Country; @@ -52,8 +51,7 @@ beforeEach(function (): void { new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), ], products: [new IncoCode(35784), new IncoCode(15522)], - printRules: new PrintRules(maxPerCustomer: 3), - warningThresholdQuantity: new WarningThresholdQuantity(10), + printRules: new PrintRules(maxPerCustomer: 3) ), ); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 2173f21..912dc8d 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -35,7 +35,6 @@ use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; -use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\DDD\UseCases\Input as BaseInput; @@ -62,8 +61,7 @@ describe('find campaign request use case', function (): void { ], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), - new GS1('01234567890128', BarcodeFormat::EAN13), - new WarningThresholdQuantity(10) + new GS1('01234567890128', BarcodeFormat::EAN13) ); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index 7699b81..aa717b2 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -28,7 +28,6 @@ use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; -use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\Enums\Country; use Shared\Enums\Currency; @@ -54,8 +53,7 @@ describe('list campaigns request use case', function (): void { ], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), - new GS1('01234567890128', BarcodeFormat::EAN13), - new WarningThresholdQuantity(10) + new GS1('01234567890128', BarcodeFormat::EAN13) ); $this->couponB = new RequestedCoupon( @@ -71,8 +69,7 @@ describe('list campaigns request use case', function (): void { ], [new IncoCode(24514), new IncoCode(46259)], new PrintRules(maxPerCustomer: 3), - new GS1('01234567890128', BarcodeFormat::EAN13), - new WarningThresholdQuantity(10) + new GS1('01234567890128', BarcodeFormat::EAN13) ); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index d0cf988..5cc0ccc 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -31,7 +31,6 @@ use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; -use Domain\Models\ValueObjects\WarningThresholdQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; use Shared\Enums\Country; use Shared\Enums\Currency; @@ -55,8 +54,7 @@ describe('reject campaign request use case', function (): void { ], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), - new GS1('01234567890128', BarcodeFormat::EAN13), - new WarningThresholdQuantity(10) + new GS1('01234567890128', BarcodeFormat::EAN13) ); }); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 1e18321..e1b936f 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -26,7 +26,6 @@ use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; -use Domain\Models\ValueObjects\WarningThresholdQuantity; use Ramsey\Uuid\Uuid; use Shared\Enums\Country; use Shared\Enums\Currency; @@ -50,8 +49,7 @@ describe('campaign request', function (): void { ], [new IncoCode(35784), new IncoCode(15522)], new PrintRules(maxPerCustomer: 3), - new GS1('01234567890128', BarcodeFormat::EAN13), - new WarningThresholdQuantity(10) + new GS1('01234567890128', BarcodeFormat::EAN13) ); $this->code = new BrandCode('AAA01'); $this->name = new CampaignName('Campaign A'); diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index 0718b78..403200c 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -16,7 +16,6 @@ use Domain\Models\ValueObjects\Campaign\CampaignName; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Domain\Models\ValueObjects\WarningThresholdQuantity; use Ramsey\Uuid\Uuid; covers(Campaign::class); @@ -32,7 +31,6 @@ describe('campaign', function (): void { new ProcessingCode('PC1'), new CouponQuantity(100), Country::FRANCE, - new WarningThresholdQuantity(100), new PrintRules(maxPerCustomer: 3) ) ]; diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php index be7fc49..9938215 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php @@ -8,7 +8,6 @@ use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; -use Domain\Models\ValueObjects\WarningThresholdQuantity; use Shared\Enums\Country; covers(CampaignCoupon::class); @@ -18,7 +17,6 @@ describe('campaign coupon', function (): void { $this->processingCode = new ProcessingCode('PC1'); $this->quantity = new CouponQuantity(10); $this->availableIn = Country::FRANCE; - $this->warningThresholdQuantity = new WarningThresholdQuantity(100); $this->printRules = new PrintRules(maxPerCustomer: 3); }); @@ -28,7 +26,6 @@ describe('campaign coupon', function (): void { $this->processingCode, $this->quantity, $this->availableIn, - $this->warningThresholdQuantity, $this->printRules ); @@ -36,7 +33,6 @@ describe('campaign coupon', function (): void { expect($campaignCoupon->processingCode)->toBe($this->processingCode) ->and($campaignCoupon->quantity)->toBe($this->quantity) ->and($campaignCoupon->availableIn)->toBe($this->availableIn) - ->and($campaignCoupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity) ->and($campaignCoupon->printRules)->toBe($this->printRules); }); @@ -62,14 +58,12 @@ describe('campaign coupon', function (): void { processingCode: $this->processingCode, quantity: $this->quantity, availableIn: $this->availableIn, - warningThresholdQuantity: $this->warningThresholdQuantity ); // THEN expect($campaignCoupon->processingCode)->toBe($this->processingCode) ->and($campaignCoupon->quantity)->toBe($this->quantity) - ->and($campaignCoupon->availableIn)->toBe($this->availableIn) - ->and($campaignCoupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); + ->and($campaignCoupon->availableIn)->toBe($this->availableIn); }); it('creates a valid campaign coupon whitout warning threshold quantity and print rules', function (): void { diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index e3d3efc..111a145 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -18,7 +18,6 @@ use Domain\Models\ValueObjects\PrintRules; use Domain\Models\ValueObjects\ProcessingCode; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; -use Domain\Models\ValueObjects\WarningThresholdQuantity; use Shared\Enums\Country; use Shared\Enums\Currency; use Shared\ValueObjects\Money; @@ -48,7 +47,6 @@ describe('requested coupon', function (): void { $this->products = [new IncoCode(35784), new IncoCode(15522)]; $this->printRules = new PrintRules(maxPerCustomer: 3); $this->gs1 = new GS1('01234567890128', BarcodeFormat::EAN13); - $this->warningThresholdQuantity = new WarningThresholdQuantity(10); }); afterEach(function (): void { @@ -68,7 +66,6 @@ describe('requested coupon', function (): void { $this->products, $this->printRules, $this->gs1, - $this->warningThresholdQuantity ); // THEN @@ -81,8 +78,7 @@ describe('requested coupon', function (): void { ->and($coupon->couponValidityInformation)->toBe($this->couponValidityInformation) ->and($coupon->products)->toBe($this->products) ->and($coupon->printRules)->toBe($this->printRules) - ->and($coupon->gs1)->toBe($this->gs1) - ->and($coupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); + ->and($coupon->gs1)->toBe($this->gs1); }); it('hydrates a requested coupon without products', function (): void { @@ -96,15 +92,13 @@ describe('requested coupon', function (): void { couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, printRules: $this->printRules, - gs1: $this->gs1, - warningThresholdQuantity: $this->warningThresholdQuantity + gs1: $this->gs1 ); // THEN expect($coupon->products)->toBe([]) ->and($coupon->printRules)->toBe($this->printRules) - ->and($coupon->gs1)->toBe($this->gs1) - ->and($coupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); + ->and($coupon->gs1)->toBe($this->gs1); }); it('hydrates a requested coupon without print rules', function (): void { @@ -118,15 +112,13 @@ describe('requested coupon', function (): void { couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, products: $this->products, - gs1: $this->gs1, - warningThresholdQuantity: $this->warningThresholdQuantity + gs1: $this->gs1 ); // THEN expect($coupon->printRules)->toBeNull() ->and($coupon->products)->toBe($this->products) - ->and($coupon->gs1)->toBe($this->gs1) - ->and($coupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); + ->and($coupon->gs1)->toBe($this->gs1); }); it('hydrates a requested coupon without gs1', function (): void { @@ -140,37 +132,13 @@ describe('requested coupon', function (): void { couponValidity: $this->couponValidity, couponValidityInformation: $this->couponValidityInformation, products: $this->products, - printRules: $this->printRules, - warningThresholdQuantity: $this->warningThresholdQuantity + printRules: $this->printRules ); // THEN expect($coupon->gs1)->toBeNull() ->and($coupon->products)->toBe($this->products) - ->and($coupon->printRules)->toBe($this->printRules) - ->and($coupon->warningThresholdQuantity)->toBe($this->warningThresholdQuantity); - }); - - it('defaults the warning threshold quantity to 100 when not provided', function (): void { - // GIVEN / WHEN - $coupon = new RequestedCoupon( - name: $this->name, - quantity: $this->quantity, - diffusion: $this->diffusion, - moneyAmount: $this->moneyAmount, - processingCode: $this->processingCode, - couponValidity: $this->couponValidity, - couponValidityInformation: $this->couponValidityInformation, - products: $this->products, - printRules: $this->printRules, - gs1: $this->gs1, - ); - - // THEN - expect($coupon->warningThresholdQuantity)->toEqual(new WarningThresholdQuantity(100)) - ->and($coupon->products)->toBe($this->products) - ->and($coupon->printRules)->toBe($this->printRules) - ->and($coupon->gs1)->toBe($this->gs1); + ->and($coupon->printRules)->toBe($this->printRules); }); it('hydrates a requested coupon with only required fields', function (): void { @@ -188,8 +156,7 @@ describe('requested coupon', function (): void { // THEN expect($coupon->products)->toBe([]) ->and($coupon->printRules)->toBeNull() - ->and($coupon->gs1)->toBeNull() - ->and($coupon->warningThresholdQuantity)->toEqual(new WarningThresholdQuantity(100)); + ->and($coupon->gs1)->toBeNull(); }); it('throws when there is no validity information for france', function (): void { @@ -217,8 +184,7 @@ describe('requested coupon', function (): void { ['Not a CouponValidityInformation'], $this->products, $this->printRules, - $this->gs1, - $this->warningThresholdQuantity + $this->gs1 ))->toThrow(\DomainException::class, 'Coupon validity information must be CouponValidityInformation instances.'); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php b/tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php deleted file mode 100644 index de80f52..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/WarningThresholdQuantityTest.php +++ /dev/null @@ -1,48 +0,0 @@ -value = 10; - $this->zeroValue = 0; - $this->negativeValue = -1; - }); - - it('creates a valid warning threshold quantity', function (): void { - // GIVEN / WHEN - $warningThresholdQuantity = new WarningThresholdQuantity( - $this->value - ); - - // THEN - expect($warningThresholdQuantity)->toBeInstanceOf(WarningThresholdQuantity::class) - ->and($warningThresholdQuantity->value)->toBe($this->value); - }); - - it('accepts a zero value, meaning no warning', function (): void { - // GIVEN / WHEN - $warningThresholdQuantity = new WarningThresholdQuantity( - $this->zeroValue - ); - - // THEN - expect($warningThresholdQuantity->value)->toBe($this->zeroValue); - }); - - it('rejects a negative value', function (): void { - // GIVEN / WHEN - $action = fn (): WarningThresholdQuantity => new WarningThresholdQuantity( - $this->negativeValue - ); - - // THEN - expect($action)->toThrow(\DomainException::class, 'Warning threshold quantity cannot be negative.'); - }); -}); From 678487aa07df4f2894af74726e0bc4451b1dcc1f Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Fri, 2 Oct 2026 12:46:02 +0200 Subject: [PATCH 52/52] refactor: :recycle: refactor coupon diffusion test --- .../Campaign/CampaignCouponTest.php | 8 +++---- .../Coupon/CouponDiffusionTest.php | 22 +++++-------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php index 9938215..e700f2a 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php @@ -14,10 +14,10 @@ covers(CampaignCoupon::class); describe('campaign coupon', function (): void { beforeEach(function (): void { - $this->processingCode = new ProcessingCode('PC1'); - $this->quantity = new CouponQuantity(10); - $this->availableIn = Country::FRANCE; - $this->printRules = new PrintRules(maxPerCustomer: 3); + $this->processingCode = new ProcessingCode('PC1'); + $this->quantity = new CouponQuantity(10); + $this->availableIn = Country::FRANCE; + $this->printRules = new PrintRules(maxPerCustomer: 3); }); it('creates a valid campaign coupon', function (): void { diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php index f9e19dc..83929ee 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php @@ -11,25 +11,15 @@ use DateTime; covers(CouponDiffusion::class); describe('coupon diffusion', function (): void { - it('creates a valid coupon diffusion with one period', function (): void { - // GIVEN - $period = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); - - // WHEN - $couponDiffusion = new CouponDiffusion($period); - - // THEN - expect($couponDiffusion->period)->toBe($period); + beforeEach(function (): void { + $this->period = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); }); - it('creates a valid coupon diffusion with several periods', function (): void { - // GIVEN - $period = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')); - - // WHEN - $couponDiffusion = new CouponDiffusion($period); + it('creates a valid coupon diffusion with one period', function (): void { + // GIVEN / WHEN + $couponDiffusion = new CouponDiffusion($this->period); // THEN - expect($couponDiffusion->period)->toBe($period); + expect($couponDiffusion->period)->toBe($this->period); }); });