From fdbde84728fdb0bc40fc54c53e80c0ef2034721a Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 12 Oct 2026 11:49:03 +0200 Subject: [PATCH 01/21] refactor: :recycle: update coupon Entity and related VO --- .../UseCases/Brand/Create/CreateBrand.php | 3 +- .../Create/CreateCampaignRequest.php | 5 +- .../Find/FindCampaignRequest.php | 3 +- src/Domain/Enums/BarcodeFormat.php | 12 --- .../BrandCodeAlreadyExistsException.php | 18 ---- .../Exceptions/BrandNotFoundException.php | 19 ---- src/Domain/Models/Entities/Campaign.php | 22 +---- .../Models/Entities/CampaignRequest.php | 1 - src/Domain/Models/Entities/Coupon.php | 94 +++++++------------ src/Domain/Models/Entities/Stock.php | 8 +- .../ValueObjects/Campaign/CampaignCoupon.php | 8 +- .../Campaign/CampaignCouponArray.php | 23 +++++ .../CampaignRequestFilters.php | 2 +- .../Coupon/CouponAvailabilityCalendar.php | 14 +-- .../CouponIncoCode.php} | 12 +-- .../Coupon/CouponLegalInformation.php | 2 +- .../Coupon/CouponLegalInformationArray.php | 35 +++++++ .../ValueObjects/Coupon/CouponMovement.php | 17 ++++ .../Coupon/CouponMovementArray.php | 23 +++++ .../Models/ValueObjects/Coupon/CouponName.php | 22 +++++ .../{ => Coupon}/CouponPicture.php | 6 +- .../Coupon/CouponProcessingCode.php | 35 +++++++ .../Coupon/CouponProcessingCodeArray.php | 35 +++++++ .../Coupon/CouponProductArray.php | 19 ++++ .../ValueObjects/Coupon/CouponQuantity.php | 4 +- .../ValueObjects/Coupon/CouponValidity.php | 26 ----- .../Coupon/CouponValidityInformationArray.php | 35 +++++++ src/Domain/Models/ValueObjects/CouponName.php | 16 ---- src/Domain/Models/ValueObjects/GS1.php | 31 ------ .../Models/ValueObjects/ProcessingCode.php | 29 ------ .../RequestedCoupon/RequestedCoupon.php | 75 ++++++++------- src/Shared/Helpers/DatesHelper.php | 22 ++--- .../ValueObjects/Pagination/Paginated.php | 2 +- .../ValueObjects/Pagination/Pagination.php | 2 +- .../Reject/RejectCampaignRequestTest.php | 2 +- .../Unit/Domain/Models/Entities/BrandTest.php | 6 +- .../Models/Entities/CampaignRequestTest.php | 2 +- 37 files changed, 366 insertions(+), 324 deletions(-) delete mode 100644 src/Domain/Enums/BarcodeFormat.php delete mode 100644 src/Domain/Exceptions/BrandCodeAlreadyExistsException.php delete mode 100644 src/Domain/Exceptions/BrandNotFoundException.php create mode 100644 src/Domain/Models/ValueObjects/Campaign/CampaignCouponArray.php rename src/Domain/Models/ValueObjects/{IncoCode.php => Coupon/CouponIncoCode.php} (50%) create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArray.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponMovement.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponName.php rename src/Domain/Models/ValueObjects/{ => Coupon}/CouponPicture.php (59%) create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponProductArray.php delete mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponValidity.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArray.php delete mode 100644 src/Domain/Models/ValueObjects/CouponName.php delete mode 100644 src/Domain/Models/ValueObjects/GS1.php delete mode 100644 src/Domain/Models/ValueObjects/ProcessingCode.php diff --git a/src/Application/UseCases/Brand/Create/CreateBrand.php b/src/Application/UseCases/Brand/Create/CreateBrand.php index 7eb070f..eb98750 100644 --- a/src/Application/UseCases/Brand/Create/CreateBrand.php +++ b/src/Application/UseCases/Brand/Create/CreateBrand.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Application\UseCases\Brand\Create; -use Domain\Exceptions\BrandCodeAlreadyExistsException; use Domain\Models\Entities\Brand; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Services\PictureStorage; @@ -28,7 +27,7 @@ final readonly class CreateBrand implements UseCase, Transactional Assert::isInstanceOf($input, Input::class); if ($this->repository->existsByCode($input->code)) { - throw new BrandCodeAlreadyExistsException($input->code); + throw new \DomainException(sprintf('Brand code "%s" already exists.', $input->code->value)); } $logoPath = $this->pictureStorage->store($input->logo); diff --git a/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php index 9dc1b30..b2d1812 100644 --- a/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php +++ b/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\Create; -use Domain\Exceptions\BrandNotFoundException; use Domain\Models\Entities\CampaignRequest; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CampaignRequestRepository; @@ -27,8 +26,8 @@ final readonly class CreateCampaignRequest implements UseCase, Transactional { Assert::isInstanceOf($input, Input::class); - if (!$this->brandRepository->existsByCode($input->brandCode)) { - throw new BrandNotFoundException($input->brandCode); + if (! $this->brandRepository->existsByCode($input->brandCode)) { + throw new \DomainException(sprintf('Brand with code "%s" was not found.', $input->brandCode->value)); } $campaignRequest = CampaignRequest::create( diff --git a/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php index 0acbbef..2361ab6 100644 --- a/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php +++ b/src/Application/UseCases/CampaignRequest/Find/FindCampaignRequest.php @@ -7,7 +7,6 @@ namespace Application\UseCases\CampaignRequest\Find; use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestFound; use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestNotFound; use Application\UseCases\CampaignRequest\Find\Output\Output; -use Domain\Exceptions\BrandNotFoundException; use Domain\Models\Entities\Brand; use Domain\Models\Entities\CampaignRequest; use Domain\Ports\Repositories\BrandRepository; @@ -39,7 +38,7 @@ final readonly class FindCampaignRequest implements UseCase $brand = $this->brandRepository->findByCode($campaignRequest->brandCode); if (! $brand instanceof Brand) { - throw new BrandNotFoundException($campaignRequest->brandCode); + throw new \DomainException(sprintf('Brand with code "%s" was not found.', $campaignRequest->brandCode->value)); } return new CampaignRequestFound( diff --git a/src/Domain/Enums/BarcodeFormat.php b/src/Domain/Enums/BarcodeFormat.php deleted file mode 100644 index f648d9d..0000000 --- a/src/Domain/Enums/BarcodeFormat.php +++ /dev/null @@ -1,12 +0,0 @@ -value)); - } -} diff --git a/src/Domain/Exceptions/BrandNotFoundException.php b/src/Domain/Exceptions/BrandNotFoundException.php deleted file mode 100644 index eee250c..0000000 --- a/src/Domain/Exceptions/BrandNotFoundException.php +++ /dev/null @@ -1,19 +0,0 @@ -value)); - } -} diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php index a52ae3a..9c099d9 100644 --- a/src/Domain/Models/Entities/Campaign.php +++ b/src/Domain/Models/Entities/Campaign.php @@ -7,7 +7,7 @@ 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; +use Domain\Models\ValueObjects\Campaign\CampaignCouponArray; use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; use Shared\DDD\Models\AggregateRoot; @@ -18,25 +18,17 @@ use Shared\DDD\Models\HasTimestampIdentifier; */ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier { - /** - * @param array $coupons - */ private function __construct( public CampaignId $id, public CampaignCode $code, public CampaignName $name, public bool $publication, public bool $printAllAtOnce, - public array $coupons, + public CampaignCouponArray $coupons, public BrandCode $brandCode, public ActorId $actorId, 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 @@ -54,15 +46,12 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier return $this->updatedAt; } - /** - * @param array $coupons - */ public static function create( CampaignCode $code, CampaignName $name, bool $publication, bool $printAllAtOnce, - array $coupons, + CampaignCouponArray $coupons, BrandCode $brandCode, ActorId $actorId, ?\DateTimeInterface $updatedAt = null, @@ -80,16 +69,13 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier ); } - /** - * @param array $coupons - */ public static function hydrate( CampaignId $id, CampaignCode $code, CampaignName $name, bool $publication, bool $printAllAtOnce, - array $coupons, + CampaignCouponArray $coupons, BrandCode $brandCode, ActorId $actorId, ?\DateTimeInterface $updatedAt = null, diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index b4e7882..56acaf6 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -72,7 +72,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent campaignType: $campaignType, requestedCoupon: $requestedCoupon, status: CampaignRequestStatus::CREATED, - rejectionReason: null, updatedAt: $updatedAt, ); } diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index f879cd9..347e3d0 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -10,15 +10,15 @@ 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\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\Coupon\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponMovementArray; +use Domain\Models\ValueObjects\Coupon\CouponName; +use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; +use Domain\Models\ValueObjects\Coupon\CouponProductArray; +use Domain\Models\ValueObjects\Coupon\CouponQuantity; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; -use Domain\Models\ValueObjects\ProcessingCode; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; @@ -27,11 +27,6 @@ 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, @@ -39,28 +34,17 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier public MoneyAmount $amount, public CouponPicture $picture, public CouponDiffusion $diffusion, - public CouponValidity $validity, - public array $couponValidityInformation, - public array $couponLegalInformation, - public ProcessingCode $processingCode, + public CouponValidityInformationArray $validityInformations, + public CouponLegalInformationArray $legalInformations, + public CouponProcessingCodeArray $processingCode, public BrandCode $brandCode, public ActorId $createdBy, - public array $products = [], + public CouponProductArray $products, + public CouponMovementArray $movements, + public CouponQuantity $quantity, public ?CouponAvailabilityCalendar $availabilityCalendar = null, - public ?GS1 $gs1 = null, public ?\DateTimeInterface $updatedAt = null, ) { - 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.'); - } - } } public function id(): CouponId @@ -78,26 +62,21 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier return $this->updatedAt; } - /** - * @param array $couponValidityInformation - * @param array $couponLegalInformation - * @param array $products - */ public static function create( CouponCode $code, CouponName $name, MoneyAmount $amount, CouponPicture $picture, CouponDiffusion $diffusion, - CouponValidity $validity, - array $couponValidityInformation, - array $couponLegalInformation, - ProcessingCode $processingCode, + CouponValidityInformationArray $validityInformations, + CouponLegalInformationArray $legalInformations, + CouponProcessingCodeArray $processingCode, BrandCode $brandCode, ActorId $createdBy, - array $products = [], + CouponProductArray $products, + CouponMovementArray $movements, + CouponQuantity $quantity, ?CouponAvailabilityCalendar $availabilityCalendar = null, - ?GS1 $gs1 = null, ?\DateTimeInterface $updatedAt = null, ): Coupon { return new self( @@ -107,24 +86,19 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier amount: $amount, picture: $picture, diffusion: $diffusion, - validity: $validity, - couponValidityInformation: $couponValidityInformation, - couponLegalInformation: $couponLegalInformation, + validityInformations: $validityInformations, + legalInformations: $legalInformations, processingCode: $processingCode, brandCode: $brandCode, createdBy: $createdBy, products: $products, + movements: $movements, + quantity: $quantity, availabilityCalendar: $availabilityCalendar, - gs1: $gs1, updatedAt: $updatedAt, ); } - /** - * @param array $couponValidityInformation - * @param array $couponLegalInformation - * @param array $products - */ public static function hydrate( CouponId $id, CouponCode $code, @@ -132,15 +106,15 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier MoneyAmount $amount, CouponPicture $picture, CouponDiffusion $diffusion, - CouponValidity $validity, - array $couponValidityInformation, - array $couponLegalInformation, - ProcessingCode $processingCode, + CouponValidityInformationArray $validityInformations, + CouponLegalInformationArray $legalInformations, + CouponProcessingCodeArray $processingCode, BrandCode $brandCode, ActorId $createdBy, - array $products = [], + CouponProductArray $products, + CouponMovementArray $movements, + CouponQuantity $quantity, ?CouponAvailabilityCalendar $availabilityCalendar = null, - ?GS1 $gs1 = null, ?\DateTimeInterface $updatedAt = null, ): self { return new self( @@ -150,15 +124,15 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier amount: $amount, picture: $picture, diffusion: $diffusion, - validity: $validity, - couponValidityInformation: $couponValidityInformation, - couponLegalInformation: $couponLegalInformation, + validityInformations: $validityInformations, + legalInformations: $legalInformations, processingCode: $processingCode, brandCode: $brandCode, createdBy: $createdBy, products: $products, + movements: $movements, + quantity: $quantity, availabilityCalendar: $availabilityCalendar, - gs1: $gs1, updatedAt: $updatedAt, ); } diff --git a/src/Domain/Models/Entities/Stock.php b/src/Domain/Models/Entities/Stock.php index 06cd24d..dbd4307 100644 --- a/src/Domain/Models/Entities/Stock.php +++ b/src/Domain/Models/Entities/Stock.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Domain\Models\Entities; use Domain\Models\ValueObjects\Campaign\CampaignCode; -use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\Stock\StockAdjustment; use Domain\Models\ValueObjects\Stock\StockAmount; use Domain\Models\ValueObjects\Stock\StockId; @@ -21,7 +21,7 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier public StockId $id, public StockAmount $stockAmount, public CampaignCode $campaignCode, - public ProcessingCode $processingCode, + public CouponCode $couponCode, /** @var array $stockAdjustment */ public array $stockAdjustment, public ?\DateTimeInterface $updatedAt = null, @@ -55,7 +55,7 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier StockId $id, StockAmount $stockAmount, CampaignCode $campaignCode, - ProcessingCode $processingCode, + CouponCode $couponCode, array $stockAdjustment = [], ?\DateTimeInterface $updatedAt = null, ): self { @@ -63,7 +63,7 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier id: $id, stockAmount: $stockAmount, campaignCode: $campaignCode, - processingCode: $processingCode, + couponCode: $couponCode, stockAdjustment: $stockAdjustment, updatedAt: $updatedAt, ); diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php index ba56632..5841a0f 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php @@ -4,17 +4,13 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Campaign; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; +use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\PrintRules; -use Domain\Models\ValueObjects\ProcessingCode; -use Shared\Enums\Country; final readonly class CampaignCoupon { public function __construct( - public ProcessingCode $processingCode, - public CouponQuantity $quantity, - public Country $availableIn, + public CouponCode $couponCode, public ?PrintRules $printRules = null, ) { } diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCouponArray.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCouponArray.php new file mode 100644 index 0000000..b312f93 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCouponArray.php @@ -0,0 +1,23 @@ +value === []) { + throw new \DomainException('Coupons requires at least one CampaignCoupon.'); + } + + foreach ($this->value as $coupon) { + if (! $coupon instanceof CampaignCoupon) { + throw new \DomainException('Coupons must be CampaignCoupon instances.'); + } + } + } +} diff --git a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php index 5978375..ca192a4 100644 --- a/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php +++ b/src/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFilters.php @@ -12,7 +12,7 @@ final readonly class CampaignRequestFilters public ?BrandCode $brandCode = null, public ?CampaignRequestStatus $status = null, ) { - if (null === $this->brandCode && null === $this->status) { + if (!$this->brandCode instanceof BrandCode && !$this->status instanceof CampaignRequestStatus) { throw new \InvalidArgumentException('CampaignRequestFilters require at least a brand code or a status.'); } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php b/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php index 718ff59..90de3ff 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php @@ -19,7 +19,7 @@ final readonly class CouponAvailabilityCalendar implements Equatable } foreach ($this->periods as $period) { - if (!$period instanceof Period) { + if (! $period instanceof Period) { throw new \DomainException('CouponAvailabilityCalendar periods must be Period instances.'); } } @@ -27,23 +27,17 @@ final readonly class CouponAvailabilityCalendar implements Equatable public function isActiveAt(DateTime $in): bool { - foreach ($this->periods as $period) { - if ($in >= $period->start && $in <= $period->end) { - return true; - } - } - - return false; + return array_any($this->periods, fn ($period): bool => $in >= $period->start && $in <= $period->end); } public function beginsAt(): DateTime { - return min(array_map(fn (Period $period) => $period->start, $this->periods)); + return min(array_map(fn (Period $period): DateTime => $period->start, $this->periods)); } public function endsAt(): DateTime { - return max(array_map(fn (Period $period) => $period->end, $this->periods)); + return max(array_map(fn (Period $period): DateTime => $period->end, $this->periods)); } #[\Override] diff --git a/src/Domain/Models/ValueObjects/IncoCode.php b/src/Domain/Models/ValueObjects/Coupon/CouponIncoCode.php similarity index 50% rename from src/Domain/Models/ValueObjects/IncoCode.php rename to src/Domain/Models/ValueObjects/Coupon/CouponIncoCode.php index e830ad6..070fc5a 100644 --- a/src/Domain/Models/ValueObjects/IncoCode.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponIncoCode.php @@ -2,11 +2,9 @@ declare(strict_types=1); -namespace Domain\Models\ValueObjects; +namespace Domain\Models\ValueObjects\Coupon; -use Shared\DDD\Models\Equatable; - -final readonly class IncoCode implements Equatable +final readonly class CouponIncoCode { /** * @throws \DomainException @@ -18,10 +16,4 @@ final readonly class IncoCode implements Equatable 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/Coupon/CouponLegalInformation.php b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php index e93b9ad..3baea56 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformation.php @@ -10,7 +10,7 @@ final readonly class CouponLegalInformation { public function __construct( public string $legalText, - public Country $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/CouponLegalInformationArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArray.php new file mode 100644 index 0000000..9fd1189 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArray.php @@ -0,0 +1,35 @@ +value === []) { + throw new \DomainException('CouponLegalInformations requires at least one CouponLegalInformation.'); + } + + $hasFrance = false; + + foreach ($this->value as $couponLegalInformation) { + if (! $couponLegalInformation instanceof CouponLegalInformation) { + throw new \DomainException('Value of CouponLegalInformationArray must be CouponLegalInformation instances.'); + } + + if ($couponLegalInformation->forLocale === Country::FRANCE) { + $hasFrance = true; + } + } + + if (! $hasFrance) { + throw new \DomainException('CouponLegalInformations requires a CouponLegalInformation for France.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponMovement.php b/src/Domain/Models/ValueObjects/Coupon/CouponMovement.php new file mode 100644 index 0000000..a620908 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponMovement.php @@ -0,0 +1,17 @@ +createdAt = $createdAt ?? new \DateTimeImmutable(); + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php new file mode 100644 index 0000000..7cfda38 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php @@ -0,0 +1,23 @@ +couponMovements === []) { + throw new \DomainException('CouponMovementArray requires at least one CouponMovement.'); + } + + foreach ($this->couponMovements as $couponMovement) { + if (! $couponMovement instanceof CouponMovement) { + throw new \DomainException('Value of CouponMovementArray must be CouponMovement instances.'); + } + } + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponName.php b/src/Domain/Models/ValueObjects/Coupon/CouponName.php new file mode 100644 index 0000000..56ec622 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponName.php @@ -0,0 +1,22 @@ + 200) { + throw new \DomainException('Coupon name exceeds the maximum allowed length.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/CouponPicture.php b/src/Domain/Models/ValueObjects/Coupon/CouponPicture.php similarity index 59% rename from src/Domain/Models/ValueObjects/CouponPicture.php rename to src/Domain/Models/ValueObjects/Coupon/CouponPicture.php index a85f12f..7d84384 100644 --- a/src/Domain/Models/ValueObjects/CouponPicture.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponPicture.php @@ -2,14 +2,16 @@ declare(strict_types=1); -namespace Domain\Models\ValueObjects; +namespace Domain\Models\ValueObjects\Coupon; + +use Domain\Models\ValueObjects\PicturePath; final readonly class CouponPicture { public function __construct( public PicturePath $decoration, public PicturePath $thumbnail, - public PicturePath $inactiveThumbnail + public PicturePath $mobileThumbnail, ) { } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php b/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php new file mode 100644 index 0000000..b464eb3 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php @@ -0,0 +1,35 @@ +value) !== 1) { + throw new \DomainException( + 'Processing Code must be exactly 13 alphanumeric characters, e.g. ABC1234567890.', + ); + } + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self && $this->value === $other->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php new file mode 100644 index 0000000..a07baff --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php @@ -0,0 +1,35 @@ +value === []) { + throw new \DomainException('ProcessingCodes requires at least one CouponProcessingCode.'); + } + + $hasFrance = false; + + foreach ($this->value as $processingCode) { + if (! $processingCode instanceof CouponProcessingCode) { + throw new \DomainException('Value of CouponProcessingCodeArray must be CouponProcessingCode instances.'); + } + + if ($processingCode->availableIn === Country::FRANCE) { + $hasFrance = true; + } + } + + if (! $hasFrance) { + throw new \DomainException('CouponProcessingCodes requires a CouponProcessingCode for France.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProductArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponProductArray.php new file mode 100644 index 0000000..e1f9cbb --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProductArray.php @@ -0,0 +1,19 @@ +value as $product) { + if (! $product instanceof CouponIncoCode) { + throw new \DomainException('Value of CouponProductArray must be CouponIncoCode instances.'); + } + } + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php b/src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php index d2f5bb0..0e6a1a0 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php @@ -7,9 +7,9 @@ namespace Domain\Models\ValueObjects\Coupon; final readonly class CouponQuantity { public function __construct( - public int $quantity, + public int $value, ) { - if ($this->quantity <= 0) { + if ($this->value <= 0) { throw new \DomainException('CouponQuantity quantity must be positive.'); } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponValidity.php b/src/Domain/Models/ValueObjects/Coupon/CouponValidity.php deleted file mode 100644 index ef59020..0000000 --- a/src/Domain/Models/ValueObjects/Coupon/CouponValidity.php +++ /dev/null @@ -1,26 +0,0 @@ -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/Coupon/CouponValidityInformationArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArray.php new file mode 100644 index 0000000..23e415d --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArray.php @@ -0,0 +1,35 @@ +value === []) { + throw new \DomainException('CouponValidityInformations requires at least one CouponValidityInformation.'); + } + + $hasFrance = false; + + foreach ($this->value as $couponLegalInformation) { + if (! $couponLegalInformation instanceof CouponValidityInformation) { + throw new \DomainException('Value of CouponValidityInformationArray must be CouponValidityInformation instances.'); + } + + if ($couponLegalInformation->forLocale === Country::FRANCE) { + $hasFrance = true; + } + } + + if (! $hasFrance) { + throw new \DomainException('CouponValidityInformations requires a CouponValidityInformation for France.'); + } + } +} diff --git a/src/Domain/Models/ValueObjects/CouponName.php b/src/Domain/Models/ValueObjects/CouponName.php deleted file mode 100644 index a681b49..0000000 --- a/src/Domain/Models/ValueObjects/CouponName.php +++ /dev/null @@ -1,16 +0,0 @@ -value) === '') { - throw new \DomainException('Coupon name cannot be empty.'); - } - } -} diff --git a/src/Domain/Models/ValueObjects/GS1.php b/src/Domain/Models/ValueObjects/GS1.php deleted file mode 100644 index 1f646a2..0000000 --- a/src/Domain/Models/ValueObjects/GS1.php +++ /dev/null @@ -1,31 +0,0 @@ -value) === '') { - 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/ProcessingCode.php b/src/Domain/Models/ValueObjects/ProcessingCode.php deleted file mode 100644 index 97dd7ff..0000000 --- a/src/Domain/Models/ValueObjects/ProcessingCode.php +++ /dev/null @@ -1,29 +0,0 @@ -value) === '') { - throw new \DomainException('Processing Code 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; - } -} diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php index c5bfbab..17f67c3 100644 --- a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -4,58 +4,67 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\RequestedCoupon; -use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponName; +use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; +use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; -use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\GS1; -use Domain\Models\ValueObjects\IncoCode; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PrintRules; -use Domain\Models\ValueObjects\ProcessingCode; use Shared\Enums\Country; final readonly class RequestedCoupon { - /** - * @param array $couponValidityInformation - * @param array $products - */ public function __construct( - public CouponName $name, + public CouponName $couponName, public RequestedCouponQuantity $quantity, public Period $diffusion, public MoneyAmount $moneyAmount, - public ProcessingCode $processingCode, - public CouponValidity $couponValidity, - public array $couponValidityInformation, - public array $products = [], + public CouponPicture $couponPicture, + public CouponProcessingCodeArray $couponProcessingCodes, + public CouponValidityInformationArray $couponValidityInformations, + public CouponLegalInformationArray $couponLegalInformations, + public CouponProductArray $couponProducts, public ?PrintRules $printRules = null, - public ?GS1 $gs1 = null, ) { - foreach ($this->couponValidityInformation as $validityInformation) { - if (!$validityInformation instanceof CouponValidityInformation) { - throw new \DomainException('Coupon validity information must be CouponValidityInformation instances.'); - } - } - - 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.'); + if ($this->hasProcessingCodeFor(Country::BENELUX)) { + $this->assertCountryIsFullyCovered(Country::BENELUX); } } - private function hasValidityInformationFor(Country $country): bool + private function assertCountryIsFullyCovered(Country $country): void { - foreach ($this->couponValidityInformation as $validityInformation) { - if ($validityInformation->forLocale === $country) { - return true; - } + if (! $this->hasProcessingCodeFor($country)) { + throw new \DomainException(sprintf('A coupon requires a processing code for %s.', $country->name)); } - return false; + if (! $this->hasInformationFor($country, $this->couponValidityInformations)) { + throw new \DomainException(sprintf('A coupon requires validity information for %s.', $country->name)); + } + + if (! $this->hasInformationFor($country, $this->couponLegalInformations)) { + throw new \DomainException(sprintf('A coupon requires legal information for %s.', $country->name)); + } + } + + private function hasProcessingCodeFor(Country $country): bool + { + return array_any( + $this->couponProcessingCodes->value, + fn (CouponProcessingCode $processingCode): bool => $processingCode->availableIn === $country, + ); + } + + private function hasInformationFor(Country $country, CouponValidityInformationArray | CouponLegalInformationArray $informations): bool + { + return array_any( + $informations->value, + fn (CouponValidityInformation | CouponLegalInformation $information): bool => $information->forLocale === $country, + ); } } diff --git a/src/Shared/Helpers/DatesHelper.php b/src/Shared/Helpers/DatesHelper.php index 8c1a0ca..021df21 100644 --- a/src/Shared/Helpers/DatesHelper.php +++ b/src/Shared/Helpers/DatesHelper.php @@ -36,10 +36,10 @@ final class DatesHelper $now = self::now(); return match (true) { - $from !== null && $to !== null => $now->between($from, $to), - $from !== null => $now->greaterThanOrEqualTo($from), - $to !== null => $now->lessThanOrEqualTo($to), - default => false, + $from instanceof CarbonInterface && $to instanceof CarbonInterface => $now->between($from, $to), + $from instanceof CarbonInterface => $now->greaterThanOrEqualTo($from), + $to instanceof CarbonInterface => $now->lessThanOrEqualTo($to), + default => false, }; } @@ -87,7 +87,7 @@ final class DatesHelper break; default: try { - $output = (new \DateTimeImmutable($datetime, new \DateTimeZone('UTC')))->setTimezone(new \DateTimeZone('UTC')); + $output = new \DateTimeImmutable($datetime, new \DateTimeZone('UTC'))->setTimezone(new \DateTimeZone('UTC')); } catch (\Exception) { throw InvalidDateFormatException::forDateString($datetime); } @@ -96,21 +96,21 @@ final class DatesHelper return $output; } - protected static function parseFrench(string $datetime): \DateTimeInterface + private static function parseFrench(string $datetime): \DateTimeInterface { $dt = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $datetime . ' 00:00:00', new \DateTimeZone('UTC')); return $dt ?: self::parseFallback($datetime); } - protected static function parseFrenchInverted(string $datetime): \DateTimeInterface + private static function parseFrenchInverted(string $datetime): \DateTimeInterface { $dt = \DateTimeImmutable::createFromFormat('d-m-Y H:i:s', $datetime . ' 00:00:00', new \DateTimeZone('UTC')); return $dt ?: self::parseFallback($datetime); } - protected static function parseRfc3339(string $datetime): \DateTimeInterface + private static function parseRfc3339(string $datetime): \DateTimeInterface { $dt = \DateTimeImmutable::createFromFormat(\DateTimeInterface::RFC3339, $datetime); if ($dt !== false) { @@ -122,17 +122,17 @@ final class DatesHelper return $dt ? $dt->setTimezone(new \DateTimeZone('UTC')) : self::parseFallback($datetime); } - protected static function parseIso8601(string $datetime): \DateTimeInterface + private static function parseIso8601(string $datetime): \DateTimeInterface { $dt = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $datetime); return $dt ? $dt->setTimezone(new \DateTimeZone('UTC')) : self::parseFallback($datetime); } - protected static function parseFallback(string $datetime): \DateTimeInterface + private static function parseFallback(string $datetime): \DateTimeInterface { try { - return (new \DateTimeImmutable($datetime, new \DateTimeZone('UTC')))->setTimezone(new \DateTimeZone('UTC')); + return new \DateTimeImmutable($datetime, new \DateTimeZone('UTC'))->setTimezone(new \DateTimeZone('UTC')); } catch (\Throwable) { throw InvalidDateFormatException::forDateString($datetime); } diff --git a/src/Shared/ValueObjects/Pagination/Paginated.php b/src/Shared/ValueObjects/Pagination/Paginated.php index 84d4a09..add80dd 100644 --- a/src/Shared/ValueObjects/Pagination/Paginated.php +++ b/src/Shared/ValueObjects/Pagination/Paginated.php @@ -9,7 +9,7 @@ final readonly class Paginated { /** @param T[] $items */ public function __construct( - private readonly Pagination $pagination, + private Pagination $pagination, private array $items, private int $total ) { diff --git a/src/Shared/ValueObjects/Pagination/Pagination.php b/src/Shared/ValueObjects/Pagination/Pagination.php index 5b38a80..599885e 100644 --- a/src/Shared/ValueObjects/Pagination/Pagination.php +++ b/src/Shared/ValueObjects/Pagination/Pagination.php @@ -9,7 +9,7 @@ use Shared\DDD\Models\Equatable; /** * Immutable value object representing a pagination range. */ -final readonly class Pagination implements Equatable +final readonly class Pagination implements Equatable, \Stringable { /** ERR_TOO_MANY_RECORDS fixed to 10k */ public const int MAX_PAGE_SIZE = PageSize::MAX_PAGE_SIZE; diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 5cc0ccc..3b8b10c 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -64,7 +64,7 @@ describe('reject campaign request use case', function (): void { it('rejects a campaign request and persists it', function (): void { // GIVEN - CarbonImmutable::setTestNow('2026-06-15 12:00:00'); + CarbonImmutable::setTestNow('2026-06-15 12:00:00 UTC'); $id = CampaignRequestId::generate(); $campaignRequest = CampaignRequest::hydrate( $id, diff --git a/tests/Unit/Domain/Models/Entities/BrandTest.php b/tests/Unit/Domain/Models/Entities/BrandTest.php index 1e3737b..04777fd 100644 --- a/tests/Unit/Domain/Models/Entities/BrandTest.php +++ b/tests/Unit/Domain/Models/Entities/BrandTest.php @@ -119,7 +119,7 @@ describe('brand', function (): void { it('updates a brand logo and sets updatedAt timestamp', function (): void { // GIVEN - CarbonImmutable::setTestNow('2026-06-15 12:00:00'); + CarbonImmutable::setTestNow('2026-06-15 12:00:00 UTC'); $id = BrandId::generate(); $logoOld = new PicturePath('files/picture-old.jpg'); $logoNew = new PicturePath('files/picture-new.jpg'); @@ -148,7 +148,7 @@ describe('brand', function (): void { it('updates a brand rcs and sets updatedAt timestamp', function (): void { // GIVEN - CarbonImmutable::setTestNow('2026-06-15 14:30:00'); + CarbonImmutable::setTestNow('2026-06-15 14:30:00 UTC'); $id = BrandId::generate(); $rcsOld = new Rcs('RCS PARIS 111 222 333'); $rcsNew = new Rcs('RCS NANTES 444 555 666'); @@ -177,7 +177,7 @@ describe('brand', function (): void { it('updates a brand contact and sets updatedAt timestamp', function (): void { // GIVEN - CarbonImmutable::setTestNow('2026-06-15 16:45:00'); + CarbonImmutable::setTestNow('2026-06-15 16:45:00 UTC'); $id = BrandId::generate(); $contactOld = new Email('contact-old@example.com'); $contactNew = new Email('contact-new@example.com'); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index e1b936f..8b79003 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -174,7 +174,7 @@ 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'); + CarbonImmutable::setTestNow('2026-06-15 12:00:00 UTC'); $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), $this->code, From 680c71ea87cfa63a4f73e061fffd7f48eb8c537e Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 12 Oct 2026 17:08:07 +0200 Subject: [PATCH 02/21] =?UTF-8?q?test:=20=E2=9C=85=20update=20on=20coupon?= =?UTF-8?q?=20Entity=20and=20related=20VO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UseCases/Brand/Create/CreateBrandTest.php | 3 +- .../UseCases/Brand/Update/UpdateBrandTest.php | 4 +- .../Create/CreateCampaignRequestTest.php | 66 +- .../Find/FindCampaignRequestTest.php | 133 ++-- .../List/ListCampaignsRequestTest.php | 192 ++--- .../Reject/RejectCampaignRequestTest.php | 82 +-- tests/Unit/Domain/Enums/BarcodeFormatTest.php | 23 - .../BrandCodeAlreadyExistsExceptionTest.php | 24 - .../Unit/Domain/Models/Entities/ActorTest.php | 20 +- .../Unit/Domain/Models/Entities/BrandTest.php | 160 ++--- .../Models/Entities/CampaignRequestTest.php | 279 +++++--- .../Domain/Models/Entities/CampaignTest.php | 232 +++--- .../Domain/Models/Entities/CouponTest.php | 672 ++++++++++-------- .../Unit/Domain/Models/Entities/StockTest.php | 100 ++- .../Actor/ActorDisplayNameTest.php | 19 +- .../ValueObjects/Brand/BrandNameTest.php | 21 +- .../Brand/BrandSortCriteriaTest.php | 34 + .../Campaign/CampaignCodeTest.php | 19 +- .../Campaign/CampaignCouponArrayTest.php | 57 ++ .../Campaign/CampaignCouponTest.php | 66 +- .../Campaign/CampaignNameTest.php | 19 +- .../CampaignRequest/ApplicantTest.php | 78 +- .../CampaignRequestFiltersTest.php | 28 +- .../CampaignRequestSortClauseTest.php | 25 + .../CampaignRequestSortCriteriaTest.php | 44 +- .../CampaignRequest/RejectionReasonTest.php | 14 +- .../Coupon/CouponAvailabilityCalendarTest.php | 50 +- .../Coupon/CouponDiffusionTest.php | 10 +- .../Coupon/CouponIncoCodeTest.php | 28 + .../CouponLegalInformationArrayTest.php | 88 +++ .../Coupon/CouponLegalInformationTest.php | 36 +- .../Coupon/CouponMovementArrayTest.php | 61 ++ .../Coupon/CouponMovementTest.php | 47 ++ .../ValueObjects/Coupon/CouponNameTest.php | 28 + .../Coupon/CouponProcessingCode.php | 45 ++ .../Coupon/CouponProcessingCodeArrayTest.php | 88 +++ .../Coupon/CouponProcessingCodeTest.php | 93 +++ .../Coupon/CouponProductArrayTest.php | 67 ++ .../Coupon/CouponQuantityTest.php | 28 +- .../CouponValidityInformationArrayTest.php | 88 +++ .../Coupon/CouponValidityInformationTest.php | 24 +- .../Coupon/CouponValidityTest.php | 38 - .../Models/ValueObjects/CouponNameTest.php | 33 - .../Domain/Models/ValueObjects/GS1Test.php | 38 - .../Models/ValueObjects/IncoCodeTest.php | 33 - .../Models/ValueObjects/MoneyAmountTest.php | 60 +- .../Models/ValueObjects/PrintRulesTest.php | 54 +- .../ValueObjects/ProcessingCodeTest.php | 33 - .../Models/ValueObjects/RawPictureTest.php | 2 +- .../RequestedCoupon/PeriodTest.php | 19 +- .../RequestedCouponQuantityTest.php | 28 +- .../RequestedCoupon/RequestedCouponTest.php | 406 ++++++----- .../Stock/StockAdjustmentTest.php | 46 +- .../ValueObjects/Stock/StockAmountTest.php | 30 +- .../{Domain => Shared}/Enums/CountryTest.php | 11 +- tests/Unit/Shared/Helpers/DatesHelperTest.php | 4 +- tests/Unit/Shared/Helpers/UuidHelperTest.php | 16 +- .../Pagination/PageNumberTest.php | 2 +- .../ValueObjects/Pagination/PageSizeTest.php | 4 +- 59 files changed, 2351 insertions(+), 1701 deletions(-) delete mode 100644 tests/Unit/Domain/Enums/BarcodeFormatTest.php delete mode 100644 tests/Unit/Domain/Exceptions/BrandCodeAlreadyExistsExceptionTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Brand/BrandSortCriteriaTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponArrayTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortClauseTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIncoCodeTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArrayTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementArrayTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponNameTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductArrayTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArrayTest.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/CouponNameTest.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/GS1Test.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/ProcessingCodeTest.php rename tests/Unit/{Domain => Shared}/Enums/CountryTest.php (57%) diff --git a/tests/Unit/Application/UseCases/Brand/Create/CreateBrandTest.php b/tests/Unit/Application/UseCases/Brand/Create/CreateBrandTest.php index aeaa717..d7e81b6 100644 --- a/tests/Unit/Application/UseCases/Brand/Create/CreateBrandTest.php +++ b/tests/Unit/Application/UseCases/Brand/Create/CreateBrandTest.php @@ -7,7 +7,6 @@ namespace Tests\Unit\Application\UseCases\Brand\Create; use Application\UseCases\Brand\Create\CreateBrand; use Application\UseCases\Brand\Create\Input; use Application\UseCases\Brand\Create\Output; -use Domain\Exceptions\BrandCodeAlreadyExistsException; use Domain\Models\Entities\Brand; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Brand\BrandName; @@ -77,7 +76,7 @@ describe('create brand use case validation rules', function (): void { $action = fn (): Output => $useCase->execute($input); // THEN - expect($action)->toThrow(BrandCodeAlreadyExistsException::class); + expect($action)->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Application/UseCases/Brand/Update/UpdateBrandTest.php b/tests/Unit/Application/UseCases/Brand/Update/UpdateBrandTest.php index e18077e..c2bbeb4 100644 --- a/tests/Unit/Application/UseCases/Brand/Update/UpdateBrandTest.php +++ b/tests/Unit/Application/UseCases/Brand/Update/UpdateBrandTest.php @@ -48,7 +48,7 @@ describe('update brand use case', function () use ($existingBrand): void { && $saved->contact === $newContact && $saved->code === $brand->code && $saved->name === $brand->name - && $saved->updatedAt !== null, + && $saved->updatedAt instanceof \DateTimeInterface, )); $useCase = new UpdateBrand($repository); @@ -77,7 +77,7 @@ describe('update brand use case', function () use ($existingBrand): void { && $saved->rcs === $brand->rcs && $saved->code === $brand->code && $saved->name === $brand->name - && $saved->updatedAt !== null, + && $saved->updatedAt instanceof \DateTimeInterface, )); $useCase = new UpdateBrand($repository); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php index efd146e..99d8350 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php @@ -8,21 +8,26 @@ use Application\UseCases\CampaignRequest\Create\CreateCampaignRequest; use Application\UseCases\CampaignRequest\Create\Input; use Application\UseCases\CampaignRequest\Create\Output; use DateTime; -use Domain\Exceptions\BrandNotFoundException; 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\CampaignRequestStatus; use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType; -use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponIncoCode; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponName; +use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; +use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; -use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\IncoCode; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; 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\Ports\Repositories\BrandRepository; @@ -34,33 +39,44 @@ use Shared\ValueObjects\Money; covers(CreateCampaignRequest::class, Input::class, Output::class); -beforeEach(function (): void { - $this->input = new Input( +describe('create campaign request use case', function (): void { + $makeInput = fn (): 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'), + couponName: 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'), - couponValidity: new CouponValidity(30, new DateTime('2026-02-01')), - couponValidityInformation: [ + couponPicture: new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/mobileThumbnail.jpg') + ), + couponProcessingCodes: new CouponProcessingCodeArray([ + new CouponProcessingCode('ABC1234567890', Country::FRANCE), + ]), + couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), - ], - products: [new IncoCode(35784), new IncoCode(15522)], + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponLegalInformations: new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), printRules: new PrintRules(maxPerCustomer: 3) ), ); -}); -describe('create campaign request use case', function (): void { - it('creates a campaign request when the brand exists', function (): void { + it('creates a campaign request when the brand exists', function () use ($makeInput): void { // GIVEN + $input = $makeInput(); + $brandRepository = \Mockery::mock(BrandRepository::class); - $brandRepository->shouldReceive('existsByCode')->once()->with($this->input->brandCode)->andReturn(true); + $brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true); $campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class); $campaignRequestRepository->shouldReceive('save')->once()->with(\Mockery::type(CampaignRequest::class)); @@ -68,20 +84,20 @@ describe('create campaign request use case', function (): void { $useCase = new CreateCampaignRequest($campaignRequestRepository, $brandRepository); // WHEN - $result = $useCase->execute($this->input); + $result = $useCase->execute($input); // THEN expect($result)->toBeInstanceOf(Output::class) ->and($result->status)->toBe(CampaignRequestStatus::CREATED) - ->and($result->brandCode)->toBe($this->input->brandCode); + ->and($result->brandCode)->toBe($input->brandCode); }); -}); -describe('create campaign request use case validation rules', function (): void { - it('throws when the brand does not exist', function (): void { + it('throws when the brand does not exist', function () use ($makeInput): void { // GIVEN + $input = $makeInput(); + $brandRepository = \Mockery::mock(BrandRepository::class); - $brandRepository->shouldReceive('existsByCode')->once()->with($this->input->brandCode)->andReturn(false); + $brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(false); $campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class); $campaignRequestRepository->shouldNotReceive('save'); @@ -89,9 +105,9 @@ describe('create campaign request use case validation rules', function (): void $useCase = new CreateCampaignRequest($campaignRequestRepository, $brandRepository); // WHEN - $action = fn (): Output => $useCase->execute($this->input); + $action = fn (): Output => $useCase->execute($input); // THEN - expect($action)->toThrow(BrandNotFoundException::class); + expect($action)->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php index 912dc8d..84f7b5b 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -10,8 +10,6 @@ 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\Exceptions\BrandNotFoundException; use Domain\Models\Entities\Brand; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; @@ -23,16 +21,20 @@ 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\CouponIncoCode; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponName; +use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; +use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; -use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\GS1; -use Domain\Models\ValueObjects\IncoCode; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; 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\Ports\Repositories\BrandRepository; @@ -47,44 +49,51 @@ use Shared\ValueObjects\Rcs; covers(FindCampaignRequest::class, Input::class, CampaignRequestFound::class, CampaignRequestNotFound::class); describe('find campaign request use case', function (): void { - beforeEach(function (): void { - $this->requestedCoupon = new RequestedCoupon( - new CouponName('Coupon A'), - new RequestedCouponQuantity(100), - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC1'), - 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::BENELUX) - ], - [new IncoCode(35784), new IncoCode(15522)], - new PrintRules(maxPerCustomer: 3), - new GS1('01234567890128', BarcodeFormat::EAN13) - ); - }); + $requestedCoupon = new RequestedCoupon( + couponName: 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)), + couponPicture: new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/mobileThumbnail.jpg') + ), + couponProcessingCodes: new CouponProcessingCodeArray([ + new CouponProcessingCode('ABC1234567890', Country::FRANCE), + ]), + couponValidityInformations: new CouponValidityInformationArray([ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponLegalInformations: new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + printRules: new PrintRules(maxPerCustomer: 3), + ); - it('finds a campaign request by its id, along with its brand', function (): void { + it('finds a campaign request by its id, along with its brand', function () use ($requestedCoupon): void { // GIVEN $id = CampaignRequestId::generate(); $brandCode = new BrandCode('AAA01'); $campaignRequest = CampaignRequest::hydrate( - $id, - $brandCode, - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, - $this->requestedCoupon, - CampaignRequestStatus::CREATED, + id: $id, + brandCode: $brandCode, + name: new CampaignName('Campaign A'), + applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')), + campaignType: CampaignRequestType::CLUB, + requestedCoupon: $requestedCoupon, + status: CampaignRequestStatus::CREATED, ); $brand = Brand::hydrate( - BrandId::generate(), - $brandCode, - new BrandName('Brand A'), - new PicturePath('brands/logo.jpg'), - new Rcs('RCS PARIS 111 222 333'), - new Email('contact@example.com'), + id: BrandId::generate(), + code: $brandCode, + name: new BrandName('Brand A'), + logo: new PicturePath('brands/logo.jpg'), + rcs: new Rcs('RCS PARIS 111 222 333'), + contact: new Email('contact@example.com'), ); $repository = \Mockery::mock(CampaignRequestRepository::class); @@ -109,20 +118,20 @@ describe('find campaign request use case', function (): void { ->and($result->rejectionReason)->toBeNull(); }); - it('finds a rejected campaign request with its rejection reason', function (): void { + it('finds a rejected campaign request with its rejection reason', function () use ($requestedCoupon): void { // GIVEN $id = CampaignRequestId::generate(); $code = new BrandCode('AAA01'); $rejectionReason = new RejectionReason('Missing legal documents.'); $campaignRequest = CampaignRequest::hydrate( - $id, - $code, - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, - $this->requestedCoupon, - CampaignRequestStatus::REJECTED, - $rejectionReason, + id: $id, + brandCode: $code, + name: new CampaignName('Campaign A'), + applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')), + campaignType: CampaignRequestType::CLUB, + requestedCoupon: $requestedCoupon, + status: CampaignRequestStatus::REJECTED, + rejectionReason: $rejectionReason, ); $repository = \Mockery::mock(CampaignRequestRepository::class); @@ -131,12 +140,12 @@ describe('find campaign request use case', function (): void { $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'), + id: BrandId::generate(), + code: $code, + name: new BrandName('Brand A'), + logo: new PicturePath('brands/logo.jpg'), + rcs: new Rcs('RCS PARIS 111 222 333'), + contact: new Email('contact@example.com'), ) ); @@ -170,18 +179,18 @@ describe('find campaign request use case', function (): void { expect($result)->toBeInstanceOf(CampaignRequestNotFound::class); }); - it('throws when the campaign request references a brand that no longer exists', function (): void { + it('throws when the campaign request references a brand that no longer exists', function () use ($requestedCoupon): void { // GIVEN $id = CampaignRequestId::generate(); $brandCode = new BrandCode('AAA01'); $campaignRequest = CampaignRequest::hydrate( - $id, - $brandCode, - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, - $this->requestedCoupon, - CampaignRequestStatus::CREATED, + id: $id, + brandCode: $brandCode, + name: new CampaignName('Campaign A'), + applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')), + campaignType: CampaignRequestType::CLUB, + requestedCoupon: $requestedCoupon, + status: CampaignRequestStatus::CREATED, ); $repository = \Mockery::mock(CampaignRequestRepository::class); @@ -196,7 +205,7 @@ describe('find campaign request use case', function (): void { $action = fn (): Output => $useCase->execute(new Input(id: $id)); // THEN - expect($action)->toThrow(BrandNotFoundException::class); + expect($action)->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index cfb1914..3f5f2e4 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Application\UseCases\CampaignRequest\List; use DateTime; -use Domain\Enums\BarcodeFormat; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignName; @@ -17,15 +16,20 @@ 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\CouponIncoCode; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponName; +use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; +use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; -use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\GS1; -use Domain\Models\ValueObjects\IncoCode; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; 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\Ports\Repositories\CampaignRequestRepository; @@ -39,69 +43,85 @@ use Shared\ValueObjects\Pagination\Pagination; covers(ListCampaignsRequest::class, Input::class, Output::class, CampaignRequestListItem::class); describe('list campaigns request use case', function (): void { - beforeEach(function (): void { - $this->couponA = new RequestedCoupon( - new CouponName('Coupon A'), - new RequestedCouponQuantity(100), - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC1'), - 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::BENELUX) - ], - [new IncoCode(35784), new IncoCode(15522)], - new PrintRules(maxPerCustomer: 3), - new GS1('01234567890128', BarcodeFormat::EAN13) - ); + $couponA = new RequestedCoupon( + couponName: 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)), + couponPicture: new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/mobileThumbnail.jpg') + ), + couponProcessingCodes: new CouponProcessingCodeArray([ + new CouponProcessingCode('ABC1234567890', Country::FRANCE), + ]), + couponValidityInformations: new CouponValidityInformationArray([ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponLegalInformations: new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + printRules: new PrintRules(maxPerCustomer: 3), + ); - $this->couponB = new RequestedCoupon( - new CouponName('Coupon B'), - new RequestedCouponQuantity(100), - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC2'), - 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::BENELUX) - ], - [new IncoCode(24514), new IncoCode(46259)], - new PrintRules(maxPerCustomer: 3), - new GS1('01234567890128', BarcodeFormat::EAN13) - ); + $couponB = new RequestedCoupon( + couponName: new CouponName('Coupon B'), + 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)), + couponPicture: new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/mobileThumbnail.jpg') + ), + couponProcessingCodes: new CouponProcessingCodeArray([ + new CouponProcessingCode('ABC1234567890', Country::FRANCE), + ]), + couponValidityInformations: new CouponValidityInformationArray([ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponLegalInformations: new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + printRules: new PrintRules(maxPerCustomer: 3), + ); - $this->pagination = Pagination::firstPage(); + $pagination = Pagination::firstPage(); - $this->sort = new CampaignRequestSortCriteria( - clauses: [ - new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending), - ] - ); - }); + $sort = new CampaignRequestSortCriteria( + clauses: [ + new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending), + ] + ); - it('maps repository campaigns request to list items, preserving pagination and sort', function (): void { + it('maps repository campaigns request to list items, preserving pagination and sort', function () use ($couponA, $couponB, $pagination): void { // GIVEN $campaignRequest1 = CampaignRequest::hydrate( - CampaignRequestId::generate(), - new BrandCode('AAA01'), - new CampaignName('Campaign A'), - new Applicant('John', 'Doe', new Email('john.doe@example.com')), - CampaignRequestType::CLUB, - $this->couponA, - CampaignRequestStatus::CREATED, + id: CampaignRequestId::generate(), + brandCode: new BrandCode('AAA01'), + name: new CampaignName('Campaign A'), + applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')), + campaignType: CampaignRequestType::CLUB, + requestedCoupon: $couponA, + status: 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, - $this->couponB, - CampaignRequestStatus::CREATED, + id: CampaignRequestId::generate(), + brandCode: new BrandCode('AAA02'), + name: new CampaignName('Campaign B'), + applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')), + campaignType: CampaignRequestType::CLUB, + requestedCoupon: $couponB, + status: CampaignRequestStatus::CREATED, ); - $paginated = new Paginated($this->pagination, [$campaignRequest1, $campaignRequest2], 2); + $paginated = new Paginated($pagination, [$campaignRequest1, $campaignRequest2], 2); $sort = new CampaignRequestSortCriteria( clauses: [ @@ -110,12 +130,12 @@ describe('list campaigns request use case', function (): void { ] ); $filters = new CampaignRequestFilters($campaignRequest2->brandCode, CampaignRequestStatus::CREATED); - $input = new Input(pagination: $this->pagination, sort: $sort, filters: $filters); + $input = new Input(pagination: $pagination, sort: $sort, filters: $filters); $repository = \Mockery::mock(CampaignRequestRepository::class); $repository->shouldReceive('findAll') ->once() - ->with($this->pagination, CampaignRequestSortCriteria::class, CampaignRequestFilters::class) + ->with($pagination, CampaignRequestSortCriteria::class, CampaignRequestFilters::class) ->andReturn($paginated); $useCase = new ListCampaignsRequest($repository); @@ -135,29 +155,29 @@ describe('list campaigns request use case', function (): void { ->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($this->pagination); + ->and($result->campaignsRequest->getPagination())->toBe($pagination); }); - it('lists campaigns request with filters', function (): void { + it('lists campaigns request with filters', function () use ($couponA, $pagination, $sort): 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->couponA, - CampaignRequestStatus::CREATED, + id: CampaignRequestId::generate(), + brandCode: new BrandCode('AAA01'), + name: new CampaignName('Campaign A'), + applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')), + campaignType: CampaignRequestType::CLUB, + requestedCoupon: $couponA, + status: CampaignRequestStatus::CREATED, ); - $paginated = new Paginated($this->pagination, [$campaignRequest], 1); + $paginated = new Paginated($pagination, [$campaignRequest], 1); $filters = new CampaignRequestFilters($campaignRequest->brandCode, CampaignRequestStatus::CREATED); - $input = new Input(pagination: $this->pagination, sort: $this->sort, filters: $filters); + $input = new Input(pagination: $pagination, sort: $sort, filters: $filters); $repository = \Mockery::mock(CampaignRequestRepository::class); $repository->shouldReceive('findAll') ->once() - ->with($this->pagination, $this->sort, $filters) + ->with($pagination, $sort, $filters) ->andReturn($paginated); $useCase = new ListCampaignsRequest($repository); @@ -173,10 +193,10 @@ describe('list campaigns request use case', function (): void { ->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($this->pagination); + ->and($result->campaignsRequest->getPagination())->toBe($pagination); }); - it('lists campaigns request with sort but without filters', function (): void { + it('lists campaigns request with sort but without filters', function () use ($couponA, $couponB, $pagination, $sort): void { // GIVEN $campaignRequest1 = CampaignRequest::hydrate( CampaignRequestId::generate(), @@ -184,7 +204,7 @@ describe('list campaigns request use case', function (): void { new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - $this->couponA, + $couponA, CampaignRequestStatus::CREATED, ); $campaignRequest2 = CampaignRequest::hydrate( @@ -193,22 +213,22 @@ describe('list campaigns request use case', function (): void { new CampaignName('Campaign B'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - $this->couponB, + $couponB, CampaignRequestStatus::CREATED, ); - $paginated = new Paginated($this->pagination, [$campaignRequest2, $campaignRequest1], 2); + $paginated = new Paginated($pagination, [$campaignRequest2, $campaignRequest1], 2); $sort = new CampaignRequestSortCriteria( clauses: [ new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Descending), ] ); - $input = new Input(pagination: $this->pagination, sort: $sort); + $input = new Input(pagination: $pagination, sort: $sort); $repository = \Mockery::mock(CampaignRequestRepository::class); $repository->shouldReceive('findAll') ->once() - ->with($this->pagination, $sort, null) + ->with($pagination, $sort, null) ->andReturn($paginated); $useCase = new ListCampaignsRequest($repository); @@ -222,18 +242,18 @@ describe('list campaigns request use case', function (): void { ->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($this->pagination); + ->and($result->campaignsRequest->getPagination())->toBe($pagination); }); - it('returns an empty list without error when there are no campaigns request', function (): void { + it('returns an empty list without error when there are no campaigns request', function () use ($pagination, $sort): void { // GIVEN - $paginated = new Paginated($this->pagination, [], 0); - $input = new Input(pagination: $this->pagination, sort: $this->sort); + $paginated = new Paginated($pagination, [], 0); + $input = new Input(pagination: $pagination, sort: $sort); $repository = \Mockery::mock(CampaignRequestRepository::class); $repository->shouldReceive('findAll') ->once() - ->with($this->pagination, $this->sort, null) + ->with($pagination, $sort, null) ->andReturn($paginated); $useCase = new ListCampaignsRequest($repository); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 3b8b10c..e0e11a9 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -11,7 +11,6 @@ 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\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignName; @@ -20,15 +19,20 @@ 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\CouponIncoCode; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponName; +use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; +use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; -use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\GS1; -use Domain\Models\ValueObjects\IncoCode; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; 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\Ports\Repositories\CampaignRequestRepository; @@ -40,29 +44,36 @@ use Shared\ValueObjects\Money; covers(RejectCampaignRequest::class, Input::class, CampaignRequestRejected::class, CampaignRequestNotFound::class); describe('reject campaign request use case', function (): void { - beforeEach(function (): void { - $this->requestedCoupon = new RequestedCoupon( - new CouponName('Coupon A'), - new RequestedCouponQuantity(100), - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC1'), - 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::BENELUX) - ], - [new IncoCode(35784), new IncoCode(15522)], - new PrintRules(maxPerCustomer: 3), - new GS1('01234567890128', BarcodeFormat::EAN13) - ); - }); + $requestedCoupon = new RequestedCoupon( + couponName: 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)), + couponPicture: new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/mobileThumbnail.jpg') + ), + couponProcessingCodes: new CouponProcessingCodeArray([ + new CouponProcessingCode('ABC1234567890', Country::FRANCE), + ]), + couponValidityInformations: new CouponValidityInformationArray([ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponLegalInformations: new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + printRules: new PrintRules(maxPerCustomer: 3), + ); afterEach(function (): void { CarbonImmutable::setTestNow(); }); - it('rejects a campaign request and persists it', function (): void { + it('rejects a campaign request and persists it', function () use ($requestedCoupon): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15 12:00:00 UTC'); $id = CampaignRequestId::generate(); @@ -72,7 +83,7 @@ describe('reject campaign request use case', function (): void { new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - $this->requestedCoupon, + $requestedCoupon, CampaignRequestStatus::CREATED, ); $reason = new RejectionReason('Missing legal documents.'); @@ -82,7 +93,7 @@ describe('reject campaign request use case', function (): void { $repository->shouldReceive('save')->once()->with(\Mockery::on( fn (CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED && $saved->rejectionReason === $reason - && $saved->updatedAt !== null, + && $saved->updatedAt instanceof \DateTimeInterface, )); $useCase = new RejectCampaignRequest($repository); @@ -115,7 +126,7 @@ describe('reject campaign request use case', function (): void { expect($result)->toBeInstanceOf(CampaignRequestNotFound::class); }); - it('propagates the domain exception when the campaign request cannot be rejected', function (): void { + it('propagates the domain exception when the campaign request cannot be rejected', function () use ($requestedCoupon): void { // GIVEN $id = CampaignRequestId::generate(); $campaignRequest = CampaignRequest::hydrate( @@ -124,7 +135,7 @@ describe('reject campaign request use case', function (): void { new CampaignName('Campaign A'), new Applicant('John', 'Doe', new Email('john.doe@example.com')), CampaignRequestType::CLUB, - $this->requestedCoupon, + $requestedCoupon, CampaignRequestStatus::TRANSFORMED, ); @@ -142,19 +153,4 @@ describe('reject campaign request use case', function (): void { // THEN expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.'); }); - - 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 (): Output => $useCase->execute(new Input(id: CampaignRequestId::generate())); - - // THEN - expect($action)->toThrow(\ArgumentCountError::class); - }); }); diff --git a/tests/Unit/Domain/Enums/BarcodeFormatTest.php b/tests/Unit/Domain/Enums/BarcodeFormatTest.php deleted file mode 100644 index d1af8e0..0000000 --- a/tests/Unit/Domain/Enums/BarcodeFormatTest.php +++ /dev/null @@ -1,23 +0,0 @@ -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'); - }); -}); diff --git a/tests/Unit/Domain/Exceptions/BrandCodeAlreadyExistsExceptionTest.php b/tests/Unit/Domain/Exceptions/BrandCodeAlreadyExistsExceptionTest.php deleted file mode 100644 index 9ead53c..0000000 --- a/tests/Unit/Domain/Exceptions/BrandCodeAlreadyExistsExceptionTest.php +++ /dev/null @@ -1,24 +0,0 @@ -getMessage())->toBe('Brand code "AAA01" already exists.') - ->and($exception)->toBeInstanceOf(\DomainException::class); - }); -}); diff --git a/tests/Unit/Domain/Models/Entities/ActorTest.php b/tests/Unit/Domain/Models/Entities/ActorTest.php index cda40c1..65942dc 100644 --- a/tests/Unit/Domain/Models/Entities/ActorTest.php +++ b/tests/Unit/Domain/Models/Entities/ActorTest.php @@ -12,36 +12,34 @@ use Ramsey\Uuid\Uuid; covers(Actor::class); describe('actor', function (): void { - beforeEach(function (): void { - $this->displayName = new ActorDisplayName('John Doe'); - }); + $displayName = new ActorDisplayName('John Doe'); - it('hydrates an actor', function (): void { + it('hydrates an actor', function () use ($displayName): void { // GIVEN $id = ActorId::generate(); // WHEN $actor = Actor::hydrate( $id, - $this->displayName + $displayName, ); // THEN expect($actor->id())->toBe($id) - ->and($actor->displayName)->toBe($this->displayName) + ->and($actor->displayName)->toBe($displayName) ->and($actor->updatedAt)->toBeNull() ->and($actor->updatedAt())->toBeNull(); }); - it('hydrates an actor with an explicit updatedAt date', function (): void { + it('hydrates an actor with an explicit updatedAt date', function () use ($displayName): void { // GIVEN $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); // WHEN $actor = Actor::hydrate( ActorId::generate(), - $this->displayName, - $updatedAt + $displayName, + $updatedAt, ); // THEN @@ -49,13 +47,13 @@ describe('actor', function (): void { ->and($actor->updatedAt())->toBe($updatedAt); }); - it('returns the creation date extracted from the id', function (): void { + it('returns the creation date extracted from the id', function () use ($displayName): 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, - $this->displayName + $displayName, ); // WHEN diff --git a/tests/Unit/Domain/Models/Entities/BrandTest.php b/tests/Unit/Domain/Models/Entities/BrandTest.php index 04777fd..cb80b26 100644 --- a/tests/Unit/Domain/Models/Entities/BrandTest.php +++ b/tests/Unit/Domain/Models/Entities/BrandTest.php @@ -17,44 +17,42 @@ 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'); - }); + $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'); afterEach(function (): void { CarbonImmutable::setTestNow(); }); - it('creates a brand', function (): void { + it('creates a brand', function () use ($code, $name, $logo, $rcs, $contact): void { // GIVEN $id = BrandId::generate(); // WHEN $brand = Brand::hydrate( $id, - $this->code, - $this->name, - $this->logo, - $this->rcs, - $this->contact + $code, + $name, + $logo, + $rcs, + $contact, ); // THEN expect($brand->id())->toBe($id) - ->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->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->updatedAt)->toBeNull() ->and($brand->updatedAt())->toBeNull(); }); - it('creates a brand with an explicit updatedAt date', function (): void { + it('creates a brand with an explicit updatedAt date', function () use ($code, $name, $logo, $rcs, $contact): void { // GIVEN $id = BrandId::generate(); $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); @@ -62,12 +60,12 @@ describe('brand', function (): void { // WHEN $brand = Brand::hydrate( $id, - $this->code, - $this->name, - $this->logo, - $this->rcs, - $this->contact, - $updatedAt + $code, + $name, + $logo, + $rcs, + $contact, + $updatedAt, ); // THEN @@ -75,49 +73,49 @@ describe('brand', function (): void { ->and($brand->updatedAt())->toBe($updatedAt); }); - it('creates a new brand with a freshly generated id', function (): void { + it('creates a new brand with a freshly generated id', function () use ($code, $name, $logo, $rcs, $contact): void { // GIVEN / WHEN $brand = Brand::create( - $this->code, - $this->name, - $this->logo, - $this->rcs, - $this->contact + $code, + $name, + $logo, + $rcs, + $contact, ); // THEN expect($brand->id())->toBeInstanceOf(BrandId::class) - ->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->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->updatedAt)->toBeNull() ->and($brand->updatedAt())->toBeNull(); }); - it('generates a different id for each created brand', function (): void { + it('generates a different id for each created brand', function () use ($code, $name, $logo, $rcs, $contact): void { // GIVEN / WHEN $first = Brand::create( - $this->code, - $this->name, - $this->logo, - $this->rcs, - $this->contact + $code, + $name, + $logo, + $rcs, + $contact, ); $second = Brand::create( - $this->code, - $this->name, - $this->logo, - $this->rcs, - $this->contact + $code, + $name, + $logo, + $rcs, + $contact, ); // THEN expect($first->id()->equals($second->id()))->toBeFalse(); }); - it('updates a brand logo and sets updatedAt timestamp', function (): void { + it('updates a brand logo and sets updatedAt timestamp', function () use ($code, $name, $rcs, $contact): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15 12:00:00 UTC'); $id = BrandId::generate(); @@ -125,11 +123,11 @@ describe('brand', function (): void { $logoNew = new PicturePath('files/picture-new.jpg'); $brand = Brand::hydrate( $id, - $this->code, - $this->name, + $code, + $name, $logoOld, - $this->rcs, - $this->contact + $rcs, + $contact, ); // WHEN @@ -137,16 +135,16 @@ describe('brand', function (): void { // THEN expect($brand->id())->toBe($id) - ->and($brand->code)->toBe($this->code) - ->and($brand->name)->toBe($this->name) + ->and($brand->code)->toBe($code) + ->and($brand->name)->toBe($name) ->and($brand->logo)->toBe($logoNew) - ->and($brand->rcs)->toBe($this->rcs) - ->and($brand->contact)->toBe($this->contact) + ->and($brand->rcs)->toBe($rcs) + ->and($brand->contact)->toBe($contact) ->and($brand->updatedAt)->not->toBeNull() ->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00'); }); - it('updates a brand rcs and sets updatedAt timestamp', function (): void { + it('updates a brand rcs and sets updatedAt timestamp', function () use ($code, $name, $logo, $contact): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15 14:30:00 UTC'); $id = BrandId::generate(); @@ -154,11 +152,11 @@ describe('brand', function (): void { $rcsNew = new Rcs('RCS NANTES 444 555 666'); $brand = Brand::hydrate( $id, - $this->code, - $this->name, - $this->logo, + $code, + $name, + $logo, $rcsOld, - $this->contact + $contact, ); // WHEN @@ -166,16 +164,16 @@ describe('brand', function (): void { // THEN expect($brand->id())->toBe($id) - ->and($brand->code)->toBe($this->code) - ->and($brand->name)->toBe($this->name) - ->and($brand->logo)->toBe($this->logo) + ->and($brand->code)->toBe($code) + ->and($brand->name)->toBe($name) + ->and($brand->logo)->toBe($logo) ->and($brand->rcs)->toBe($rcsNew) - ->and($brand->contact)->toBe($this->contact) + ->and($brand->contact)->toBe($contact) ->and($brand->updatedAt)->not->toBeNull() ->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 14:30:00'); }); - it('updates a brand contact and sets updatedAt timestamp', function (): void { + it('updates a brand contact and sets updatedAt timestamp', function () use ($code, $name, $logo, $rcs): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15 16:45:00 UTC'); $id = BrandId::generate(); @@ -183,11 +181,11 @@ describe('brand', function (): void { $contactNew = new Email('contact-new@example.com'); $brand = Brand::hydrate( $id, - $this->code, - $this->name, - $this->logo, - $this->rcs, - $contactOld + $code, + $name, + $logo, + $rcs, + $contactOld, ); // WHEN @@ -195,26 +193,26 @@ describe('brand', function (): void { // THEN expect($brand->id())->toBe($id) - ->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->code)->toBe($code) + ->and($brand->name)->toBe($name) + ->and($brand->logo)->toBe($logo) + ->and($brand->rcs)->toBe($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'); }); - it('returns the creation date extracted from the id', function (): void { + it('returns the creation date extracted from the id', function () use ($code, $name, $logo, $rcs, $contact): void { // GIVEN $expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC')); $id = new BrandId(Uuid::uuid7($expected)); $brand = Brand::hydrate( $id, - $this->code, - $this->name, - $this->logo, - $this->rcs, - $this->contact + $code, + $name, + $logo, + $rcs, + $contact, ); // WHEN diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 8b79003..0790326 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -6,7 +6,6 @@ namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; use DateTime; -use Domain\Enums\BarcodeFormat; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignName; @@ -15,15 +14,20 @@ 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\CouponIncoCode; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponName; +use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; +use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; -use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\GS1; -use Domain\Models\ValueObjects\IncoCode; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; 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 Ramsey\Uuid\Uuid; @@ -35,77 +39,90 @@ use Shared\ValueObjects\Money; covers(CampaignRequest::class); describe('campaign request', function (): void { - beforeEach(function (): void { - $this->requestedCoupon = new RequestedCoupon( - new CouponName('Coupon A'), - new RequestedCouponQuantity(100), - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - new MoneyAmount(new Money(Currency::EUR, 300)), - new ProcessingCode('PC1'), - 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::BENELUX) - ], - [new IncoCode(35784), new IncoCode(15522)], - new PrintRules(maxPerCustomer: 3), - new GS1('01234567890128', BarcodeFormat::EAN13) - ); - $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.'); - }); + $requestedCoupon = new RequestedCoupon( + couponName: 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)), + couponPicture: new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/mobileThumbnail.jpg') + ), + couponProcessingCodes: new CouponProcessingCodeArray([ + new CouponProcessingCode('ABC1234567890', Country::FRANCE), + ]), + couponValidityInformations: new CouponValidityInformationArray([ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponLegalInformations: new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) + ]), + couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + printRules: new PrintRules(maxPerCustomer: 3), + ); + $code = new BrandCode('AAA01'); + $name = new CampaignName('Campaign A'); + $applicant = new Applicant('John', 'Doe', new Email('john.doe@example.com')); + $campaignType = CampaignRequestType::CLUB; + $status = CampaignRequestStatus::CREATED; + $transformedStatus = CampaignRequestStatus::TRANSFORMED; + $rejectedStatus = CampaignRequestStatus::REJECTED; + $rejectionReason = new RejectionReason('Missing legal documents.'); afterEach(function (): void { CarbonImmutable::setTestNow(); }); - it('hydrates a campaign request with all its fields', function (): void { + it('hydrates a campaign request with all its fields', function () use ($code, $name, $applicant, $campaignType, $requestedCoupon, $status): void { // GIVEN $id = CampaignRequestId::generate(); // WHEN $campaignRequest = CampaignRequest::hydrate( $id, - $this->code, - $this->name, - $this->applicant, - $this->campaignType, - $this->requestedCoupon, - $this->status, + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $status, ); // THEN expect($campaignRequest->id())->toBe($id) - ->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->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->updatedAt)->toBeNull() ->and($campaignRequest->updatedAt())->toBeNull(); }); - it('hydrates a campaign request with an explicit updatedAt date', function (): void { + it('hydrates a campaign request with an explicit updatedAt date', function () use ( + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $status, + ): void { // GIVEN $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); // WHEN $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), - $this->code, - $this->name, - $this->applicant, - $this->campaignType, - $this->requestedCoupon, - $this->status, + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $status, null, $updatedAt, ); @@ -115,34 +132,49 @@ describe('campaign request', function (): void { ->and($campaignRequest->updatedAt())->toBe($updatedAt); }); - it('hydrates a rejected campaign request with a rejection reason', function (): void { + it('hydrates a rejected campaign request with a rejection reason', function () use ( + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $rejectedStatus, + $rejectionReason, + ): void { // GIVEN / WHEN $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), - $this->code, - $this->name, - $this->applicant, - $this->campaignType, - $this->requestedCoupon, - $this->rejectedStatus, - $this->rejectionReason, + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $rejectedStatus, + $rejectionReason, ); // THEN - expect($campaignRequest->status)->toBe($this->rejectedStatus) - ->and($campaignRequest->rejectionReason)->toBe($this->rejectionReason); + expect($campaignRequest->status)->toBe($rejectedStatus) + ->and($campaignRequest->rejectionReason)->toBe($rejectionReason); }); - it('rejects a rejected campaign request without a rejection reason', function (): void { + it('rejects a rejected campaign request without a rejection reason', function () use ( + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $rejectedStatus, + ): void { // GIVEN / WHEN $action = fn (): CampaignRequest => CampaignRequest::hydrate( CampaignRequestId::generate(), - $this->code, - $this->name, - $this->applicant, - $this->campaignType, - $this->requestedCoupon, - $this->rejectedStatus, + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $rejectedStatus, ); // THEN @@ -152,17 +184,25 @@ describe('campaign request', function (): void { ); }); - it('rejects a non-rejected campaign request with a rejection reason', function (): void { + it('rejects a non-rejected campaign request with a rejection reason', function () use ( + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $status, + $rejectionReason, + ): void { // WHEN $action = fn (): CampaignRequest => CampaignRequest::hydrate( CampaignRequestId::generate(), - $this->code, - $this->name, - $this->applicant, - $this->campaignType, - $this->requestedCoupon, - $this->status, - $this->rejectionReason, + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $status, + $rejectionReason, ); // THEN @@ -172,25 +212,33 @@ describe('campaign request', function (): void { ); }); - it('rejects a campaign request and sets the rejection reason and updatedAt', function (): void { + it('rejects a campaign request and sets the rejection reason and updatedAt', function () use ( + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $status, + $rejectionReason, + ): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15 12:00:00 UTC'); $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), - $this->code, - $this->name, - $this->applicant, - $this->campaignType, - $this->requestedCoupon, - $this->status, + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $status, ); // WHEN - $rejected = $campaignRequest->reject($this->rejectionReason); + $rejected = $campaignRequest->reject($rejectionReason); // THEN expect($rejected->status)->toBe(CampaignRequestStatus::REJECTED) - ->and($rejected->rejectionReason)->toBe($this->rejectionReason) + ->and($rejected->rejectionReason)->toBe($rejectionReason) ->and($rejected->id())->toBe($campaignRequest->id()) ->and($rejected->brandCode)->toBe($campaignRequest->brandCode) ->and($rejected->name)->toBe($campaignRequest->name) @@ -201,35 +249,50 @@ describe('campaign request', function (): void { ->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 { + it('rejects rejecting a transformed campaign request', function () use ( + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $transformedStatus, + $rejectionReason, + ): void { // GIVEN $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), - $this->code, - $this->name, - $this->applicant, - $this->campaignType, - $this->requestedCoupon, - $this->transformedStatus, + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $transformedStatus, ); // WHEN - $action = fn (): CampaignRequest => $campaignRequest->reject($this->rejectionReason); + $action = fn (): CampaignRequest => $campaignRequest->reject($rejectionReason); // THEN expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.'); }); - it('rejects rejecting an already rejected campaign request', function (): void { + it('rejects rejecting an already rejected campaign request', function () use ( + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $rejectedStatus, + ): void { // GIVEN $campaignRequest = CampaignRequest::hydrate( CampaignRequestId::generate(), - $this->code, - $this->name, - $this->applicant, - $this->campaignType, - $this->requestedCoupon, - $this->rejectedStatus, + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $rejectedStatus, new RejectionReason('First reason.'), ); @@ -240,18 +303,18 @@ describe('campaign request', function (): void { expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.'); }); - it('returns the creation date extracted from the id', function (): void { + it('returns the creation date extracted from the id', function () use ($code, $name, $applicant, $campaignType, $requestedCoupon, $status): 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, - $this->code, - $this->name, - $this->applicant, - $this->campaignType, - $this->requestedCoupon, - $this->status, + $code, + $name, + $applicant, + $campaignType, + $requestedCoupon, + $status, ); // WHEN diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index 403200c..e48d8a5 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -5,92 +5,83 @@ declare(strict_types=1); 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; +use Domain\Models\ValueObjects\Campaign\CampaignCouponArray; use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; +use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\PrintRules; -use Domain\Models\ValueObjects\ProcessingCode; use Ramsey\Uuid\Uuid; covers(Campaign::class); describe('campaign', function (): void { - 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), - Country::FRANCE, - new PrintRules(maxPerCustomer: 3) - ) - ]; - $this->brandCode = new BrandCode('AAA01'); - $this->actorId = ActorId::generate(); - }); + $code = new CampaignCode('Camp100'); + $name = new CampaignName('Name'); + $publication = \boolval(0); + $printAllAtOnce = \boolval(1); + $coupons = new CampaignCouponArray([ + new CampaignCoupon( + new CouponCode('AAA01'), + new PrintRules(maxPerCustomer: 3) + ) + ]); + $brandCode = new BrandCode('AAA01'); + $actorId = ActorId::generate(); afterEach(function (): void { CarbonImmutable::setTestNow(); }); - it('creates a campaign', function (): void { + it('creates a campaign', function () use ( + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId, + ): void { // GIVEN $id = CampaignId::generate(); // WHEN $campaign = Campaign::hydrate( $id, - $this->code, - $this->name, - $this->publication, - $this->printAllAtOnce, - $this->coupons, - $this->brandCode, - $this->actorId + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId ); // THEN expect($campaign->id())->toBe($id) - ->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->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->updatedAt)->toBeNull() ->and($campaign->updatedAt())->toBeNull(); }); - it('creates a campaign without coupon', function (): void { - // GIVEN - $id = CampaignId::generate(); - - // WHEN - $campaign = Campaign::hydrate( - $id, - $this->code, - $this->name, - $this->publication, - $this->printAllAtOnce, - [], - $this->brandCode, - $this->actorId - ); - - // THEN - expect($campaign->coupons)->toBe([]); - }); - - it('creates a coupon with an explicit updatedAt date', function (): void { + it('creates a coupon with an explicit updatedAt date', function () use ( + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId, + ): void { // GIVEN $id = CampaignId::generate(); $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); @@ -98,13 +89,13 @@ describe('campaign', function (): void { // WHEN $campaign = Campaign::hydrate( $id, - $this->code, - $this->name, - $this->publication, - $this->printAllAtOnce, - $this->coupons, - $this->brandCode, - $this->actorId, + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId, $updatedAt ); @@ -113,68 +104,92 @@ describe('campaign', function (): void { ->and($campaign->updatedAt())->toBe($updatedAt); }); - it('creates a new campaign with a freshly generated id', function (): void { + it('creates a new campaign with a freshly generated id', function () use ( + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId, + ): void { // GIVEN / WHEN $campaign = Campaign::create( - $this->code, - $this->name, - $this->publication, - $this->printAllAtOnce, - $this->coupons, - $this->brandCode, - $this->actorId + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId ); // THEN expect($campaign->id())->toBeInstanceOf(CampaignId::class) - ->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->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->updatedAt)->toBeNull() ->and($campaign->updatedAt())->toBeNull(); }); - it('generates a different id for each created campaign', function (): void { + it('generates a different id for each created campaign', function () use ( + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId, + ): void { // GIVEN / WHEN $firstCampaign = Campaign::create( - $this->code, - $this->name, - $this->publication, - $this->printAllAtOnce, - $this->coupons, - $this->brandCode, - $this->actorId + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId ); $secondCampaign = Campaign::create( - $this->code, - $this->name, - $this->publication, - $this->printAllAtOnce, - $this->coupons, - $this->brandCode, - $this->actorId + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId ); // THEN expect($firstCampaign->id()->equals($secondCampaign->id()))->toBeFalse(); }); - it('returns the creation date extracted from the id', function (): void { + it('returns the creation date extracted from the id', function () use ( + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId, + ): 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, - $this->code, - $this->name, - $this->publication, - $this->printAllAtOnce, - $this->coupons, - $this->brandCode, - $this->actorId + $code, + $name, + $publication, + $printAllAtOnce, + $coupons, + $brandCode, + $actorId ); // WHEN @@ -184,21 +199,20 @@ describe('campaign', function (): void { 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 { + it('rejects a campaign with a non-coupon element', function () use ($code, $name, $publication, $printAllAtOnce, $brandCode, $actorId): void { // GIVEN - $id = CampaignId::generate(); - $coupons = ['PC1']; + $id = CampaignId::generate(); // WHEN / THEN expect(fn (): Campaign => Campaign::hydrate( $id, - $this->code, - $this->name, - $this->publication, - $this->printAllAtOnce, - $coupons, - $this->brandCode, - $this->actorId - ))->toThrow(\DomainException::class, 'Campaign coupons must be CampaignCoupon instances.'); + $code, + $name, + $publication, + $printAllAtOnce, + new CampaignCouponArray(['PC1']), + $brandCode, + $actorId + ))->toThrow(\DomainException::class, 'Coupons must be CampaignCoupon instances.'); }); }); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index 6b54f95..003084b 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -6,7 +6,6 @@ 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; @@ -15,17 +14,22 @@ 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\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; -use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponMovement; +use Domain\Models\ValueObjects\Coupon\CouponMovementArray; +use Domain\Models\ValueObjects\Coupon\CouponName; +use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; +use Domain\Models\ValueObjects\Coupon\CouponProductArray; +use Domain\Models\ValueObjects\Coupon\CouponQuantity; 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\Coupon\CouponValidityInformationArray; 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 Shared\ValueObjects\Money; @@ -33,274 +37,277 @@ 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.', - Country::FRANCE - ) - ]; - $this->couponLegalInformation = [ - new CouponLegalInformation( - 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - 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->gs1 = new GS1('110101010', BarcodeFormat::EAN13); - }); + $code = new CouponCode('AAA01'); + $name = new CouponName('Name'); + $amount = new MoneyAmount(new Money(Currency::EUR, 1)); + $picture = new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/mobileThumbnail.jpg'), + ); + $diffusion = new CouponDiffusion(new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))); + $validityInformations = new CouponValidityInformationArray([ + new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE, + ) + ]); + $legalInformations = new CouponLegalInformationArray([ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE, + ) + ]); + $processingCode = new CouponProcessingCodeArray([ + new CouponProcessingCode('5425003944130', Country::FRANCE), + ]); + $brandCode = new BrandCode('AAA01'); + $createdBy = ActorId::generate(); + $products = new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]); + $movements = new CouponMovementArray([new CouponMovement(100)]); + $quantity = new CouponQuantity(100); + $availabilityCalendar = new CouponAvailabilityCalendar([ + new Period( + new DateTime('2026-08-25'), + new DateTime('2026-10-31'), + )]); afterEach(function (): void { CarbonImmutable::setTestNow(); }); - it('creates a coupon', function (): void { + it('creates a coupon', function () use ( + $code, + $name, + $amount, + $picture, + $diffusion, + $validityInformations, + $legalInformations, + $processingCode, + $brandCode, + $createdBy, + $products, + $movements, + $quantity, + $availabilityCalendar, + ): void { // GIVEN $id = CouponId::generate(); // 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->gs1, + id: $id, + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: $validityInformations, + legalInformations: $legalInformations, + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: $products, + movements: $movements, + quantity: $quantity, + availabilityCalendar: $availabilityCalendar, ); // THEN expect($coupon->id())->toBe($id) - ->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->gs1)->toBe($this->gs1) + ->and($coupon->code)->toBe($code) + ->and($coupon->name)->toBe($name) + ->and($coupon->amount)->toBe($amount) + ->and($coupon->picture)->toBe($picture) + ->and($coupon->diffusion)->toBe($diffusion) + ->and($coupon->validityInformations)->toBe($validityInformations) + ->and($coupon->legalInformations)->toBe($legalInformations) + ->and($coupon->processingCode)->toBe($processingCode) + ->and($coupon->brandCode)->toBe($brandCode) + ->and($coupon->createdBy)->toBe($createdBy) + ->and($coupon->products)->toBe($products) + ->and($coupon->availabilityCalendar)->toBe($availabilityCalendar) + ->and($coupon->movements)->toBe($movements) ->and($coupon->updatedAt)->toBeNull() ->and($coupon->updatedAt())->toBeNull(); }); - it('hydrates a coupon without gs1', function (): void { + it('hydrates a coupon without availability calendar', function () use ( + $code, + $name, + $amount, + $picture, + $diffusion, + $validityInformations, + $legalInformations, + $processingCode, + $brandCode, + $createdBy, + $products, + $movements, + $quantity, + ): 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, - availabilityCalendar: $this->availabilityCalendar - ); - - // THEN - expect($coupon->products)->toBe($this->products) - ->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar) - ->and($coupon->gs1)->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, - gs1: $this->gs1 + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: $validityInformations, + legalInformations: $legalInformations, + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: $products, + movements: $movements, + quantity: $quantity, ); // THEN expect($coupon->availabilityCalendar)->toBeNull() - ->and($coupon->products)->toBe($this->products) - ->and($coupon->gs1)->toBe($this->gs1); + ->and($coupon->products)->toBe($products); }); - it('hydrates a coupon without products', function (): void { + it('hydrates a coupon without products', function () use ( + $code, + $name, + $amount, + $picture, + $diffusion, + $validityInformations, + $legalInformations, + $processingCode, + $brandCode, + $createdBy, + $movements, + $quantity, + $availabilityCalendar, + ): 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, - gs1: $this->gs1 + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: $validityInformations, + legalInformations: $legalInformations, + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: new CouponProductArray(), + movements: $movements, + quantity: $quantity, + availabilityCalendar: $availabilityCalendar, ); // THEN - expect($coupon->products)->toBe([]) - ->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar) - ->and($coupon->gs1)->toBe($this->gs1); + expect($coupon->products->value)->toBe([]) + ->and($coupon->availabilityCalendar)->toBe($availabilityCalendar); }); - it('hydrates a coupon with only required fields', function (): void { + it('throws when validity information contains an invalid item', function () use ( + $code, + $name, + $amount, + $picture, + $diffusion, + $legalInformations, + $processingCode, + $brandCode, + $createdBy, + $products, + $movements, + $quantity, + $availabilityCalendar, + ): void { // GIVEN $id = CouponId::generate(); - // WHEN - $coupon = Coupon::hydrate( + // WHEN / THEN + expect(fn (): 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->products)->toBe([]) - ->and($coupon->availabilityCalendar)->toBeNull() - ->and($coupon->gs1)->toBeNull() - ->and($coupon->updatedAt)->toBeNull() - ->and($coupon->updatedAt())->toBeNull(); + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: new CouponValidityInformationArray(['Not a CouponValidityInformation']), + legalInformations: $legalInformations, + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: $products, + movements: $movements, + quantity: $quantity, + availabilityCalendar: $availabilityCalendar, + ))->toThrow(\DomainException::class, 'Value of CouponValidityInformationArray must be CouponValidityInformation instances.'); }); - it('accepts empty validity and legal information lists', function (): void { - // GIVEN - $id = CouponId::generate(); - - // WHEN - $coupon = Coupon::hydrate( - $id, - $this->code, - $this->name, - $this->amount, - $this->picture, - $this->diffusion, - $this->validity, - [], - [], - $this->processingCode, - $this->brandCode, - $this->createdBy, - $this->products, - $this->availabilityCalendar, - $this->gs1, - ); - - // THEN - expect($coupon->couponValidityInformation)->toBe([]) - ->and($coupon->couponLegalInformation)->toBe([]); - }); - - it('throws when validity information contains an invalid item', function (): void { + it('throws when legal information contains an invalid item', function () use ( + $code, + $name, + $amount, + $picture, + $diffusion, + $validityInformations, + $processingCode, + $brandCode, + $createdBy, + $products, + $movements, + $quantity, + $availabilityCalendar, + ): 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->gs1, - ))->toThrow(\DomainException::class, 'Coupon validity information must be CouponValidityInformation instances.'); + expect(fn (): Coupon => Coupon::hydrate( + id: $id, + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: $validityInformations, + legalInformations: new CouponLegalInformationArray(['Not a CouponLegalInformation']), + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: $products, + movements: $movements, + quantity: $quantity, + availabilityCalendar: $availabilityCalendar, + ))->toThrow(\DomainException::class, 'Value of CouponLegalInformationArray must be CouponLegalInformation 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->gs1, - ))->toThrow(\DomainException::class, 'Coupon legal information must be CouponLegalInformation instances.'); - }); - - it('hydrates a coupon with an explicit updatedAt date', function (): void { + it('hydrates a coupon with an explicit updatedAt date', function () use ( + $code, + $name, + $amount, + $picture, + $diffusion, + $validityInformations, + $legalInformations, + $processingCode, + $brandCode, + $createdBy, + $products, + $movements, + $quantity, + $availabilityCalendar, + ): void { // GIVEN $id = CouponId::generate(); $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); @@ -308,21 +315,21 @@ describe('coupon', function (): void { // 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->gs1, - $updatedAt + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: $validityInformations, + legalInformations: $legalInformations, + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: $products, + movements: $movements, + quantity: $quantity, + availabilityCalendar: $availabilityCalendar, + updatedAt: $updatedAt, ); // THEN @@ -330,104 +337,149 @@ describe('coupon', function (): void { ->and($coupon->updatedAt())->toBe($updatedAt); }); - it('creates a new coupon with a freshly generated id', function (): void { + it('creates a new coupon with a freshly generated id', function () use ( + $code, + $name, + $amount, + $picture, + $diffusion, + $validityInformations, + $legalInformations, + $processingCode, + $brandCode, + $createdBy, + $products, + $movements, + $quantity, + $availabilityCalendar, + ): void { // GIVEN / WHEN $coupon = Coupon::create( - $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->gs1, + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: $validityInformations, + legalInformations: $legalInformations, + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: $products, + movements: $movements, + quantity: $quantity, + availabilityCalendar: $availabilityCalendar, ); // THEN expect($coupon->id())->toBeInstanceOf(CouponId::class) - ->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->gs1)->toBe($this->gs1) + ->and($coupon->code)->toBe($code) + ->and($coupon->name)->toBe($name) + ->and($coupon->amount)->toBe($amount) + ->and($coupon->picture)->toBe($picture) + ->and($coupon->diffusion)->toBe($diffusion) + ->and($coupon->validityInformations)->toBe($validityInformations) + ->and($coupon->legalInformations)->toBe($legalInformations) + ->and($coupon->processingCode)->toBe($processingCode) + ->and($coupon->brandCode)->toBe($brandCode) + ->and($coupon->createdBy)->toBe($createdBy) + ->and($coupon->products)->toBe($products) + ->and($coupon->movements)->toBe($movements) + ->and($coupon->quantity)->toBe($quantity) + ->and($coupon->availabilityCalendar)->toBe($availabilityCalendar) ->and($coupon->updatedAt)->toBeNull() ->and($coupon->updatedAt())->toBeNull(); }); - it('generates a different id for each created coupon', function (): void { + it('generates a different id for each created coupon', function () use ( + $code, + $name, + $amount, + $picture, + $diffusion, + $validityInformations, + $legalInformations, + $processingCode, + $brandCode, + $createdBy, + $products, + $movements, + $quantity, + $availabilityCalendar, + ): void { // GIVEN / WHEN $firstCoupon = Coupon::create( - $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->gs1, + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: $validityInformations, + legalInformations: $legalInformations, + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: $products, + movements: $movements, + quantity: $quantity, + availabilityCalendar: $availabilityCalendar, ); $secondCoupon = Coupon::create( - $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->gs1, + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: $validityInformations, + legalInformations: $legalInformations, + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: $products, + movements: $movements, + quantity: $quantity, + availabilityCalendar: $availabilityCalendar, ); // THEN expect($firstCoupon->id()->equals($secondCoupon->id()))->toBeFalse(); }); - it('returns the creation date extracted from the id', function (): void { + it('returns the creation date extracted from the id', function () use ( + $code, + $name, + $amount, + $picture, + $diffusion, + $validityInformations, + $legalInformations, + $processingCode, + $brandCode, + $createdBy, + $products, + $movements, + $quantity, + $availabilityCalendar, + ): 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, - $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->gs1, + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: $validityInformations, + legalInformations: $legalInformations, + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: $products, + movements: $movements, + quantity: $quantity, + availabilityCalendar: $availabilityCalendar, ); // WHEN diff --git a/tests/Unit/Domain/Models/Entities/StockTest.php b/tests/Unit/Domain/Models/Entities/StockTest.php index a30b6d7..6d4d469 100644 --- a/tests/Unit/Domain/Models/Entities/StockTest.php +++ b/tests/Unit/Domain/Models/Entities/StockTest.php @@ -7,7 +7,7 @@ namespace Tests\Unit\Domain\Models\Entities; use Domain\Models\Entities\Stock; use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Campaign\CampaignCode; -use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\Stock\StockAdjustment; use Domain\Models\ValueObjects\Stock\StockAmount; use Domain\Models\ValueObjects\Stock\StockId; @@ -16,40 +16,38 @@ 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())]; - }); + $stockAmount = new StockAmount(10); + $campaignCode = new CampaignCode('AAA01'); + $couponCode = new CouponCode('AAA01'); + $stockAdjustment = [new StockAdjustment(5, 'Inventory correction', ActorId::generate())]; - it('hydrates a stock', function (): void { + it('hydrates a stock', function () use ($stockAmount, $campaignCode, $couponCode, $stockAdjustment): void { // GIVEN $id = StockId::generate(); // WHEN $stock = Stock::hydrate( $id, - $this->stockAmount, - $this->campaignCode, - $this->processingCode, - $this->stockAdjustment + $stockAmount, + $campaignCode, + $couponCode, + $stockAdjustment, ); // THEN expect($stock->id())->toBe($id) - ->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->stockAmount)->toBe($stockAmount) + ->and($stock->campaignCode)->toBe($campaignCode) + ->and($stock->couponCode)->toBe($couponCode) + ->and($stock->stockAdjustment)->toBe($stockAdjustment) ->and($stock->updatedAt)->toBeNull() ->and($stock->updatedAt())->toBeNull(); }); - it('hydrates a stock with several stock adjustments', function (): void { + it('hydrates a stock with several stock adjustments', function () use ($stockAmount, $campaignCode, $couponCode): void { // GIVEN - $id = StockId::generate(); - $stockAdjustment = [ + $id = StockId::generate(); + $severalAdjustments = [ new StockAdjustment(5, 'Inventory correction', ActorId::generate()), new StockAdjustment(10, 'Inventory correction', ActorId::generate()), ]; @@ -57,70 +55,70 @@ describe('stock', function (): void { // WHEN $stock = Stock::hydrate( $id, - $this->stockAmount, - $this->campaignCode, - $this->processingCode, - $stockAdjustment, + $stockAmount, + $campaignCode, + $couponCode, + $severalAdjustments, ); // THEN expect($stock->id())->toBe($id) - ->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->stockAmount)->toBe($stockAmount) + ->and($stock->campaignCode)->toBe($campaignCode) + ->and($stock->couponCode)->toBe($couponCode) + ->and($stock->stockAdjustment)->toBe($severalAdjustments) ->and($stock->updatedAt)->toBeNull() ->and($stock->updatedAt())->toBeNull(); }); - it('hydrates a stock without stock adjustement', function (): void { + it('hydrates a stock without stock adjustement', function () use ($stockAmount, $campaignCode, $couponCode): void { // GIVEN $id = StockId::generate(); // WHEN $stock = Stock::hydrate( $id, - $this->stockAmount, - $this->campaignCode, - $this->processingCode + $stockAmount, + $campaignCode, + $couponCode, ); // THEN expect($stock->id())->toBe($id) - ->and($stock->stockAmount)->toBe($this->stockAmount) - ->and($stock->campaignCode)->toBe($this->campaignCode) - ->and($stock->processingCode)->toBe($this->processingCode) + ->and($stock->stockAmount)->toBe($stockAmount) + ->and($stock->campaignCode)->toBe($campaignCode) + ->and($stock->couponCode)->toBe($couponCode) ->and($stock->stockAdjustment)->toBe([]) ->and($stock->updatedAt)->toBeNull() ->and($stock->updatedAt())->toBeNull(); }); - it('rejects a stock with a non-stock-adjustment element', function (): void { + it('rejects a stock with a non-stock-adjustment element', function () use ($stockAmount, $campaignCode, $couponCode): void { // GIVEN - $id = StockId::generate(); - $stockAdjustment = [new StockAmount(5)]; + $id = StockId::generate(); + $invalidAdjustment = [new StockAmount(5)]; // WHEN / THEN expect(fn (): Stock => Stock::hydrate( $id, - $this->stockAmount, - $this->campaignCode, - $this->processingCode, - $stockAdjustment, + $stockAmount, + $campaignCode, + $couponCode, + $invalidAdjustment, ))->toThrow(\DomainException::class, 'Stock adjustment must be StockAdjustment instances.'); }); - it('hydrates a stock with an explicit updatedAt date', function (): void { + it('hydrates a stock with an explicit updatedAt date', function () use ($stockAmount, $campaignCode, $couponCode, $stockAdjustment): void { // GIVEN $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); // WHEN $stock = Stock::hydrate( StockId::generate(), - $this->stockAmount, - $this->campaignCode, - $this->processingCode, - $this->stockAdjustment, + $stockAmount, + $campaignCode, + $couponCode, + $stockAdjustment, $updatedAt, ); @@ -129,16 +127,16 @@ describe('stock', function (): void { ->and($stock->updatedAt())->toBe($updatedAt); }); - it('returns the creation date extracted from the id', function (): void { + it('returns the creation date extracted from the id', function () use ($stockAmount, $campaignCode, $couponCode, $stockAdjustment): 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, - $this->stockAmount, - $this->campaignCode, - $this->processingCode, - $this->stockAdjustment + $stockAmount, + $campaignCode, + $couponCode, + $stockAdjustment ); // WHEN diff --git a/tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php index 82b09a3..13a8cc6 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Actor/ActorDisplayNameTest.php @@ -9,25 +9,22 @@ use Domain\Models\ValueObjects\Actor\ActorDisplayName; covers(ActorDisplayName::class); describe('actor displayname', function (): void { - beforeEach(function (): void { - $this->value = 'Johnny'; - $this->emptyValue = ''; - }); + $value = 'Johnny'; + $emptyValue = ''; - it('creates a valid actor displayname', function (): void { + it('creates a valid actor displayname', function () use ($value): void { // GIVEN / WHEN $name = new ActorDisplayName( - $this->value + $value ); // THEN - expect($name->value)->toBe($this->value); + expect($name->value)->toBe($value); }); - it('rejects invalid actor displayname', function (): void { + it('rejects invalid actor displayname', function () use ($emptyValue): void { // GIVEN / WHEN / THEN - expect(fn (): ActorDisplayName => new ActorDisplayName( - $this->emptyValue - ))->toThrow(\DomainException::class); + expect(fn (): ActorDisplayName => new ActorDisplayName($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 41a3762..c2f96aa 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Brand/BrandNameTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Brand/BrandNameTest.php @@ -9,26 +9,21 @@ use Domain\Models\ValueObjects\Brand\BrandName; covers(BrandName::class); describe('brand name', function (): void { - beforeEach(function (): void { - $this->value = 'Name'; - $this->emptyValue = ''; - }); + $value = 'Name'; + $emptyValue = ''; - it('creates a valid brand name', function (): void { + it('creates a valid brand name', function () use ($value): void { // GIVEN / WHEN - $name = new BrandName( - $this->value - ); + $name = new BrandName($value); // THEN - expect($name->value)->toBe($this->value); + expect($name->value)->toBe($value); }); - it('rejects invalid brand name', function (): void { + it('rejects invalid brand name', function () use ($emptyValue): void { // GIVEN / WHEN / THEN - expect(fn (): BrandName => new BrandName( - $this->emptyValue - ))->toThrow(\DomainException::class); + expect(fn (): BrandName => new BrandName($emptyValue)) + ->toThrow(\DomainException::class); }); it('rejects a name that exceeds the maximum allowed length', function (): void { diff --git a/tests/Unit/Domain/Models/ValueObjects/Brand/BrandSortCriteriaTest.php b/tests/Unit/Domain/Models/ValueObjects/Brand/BrandSortCriteriaTest.php new file mode 100644 index 0000000..e9e946e --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Brand/BrandSortCriteriaTest.php @@ -0,0 +1,34 @@ +field)->toBe(BrandSortField::Name) + ->and($criteria->direction)->toBe(\SortDirection::Ascending); + }); + + it('keeps the given field and direction', function (): void { + // GIVEN + $field = BrandSortField::Name; + $direction = \SortDirection::Descending; + + // WHEN + $criteria = new BrandSortCriteria($field, $direction); + + // THEN + expect($criteria->field)->toBe($field) + ->and($criteria->direction)->toBe($direction); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php index 939cefd..59bc1b3 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php @@ -9,23 +9,20 @@ use Domain\Models\ValueObjects\Campaign\CampaignCode; covers(CampaignCode::class); describe('campaign code', function (): void { - beforeEach(function (): void { - $this->code = 'Name'; - $this->emptyCode = ''; - }); + $code = 'Name'; + $emptyCode = ''; - it('creates a valid campaign code', function (): void { + it('creates a valid campaign code', function () use ($code): void { // GIVEN / WHEN - $name = new CampaignCode($this->code); + $name = new CampaignCode($code); // THEN - expect($name->code)->toBe($this->code); + expect($name->code)->toBe($code); }); - it('rejects invalid campaign code', function (): void { + it('rejects invalid campaign code', function () use ($emptyCode): void { // GIVEN / WHEN / THEN - expect(fn (): CampaignCode => new CampaignCode( - $this->emptyCode - ))->toThrow(\DomainException::class); + expect(fn (): CampaignCode => new CampaignCode($emptyCode)) + ->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponArrayTest.php new file mode 100644 index 0000000..8daae6e --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponArrayTest.php @@ -0,0 +1,57 @@ +value)->toBe($coupons); + }); + + it('rejects an empty array', function (): void { + // GIVEN / WHEN + $action = fn (): CampaignCouponArray => new CampaignCouponArray([]); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Coupons requires at least one CampaignCoupon.'); + }); + + it('rejects a non-coupon element', function (): void { + // GIVEN / WHEN + $action = fn (): CampaignCouponArray => new CampaignCouponArray(['PC1']); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Coupons must be CampaignCoupon instances.'); + }); + + it('rejects a non-coupon element mixed with valid coupons', function () use ($coupon): void { + // GIVEN + $coupons = [$coupon, 'PC1']; + + // WHEN + $action = fn (): CampaignCouponArray => new CampaignCouponArray($coupons); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Coupons must be CampaignCoupon instances.'); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php index e700f2a..9be21d3 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCouponTest.php @@ -5,78 +5,34 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\Campaign; use Domain\Models\ValueObjects\Campaign\CampaignCoupon; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; +use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\PrintRules; -use Domain\Models\ValueObjects\ProcessingCode; -use Shared\Enums\Country; 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); - }); + $couponCode = new CouponCode('AAA01'); + $printRules = new PrintRules(maxPerCustomer: 3); - it('creates a valid campaign coupon', function (): void { + it('creates a valid campaign coupon', function () use ($couponCode, $printRules): void { // GIVEN / WHEN $campaignCoupon = new CampaignCoupon( - $this->processingCode, - $this->quantity, - $this->availableIn, - $this->printRules + couponCode: $couponCode, + printRules: $printRules, ); // THEN - expect($campaignCoupon->processingCode)->toBe($this->processingCode) - ->and($campaignCoupon->quantity)->toBe($this->quantity) - ->and($campaignCoupon->availableIn)->toBe($this->availableIn) - ->and($campaignCoupon->printRules)->toBe($this->printRules); + expect($campaignCoupon->couponCode)->toBe($couponCode) + ->and($campaignCoupon->printRules)->toBe($printRules); }); - it('creates a valid campaign coupon whitout warning threshold quantity', function (): void { + it('creates a valid campaign coupon whitout print rules', function () use ($couponCode): void { // GIVEN / WHEN $campaignCoupon = new CampaignCoupon( - processingCode: $this->processingCode, - quantity: $this->quantity, - availableIn: $this->availableIn, - printRules: $this->printRules + couponCode: $couponCode, ); // THEN - 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 / WHEN - $campaignCoupon = new CampaignCoupon( - processingCode: $this->processingCode, - quantity: $this->quantity, - availableIn: $this->availableIn, - ); - - // THEN - expect($campaignCoupon->processingCode)->toBe($this->processingCode) - ->and($campaignCoupon->quantity)->toBe($this->quantity) - ->and($campaignCoupon->availableIn)->toBe($this->availableIn); - }); - - it('creates a valid campaign coupon whitout warning threshold quantity and print rules', function (): void { - // GIVEN / WHEN - $campaignCoupon = new CampaignCoupon( - processingCode: $this->processingCode, - quantity: $this->quantity, - availableIn: $this->availableIn - ); - - // THEN - expect($campaignCoupon->processingCode)->toBe($this->processingCode) - ->and($campaignCoupon->quantity)->toBe($this->quantity) - ->and($campaignCoupon->availableIn)->toBe($this->availableIn); + expect($campaignCoupon->couponCode)->toBe($couponCode); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php index ad4af96..770ee44 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignNameTest.php @@ -9,23 +9,20 @@ use Domain\Models\ValueObjects\Campaign\CampaignName; covers(CampaignName::class); describe('campaign name', function (): void { - beforeEach(function (): void { - $this->value = 'Name'; - $this->emptyValue = ''; - }); + $value = 'Name'; + $emptyValue = ''; - it('creates a valid campaign name', function (): void { + it('creates a valid campaign name', function () use ($value): void { // GIVEN / WHEN - $name = new CampaignName($this->value); + $name = new CampaignName($value); // THEN - expect($name->value)->toBe($this->value); + expect($name->value)->toBe($value); }); - it('rejects invalid campaign name', function (): void { + it('rejects invalid campaign name', function () use ($emptyValue): void { // GIVEN / WHEN / THEN - expect(fn (): CampaignName => new CampaignName( - $this->emptyValue - ))->toThrow(\DomainException::class); + expect(fn (): CampaignName => new CampaignName($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 730b5c6..ee75550 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/ApplicantTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/ApplicantTest.php @@ -10,33 +10,31 @@ use Shared\ValueObjects\Email; covers(Applicant::class); describe('applicant', function (): void { - beforeEach(function (): void { - $this->firstname = 'John'; - $this->lastname = 'Doe'; - $this->email = new Email('john.doe@example.com'); - }); + $firstname = 'John'; + $lastname = 'Doe'; + $email = new Email('john.doe@example.com'); - it('creates a valid applicant', function (): void { + it('creates a valid applicant', function () use ($firstname, $lastname, $email): void { // GIVEN / WHEN $applicant = new Applicant( - $this->firstname, - $this->lastname, - $this->email + $firstname, + $lastname, + $email, ); // THEN expect($applicant)->toBeInstanceOf(Applicant::class) - ->and($applicant->firstname)->toBe($this->firstname) - ->and($applicant->lastname)->toBe($this->lastname) - ->and($applicant->email)->toBe($this->email); + ->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 { + it('rejects an empty or whitespace-only firstname', function (string $invalidFirstname) use ($lastname, $email): void { // WHEN $action = fn (): Applicant => new Applicant( - $firstname, - $this->lastname, - $this->email + $invalidFirstname, + $lastname, + $email, ); // THEN @@ -46,42 +44,42 @@ describe('applicant', function (): void { 'spaces' => ' ', ]); - it('rejects a firstname that exceeds the maximum allowed length', function (): void { + it('rejects a firstname that exceeds the maximum allowed length', function () use ($lastname, $email): void { // GIVEN - $firstname = str_repeat('a', 201); + $tooLongFirstname = str_repeat('a', 201); // WHEN $action = fn (): Applicant => new Applicant( - $firstname, - $this->lastname, - $this->email + $tooLongFirstname, + $lastname, + $email, ); // THEN expect($action)->toThrow(\DomainException::class, 'Applicant firstname exceeds the maximum allowed length.'); }); - it('accepts a firstname of exactly the maximum length', function (): void { + it('accepts a firstname of exactly the maximum length', function () use ($lastname, $email): void { // GIVEN - $firstname = str_repeat('a', 200); + $maxLengthFirstname = str_repeat('a', 200); // WHEN $applicant = new Applicant( - $firstname, - $this->lastname, - $this->email + $maxLengthFirstname, + $lastname, + $email, ); // THEN expect(mb_strlen($applicant->firstname))->toBe(200); }); - it('rejects an empty or whitespace-only lastname', function (string $lastname): void { + it('rejects an empty or whitespace-only lastname', function (string $invalidLastname) use ($firstname, $email): void { // WHEN $action = fn (): Applicant => new Applicant( - $this->firstname, - $lastname, - $this->email + $firstname, + $invalidLastname, + $email, ); // THEN @@ -91,30 +89,30 @@ describe('applicant', function (): void { 'spaces' => ' ', ]); - it('rejects a lastname that exceeds the maximum allowed length', function (): void { + it('rejects a lastname that exceeds the maximum allowed length', function () use ($firstname, $email): void { // GIVEN - $lastname = str_repeat('a', 201); + $tooLongLastname = str_repeat('a', 201); // WHEN $action = fn (): Applicant => new Applicant( - $this->firstname, - $lastname, - $this->email + $firstname, + $tooLongLastname, + $email, ); // THEN expect($action)->toThrow(\DomainException::class, 'Applicant lastname exceeds the maximum allowed length.'); }); - it('accepts a lastname of exactly the maximum length', function (): void { + it('accepts a lastname of exactly the maximum length', function () use ($firstname, $email): void { // GIVEN - $lastname = str_repeat('a', 200); + $maxLengthLastname = str_repeat('a', 200); // WHEN $applicant = new Applicant( - $this->firstname, - $lastname, - $this->email + $firstname, + $maxLengthLastname, + $email, ); // THEN diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php index b85bdb3..a8ca643 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestFiltersTest.php @@ -11,43 +11,41 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus; covers(CampaignRequestFilters::class); describe('campaign request filters', function (): void { - beforeEach(function (): void { - $this->brandCode = new BrandCode('AAA01'); - $this->status = CampaignRequestStatus::CREATED; - }); + $brandCode = new BrandCode('AAA01'); + $status = CampaignRequestStatus::CREATED; - it('creates filters with both fields set', function (): void { + it('creates filters with both fields set', function () use ($brandCode, $status): void { // GIVEN / WHEN $filters = new CampaignRequestFilters( - brandCode: $this->brandCode, - status: $this->status + brandCode: $brandCode, + status: $status, ); // THEN - expect($filters->brandCode)->toBe($this->brandCode) - ->and($filters->status)->toBe($this->status); + expect($filters->brandCode)->toBe($brandCode) + ->and($filters->status)->toBe($status); }); - it('creates filters with only brandCode set', function (): void { + it('creates filters with only brandCode set', function () use ($brandCode): void { // GIVEN / WHEN $filters = new CampaignRequestFilters( - brandCode: $this->brandCode + brandCode: $brandCode, ); // THEN - expect($filters->brandCode)->toBe($this->brandCode) + expect($filters->brandCode)->toBe($brandCode) ->and($filters->status)->toBeNull(); }); - it('creates filters with only status set', function (): void { + it('creates filters with only status set', function () use ($status): void { // GIVEN / WHEN $filters = new CampaignRequestFilters( - status: $this->status + status: $status, ); // THEN expect($filters->brandCode)->toBeNull() - ->and($filters->status)->toBe($this->status); + ->and($filters->status)->toBe($status); }); it('rejects filters without any field set', function (): void { diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortClauseTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortClauseTest.php new file mode 100644 index 0000000..f78c0ea --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortClauseTest.php @@ -0,0 +1,25 @@ +field)->toBe($field) + ->and($clause->direction)->toBe($direction); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php index af31c54..42260c6 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/CampaignRequestSortCriteriaTest.php @@ -11,48 +11,42 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField; covers(CampaignRequestSortCriteria::class); describe('campaign request sort criteria', function (): void { - beforeEach(function (): void { - $this->singleClauses = [ - new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending), - ]; - $this->clauses = [ - new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending), - new CampaignRequestSortClause(field: CampaignRequestSortField::Status, direction: \SortDirection::Descending), - ]; - $this->invalidClauses = [CampaignRequestSortField::BrandCode]; - $this->emptyClauses = []; - }); + $singleClauses = [ + new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending), + ]; + $clauses = [ + new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending), + new CampaignRequestSortClause(field: CampaignRequestSortField::Status, direction: \SortDirection::Descending), + ]; + $invalidClauses = [CampaignRequestSortField::BrandCode]; + $emptyClauses = []; - it('creates a valid campaign request sort criteria with one clause', function (): void { + it('creates a valid campaign request sort criteria with one clause', function () use ($singleClauses): void { // GIVEN / WHEN - $campaignRequestSortCriteria = new CampaignRequestSortCriteria( - $this->singleClauses - ); + $campaignRequestSortCriteria = new CampaignRequestSortCriteria($singleClauses); // THEN - expect($campaignRequestSortCriteria->clauses)->toBe($this->singleClauses); + expect($campaignRequestSortCriteria->clauses)->toBe($singleClauses); }); - it('creates a valid campaign request sort criteria with several clauses', function (): void { + it('creates a valid campaign request sort criteria with several clauses', function () use ($clauses): void { // GIVEN / WHEN - $campaignRequestSortCriteria = new CampaignRequestSortCriteria( - $this->clauses - ); + $campaignRequestSortCriteria = new CampaignRequestSortCriteria($clauses); // THEN - expect($campaignRequestSortCriteria->clauses)->toBe($this->clauses); + expect($campaignRequestSortCriteria->clauses)->toBe($clauses); }); - it('rejects a campaign request sort criteria without clause', function (): void { + it('rejects a campaign request sort criteria without clause', function () use ($emptyClauses): void { // GIVEN / WHEN / THEN - expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($this->emptyClauses)) + expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($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 { + it('rejects a campaign request sort criteria with a non-clause element', function () use ($invalidClauses): void { // GIVEN / WHEN / THEN - expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($this->invalidClauses)) + expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($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 f72656f..ee1ddff 100644 --- a/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/RejectionReasonTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/CampaignRequest/RejectionReasonTest.php @@ -9,22 +9,20 @@ use Domain\Models\ValueObjects\CampaignRequest\RejectionReason; covers(RejectionReason::class); describe('rejection reason', function (): void { - beforeEach(function (): void { - $this->value = 'Missing legal documents.'; - }); + $value = 'Missing legal documents.'; - it('creates a valid rejection reason', function (): void { + it('creates a valid rejection reason', function () use ($value): void { // GIVEN / WHEN - $reason = new RejectionReason($this->value); + $reason = new RejectionReason($value); // THEN expect($reason)->toBeInstanceOf(RejectionReason::class) - ->and($reason->value)->toBe($this->value); + ->and($reason->value)->toBe($value); }); - it('rejects an empty or whitespace-only reason', function (string $value): void { + it('rejects an empty or whitespace-only reason', function (string $invalidValue): void { // WHEN - $action = fn (): RejectionReason => new RejectionReason($value); + $action = fn (): RejectionReason => new RejectionReason($invalidValue); // THEN expect($action)->toThrow(\DomainException::class, 'Rejection reason cannot be empty.'); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php index 829915c..5f05e45 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendarTest.php @@ -11,49 +11,41 @@ use DateTime; covers(CouponAvailabilityCalendar::class); describe('coupon availability calendar', function (): void { - beforeEach(function (): void { - $this->singlePeriods = [ - new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), - ]; - $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 = []; - }); + $singlePeriods = [ + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), + ]; + $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')), + ]; + $invalidPeriods = ['2026-08-25', '2026-10-31']; + $emptyPeriods = []; - it('creates a valid coupon availability calendar with one period', function (): void { + it('creates a valid coupon availability calendar with one period', function () use ($singlePeriods): void { // GIVEN / WHEN - $couponAvailabilityCalendar = new CouponAvailabilityCalendar( - $this->singlePeriods - ); + $couponAvailabilityCalendar = new CouponAvailabilityCalendar($singlePeriods); // THEN - expect($couponAvailabilityCalendar->periods)->toBe($this->singlePeriods); + expect($couponAvailabilityCalendar->periods)->toBe($singlePeriods); }); - it('creates a valid coupon availability calendar with several periods', function (): void { + it('creates a valid coupon availability calendar with several periods', function () use ($periods): void { // GIVEN / WHEN - $couponAvailabilityCalendar = new CouponAvailabilityCalendar( - $this->periods - ); + $couponAvailabilityCalendar = new CouponAvailabilityCalendar($periods); // THEN - expect($couponAvailabilityCalendar->periods)->toBe($this->periods); + expect($couponAvailabilityCalendar->periods)->toBe($periods); }); - it('rejects a coupon availability calendar without period', function (): void { + it('rejects a coupon availability calendar without period', function () use ($emptyPeriods): void { // GIVEN / WHEN / THEN - expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar( - $this->emptyPeriods - ))->toThrow(\DomainException::class); + expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar($emptyPeriods)) + ->toThrow(\DomainException::class); }); - it('rejects a coupon availability calendar with a non-period element', function (): void { + it('rejects a coupon availability calendar with a non-period element', function () use ($invalidPeriods): void { // GIVEN / WHEN / THEN - expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar( - $this->invalidPeriods - ))->toThrow(\DomainException::class, 'CouponAvailabilityCalendar periods must be Period instances.'); + expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar($invalidPeriods)) + ->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 83929ee..b893d7b 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponDiffusionTest.php @@ -11,15 +11,13 @@ use DateTime; covers(CouponDiffusion::class); describe('coupon diffusion', function (): void { - beforeEach(function (): void { - $this->period = 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')); - it('creates a valid coupon diffusion with one period', function (): void { + it('creates a valid coupon diffusion with one period', function () use ($period): void { // GIVEN / WHEN - $couponDiffusion = new CouponDiffusion($this->period); + $couponDiffusion = new CouponDiffusion($period); // THEN - expect($couponDiffusion->period)->toBe($this->period); + expect($couponDiffusion->period)->toBe($period); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIncoCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIncoCodeTest.php new file mode 100644 index 0000000..3302c49 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIncoCodeTest.php @@ -0,0 +1,28 @@ +value)->toBe($value); + }); + + it('rejects invalid inco code', function () use ($zeroValue): void { + // GIVEN / WHEN / THEN + expect(fn (): CouponIncoCode => new CouponIncoCode($zeroValue)) + ->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArrayTest.php new file mode 100644 index 0000000..6bcc31f --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArrayTest.php @@ -0,0 +1,88 @@ +value)->toBe($informations); + }); + + it('accepts additional countries next to France', function () use ($franceInformation, $beneluxInformation): void { + // GIVEN + $informations = [$beneluxInformation, $franceInformation]; + + // WHEN + $array = new CouponLegalInformationArray($informations); + + // THEN + expect($array->value)->toBe($informations); + }); + + it('rejects an empty array', function (): void { + // GIVEN / WHEN + $action = fn (): CouponLegalInformationArray => new CouponLegalInformationArray([]); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'CouponLegalInformations requires at least one CouponLegalInformation.', + ); + }); + + it('rejects a non legal information element', function (): void { + // GIVEN / WHEN + $action = fn (): CouponLegalInformationArray => new CouponLegalInformationArray(['legal']); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Value of CouponLegalInformationArray must be CouponLegalInformation instances.', + ); + }); + + it('rejects a non legal information element mixed with valid ones', function () use ($franceInformation): void { + // GIVEN + $informations = [$franceInformation, 'legal']; + + // WHEN + $action = fn (): CouponLegalInformationArray => new CouponLegalInformationArray($informations); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Value of CouponLegalInformationArray must be CouponLegalInformation instances.', + ); + }); + + it('rejects an array without France legal information', function () use ($beneluxInformation): void { + // GIVEN + $informations = [$beneluxInformation]; + + // WHEN + $action = fn (): CouponLegalInformationArray => new CouponLegalInformationArray($informations); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'CouponLegalInformations requires a CouponLegalInformation for France.', + ); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php index 007b7d6..d404a4a 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationTest.php @@ -10,29 +10,39 @@ use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; covers(CouponLegalInformation::class); describe('coupon legal information', function (): void { - beforeEach(function (): void { - $this->legalText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; - $this->emptyLegalText = ''; - $this->forLocale = Country::FRANCE; - }); + $legalText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; + $emptyLegalText = ''; + $forLocale = Country::FRANCE; - it('creates a valid coupon legal information', function (): void { + it('creates a valid coupon legal information FRANCE', function () use ($legalText, $forLocale): void { // GIVEN / WHEN $couponLegalInformation = new CouponLegalInformation( - $this->legalText, - $this->forLocale + $legalText, + $forLocale, ); // THEN - expect($couponLegalInformation->legalText)->toBe($this->legalText) - ->and($couponLegalInformation->forLocale)->toBe($this->forLocale); + expect($couponLegalInformation->legalText)->toBe($legalText) + ->and($couponLegalInformation->forLocale)->toBe($forLocale); }); - it('rejects invalid coupon legal information', function (): void { + it('creates a valid coupon legal information BENELUX', function () use ($legalText): void { + // GIVEN / WHEN + $couponLegalInformation = new CouponLegalInformation( + $legalText, + Country::BENELUX, + ); + + // THEN + expect($couponLegalInformation->legalText)->toBe($legalText) + ->and($couponLegalInformation->forLocale)->toBe(Country::BENELUX); + }); + + it('rejects invalid coupon legal information', function () use ($emptyLegalText, $forLocale): void { // GIVEN / WHEN / THEN expect(fn (): CouponLegalInformation => new CouponLegalInformation( - $this->emptyLegalText, - $this->forLocale + $emptyLegalText, + $forLocale, ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementArrayTest.php new file mode 100644 index 0000000..183b1f5 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementArrayTest.php @@ -0,0 +1,61 @@ +couponMovements)->toBe($couponMovements); + }); + + it('rejects an empty array', function (): void { + // GIVEN / WHEN + $action = fn (): CouponMovementArray => new CouponMovementArray([]); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'CouponMovementArray requires at least one CouponMovement.', + ); + }); + + it('rejects a non coupon movement element', function (): void { + // GIVEN / WHEN + $action = fn (): CouponMovementArray => new CouponMovementArray(['movement']); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Value of CouponMovementArray must be CouponMovement instances.', + ); + }); + + it('rejects a non coupon movement element mixed with valid ones', function () use ($couponMovement): void { + // GIVEN + $couponMovements = [$couponMovement, 'movement']; + + // WHEN + $action = fn (): CouponMovementArray => new CouponMovementArray($couponMovements); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Value of CouponMovementArray must be CouponMovement instances.', + ); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementTest.php new file mode 100644 index 0000000..7750d8e --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementTest.php @@ -0,0 +1,47 @@ +quantity)->toBe($quantity) + ->and($movement->createdAt)->toBe($createdAt); + }); + + it('defaults the creation date to now', function (): void { + // GIVEN + $before = new \DateTimeImmutable(); + + // WHEN + $movement = new CouponMovement(5); + + // THEN + $after = new \DateTimeImmutable(); + + expect($movement->createdAt)->toBeInstanceOf(\DateTimeImmutable::class) + ->and($movement->createdAt >= $before)->toBeTrue() + ->and($movement->createdAt <= $after)->toBeTrue(); + }); + + it('accepts a negative quantity', function (): void { + // GIVEN / WHEN + $movement = new CouponMovement(-3); + + // THEN + expect($movement->quantity)->toBe(-3); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponNameTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponNameTest.php new file mode 100644 index 0000000..1a8e560 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponNameTest.php @@ -0,0 +1,28 @@ +value)->toBe($value); + }); + + it('rejects invalid coupon name', function () use ($emptyValue): void { + // GIVEN / WHEN / THEN + expect(fn (): CouponName => new CouponName($emptyValue)) + ->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php new file mode 100644 index 0000000..7b1012a --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php @@ -0,0 +1,45 @@ +value)->toBe($value); + }); + + it('creates a valid processing code BENELUX', function () use ($value): void { + // GIVEN / WHEN + $processingCode = new CouponProcessingCode( + $value, + Country::BENELUX, + ); + + // THEN + expect($processingCode->value)->toBe($value); + }); + + it('rejects invalid processing code', function () use ($emptyValue): void { + // GIVEN / WHEN / THEN + expect(fn (): CouponProcessingCode => new CouponProcessingCode( + $emptyValue, + Country::FRANCE, + ))->toThrow(\DomainException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php new file mode 100644 index 0000000..6896093 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php @@ -0,0 +1,88 @@ +value)->toBe($processingCodes); + }); + + it('accepts additional countries next to France', function () use ($franceCode, $beneluxCode): void { + // GIVEN + $processingCodes = [$beneluxCode, $franceCode]; + + // WHEN + $array = new CouponProcessingCodeArray($processingCodes); + + // THEN + expect($array->value)->toBe($processingCodes); + }); + + it('rejects an empty array', function (): void { + // GIVEN / WHEN + $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray([]); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'ProcessingCodes requires at least one CouponProcessingCode.', + ); + }); + + it('rejects a non processing code element', function (): void { + // GIVEN / WHEN + $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray(['ABC1234567890']); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Value of CouponProcessingCodeArray must be CouponProcessingCode instances.', + ); + }); + + it('rejects a non processing code element mixed with valid ones', function () use ($franceCode): void { + // GIVEN + $processingCodes = [$franceCode, 'ABC1234567890']; + + // WHEN + $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray($processingCodes); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Value of CouponProcessingCodeArray must be CouponProcessingCode instances.', + ); + }); + + it('rejects an array without France processing code', function () use ($beneluxCode): void { + // GIVEN + $processingCodes = [$beneluxCode]; + + // WHEN + $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray($processingCodes); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'CouponProcessingCodes requires a CouponProcessingCode for France.', + ); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php new file mode 100644 index 0000000..fb61519 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php @@ -0,0 +1,93 @@ +value)->toBe($value) + ->and($code->availableIn)->toBe(Country::FRANCE); + }); + + it('accepts lowercase and uppercase letters and digits', function (): void { + // GIVEN / WHEN + $code = new CouponProcessingCode('aBc1234567xYz', Country::FRANCE); + + // THEN + expect($code->value)->toBe('aBc1234567xYz'); + }); + + it('rejects an invalid value', function (string $invalidValue): void { + // GIVEN / WHEN + $action = fn (): CouponProcessingCode => new CouponProcessingCode($invalidValue, Country::FRANCE); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Processing Code must be exactly 13 alphanumeric characters, e.g. ABC1234567890.', + ); + })->with([ + 'empty' => '', + 'too short' => 'ABC123456789', + 'too long' => 'ABC12345678901', + 'special char' => 'ABC123456789!', + 'with space' => 'ABC 123456789', + 'trailing break' => "ABC1234567890\n", + ]); + + it('is equal to another processing code with the same value', function () use ($value): void { + // GIVEN + $code = new CouponProcessingCode($value, Country::FRANCE); + $other = new CouponProcessingCode($value, Country::FRANCE); + + // WHEN / THEN + expect($code->equals($other))->toBeTrue(); + }); + + it('ignores the country when comparing', function () use ($value): void { + // GIVEN + $france = new CouponProcessingCode($value, Country::FRANCE); + $benelux = new CouponProcessingCode($value, Country::BENELUX); + + // WHEN / THEN + expect($france->equals($benelux))->toBeTrue(); + }); + + it('is not equal to a processing code with a different value', function () use ($value): void { + // GIVEN + $code = new CouponProcessingCode($value, Country::FRANCE); + $other = new CouponProcessingCode('XYZ0987654321', Country::FRANCE); + + // WHEN / THEN + expect($code->equals($other))->toBeFalse(); + }); + + it('is not equal to a value that is not a processing code', function () use ($value): void { + // GIVEN + $code = new CouponProcessingCode($value, Country::FRANCE); + + // WHEN / THEN + expect($code->equals($value))->toBeFalse() + ->and($code->equals(null))->toBeFalse(); + }); + + it('is converted to its value as a string', function () use ($value): void { + // GIVEN + $code = new CouponProcessingCode($value, Country::FRANCE); + + // WHEN / THEN + expect((string) $code)->toBe($value); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductArrayTest.php new file mode 100644 index 0000000..342c6f5 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductArrayTest.php @@ -0,0 +1,67 @@ +value)->toBe([]); + }); + + it('accepts an empty array explicitly', function (): void { + // GIVEN / WHEN + $array = new CouponProductArray([]); + + // THEN + expect($array->value)->toBe([]); + }); + + it('creates an array with valid products', function () use ($firstProduct, $secondProduct): void { + // GIVEN + $products = [$firstProduct, $secondProduct]; + + // WHEN + $array = new CouponProductArray($products); + + // THEN + expect($array->value)->toBe($products); + }); + + it('rejects a non product element', function (): void { + // GIVEN / WHEN + $action = fn (): CouponProductArray => new CouponProductArray([35784]); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Value of CouponProductArray must be CouponIncoCode instances.', + ); + }); + + it('rejects a non product element mixed with valid ones', function () use ($firstProduct): void { + // GIVEN + $products = [$firstProduct, 35784]; + + // WHEN + $action = fn (): CouponProductArray => new CouponProductArray($products); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Value of CouponProductArray must be CouponIncoCode instances.', + ); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php index 35dd4b3..b4c7295 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php @@ -9,38 +9,30 @@ use Domain\Models\ValueObjects\Coupon\CouponQuantity; covers(CouponQuantity::class); describe('coupon quantity', function (): void { - beforeEach(function (): void { - $this->quantity = 10; - $this->zeroQuantity = 0; - $this->negativeQuantity = -1; - }); + $quantity = 10; + $zeroQuantity = 0; + $negativeQuantity = -1; - it('creates a valid coupon quantity', function (): void { + it('creates a valid coupon quantity', function () use ($quantity): void { // GIVEN / WHEN - $couponQuantity = new CouponQuantity( - $this->quantity - ); + $couponQuantity = new CouponQuantity($quantity); // THEN expect($couponQuantity)->toBeInstanceOf(CouponQuantity::class) - ->and($couponQuantity->quantity)->toBe($this->quantity); + ->and($couponQuantity->value)->toBe($quantity); }); - it('rejects a quantity of zero', function (): void { + it('rejects a quantity of zero', function () use ($zeroQuantity): void { // WHEN - $action = fn (): CouponQuantity => new CouponQuantity( - $this->zeroQuantity - ); + $action = fn (): CouponQuantity => new CouponQuantity($zeroQuantity); // THEN expect($action)->toThrow(\DomainException::class, 'CouponQuantity quantity must be positive.'); }); - it('rejects a negative quantity', function (): void { + it('rejects a negative quantity', function () use ($negativeQuantity): void { // WHEN - $action = fn (): CouponQuantity => new CouponQuantity( - $this->negativeQuantity - ); + $action = fn (): CouponQuantity => new CouponQuantity($negativeQuantity); // THEN expect($action)->toThrow(\DomainException::class, 'CouponQuantity quantity must be positive.'); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArrayTest.php new file mode 100644 index 0000000..1a31273 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArrayTest.php @@ -0,0 +1,88 @@ +value)->toBe($informations); + }); + + it('accepts additional countries next to France', function () use ($franceInformation, $beneluxInformation): void { + // GIVEN + $informations = [$beneluxInformation, $franceInformation]; + + // WHEN + $array = new CouponValidityInformationArray($informations); + + // THEN + expect($array->value)->toBe($informations); + }); + + it('rejects an empty array', function (): void { + // GIVEN / WHEN + $action = fn (): CouponValidityInformationArray => new CouponValidityInformationArray([]); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'CouponValidityInformations requires at least one CouponValidityInformation.', + ); + }); + + it('rejects a non validity information element', function (): void { + // GIVEN / WHEN + $action = fn (): CouponValidityInformationArray => new CouponValidityInformationArray(['validity']); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Value of CouponValidityInformationArray must be CouponValidityInformation instances.', + ); + }); + + it('rejects a non validity information element mixed with valid ones', function () use ($franceInformation): void { + // GIVEN + $informations = [$franceInformation, 'validity']; + + // WHEN + $action = fn (): CouponValidityInformationArray => new CouponValidityInformationArray($informations); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Value of CouponValidityInformationArray must be CouponValidityInformation instances.', + ); + }); + + it('rejects an array without France validity information', function () use ($beneluxInformation): void { + // GIVEN + $informations = [$beneluxInformation]; + + // WHEN + $action = fn (): CouponValidityInformationArray => new CouponValidityInformationArray($informations); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'CouponValidityInformations requires a CouponValidityInformation for France.', + ); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php index 952a9b2..35aa0e8 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationTest.php @@ -10,29 +10,27 @@ use Shared\Enums\Country; covers(CouponValidityInformation::class); describe('coupon validity information', function (): void { - beforeEach(function (): void { - $this->validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; - $this->emptyValidityText = ''; - $this->forLocale = Country::FRANCE; - }); + $validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.'; + $emptyValidityText = ''; + $forLocale = Country::FRANCE; - it('creates a valid coupon validity information', function (): void { + it('creates a valid coupon validity information', function () use ($validityText, $forLocale): void { // GIVEN / WHEN $couponValidityInformation = new CouponValidityInformation( - $this->validityText, - $this->forLocale + $validityText, + $forLocale, ); // THEN - expect($couponValidityInformation->validityText)->toBe($this->validityText) - ->and($couponValidityInformation->forLocale)->toBe($this->forLocale); + expect($couponValidityInformation->validityText)->toBe($validityText) + ->and($couponValidityInformation->forLocale)->toBe($forLocale); }); - it('rejects invalid coupon validity information', function (): void { + it('rejects invalid coupon validity information', function () use ($emptyValidityText, $forLocale): void { // GIVEN / WHEN / THEN expect(fn (): CouponValidityInformation => new CouponValidityInformation( - $this->emptyValidityText, - $this->forLocale + $emptyValidityText, + $forLocale, ))->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php deleted file mode 100644 index 57c85c9..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityTest.php +++ /dev/null @@ -1,38 +0,0 @@ -days = 30; - $this->zeroDays = 0; - $this->until = new DateTime('2026-11-30'); - }); - - it('creates a valid coupon validity', function (): void { - // GIVEN / WHEN - $couponValidity = new CouponValidity( - $this->days, - $this->until - ); - - // THEN - expect($couponValidity->days)->toBe($this->days) - ->and($couponValidity->until)->toBe($this->until); - }); - - it('rejects invalid coupon validity', function (): void { - // 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 deleted file mode 100644 index f1b0524..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/CouponNameTest.php +++ /dev/null @@ -1,33 +0,0 @@ -value = 'Name'; - $this->emptyValue = ''; - }); - - it('creates a valid coupon name', function (): void { - // GIVEN / WHEN - $name = new CouponName( - $this->value - ); - - // THEN - expect($name->value)->toBe($this->value); - }); - - it('rejects invalid coupon name', function (): void { - // 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 deleted file mode 100644 index 1e56044..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/GS1Test.php +++ /dev/null @@ -1,38 +0,0 @@ -value = '1010100101'; - $this->emptyValue = ''; - $this->format = BarcodeFormat::EAN13; - }); - - it('creates a valid gs1', function (): void { - // WHEN - $GS1 = new GS1( - $this->value, - $this->format - ); - - // THEN - expect($GS1->value)->toBe($this->value) - ->and($GS1->format)->toBe($this->format); - }); - - it('rejects invalid gs1', function (): void { - // 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 deleted file mode 100644 index 54b03ea..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php +++ /dev/null @@ -1,33 +0,0 @@ -value = 1010100101; - $this->zeroValue = 0; - }); - - it('creates a valid inco code', function (): void { - // GIVEN / WHEN - $incoCode = new IncoCode( - $this->value - ); - - // THEN - expect($incoCode->value)->toBe($this->value); - }); - - it('rejects invalid inco code', function (): void { - // 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 84ea7e2..ee12511 100644 --- a/tests/Unit/Domain/Models/ValueObjects/MoneyAmountTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/MoneyAmountTest.php @@ -11,36 +11,26 @@ use Shared\ValueObjects\Money; covers(MoneyAmount::class); describe('money amount', function (): void { - beforeEach(function (): void { - $this->money = new Money(Currency::EUR, 300); - $this->secondMoney = new Money(Currency::EUR, 200); - $this->minimumMoney = new Money(Currency::EUR, 1); - $this->zeroMoney = new Money(Currency::EUR, 0); - $this->aboveLimitMoney = new Money(Currency::EUR, 301); - }); + $money = new Money(Currency::EUR, 300); + $secondMoney = new Money(Currency::EUR, 200); + $minimumMoney = new Money(Currency::EUR, 1); + $zeroMoney = new Money(Currency::EUR, 0); + $aboveLimitMoney = new Money(Currency::EUR, 301); - it('wraps a Money value', function (): void { + it('wraps a Money value', function () use ($money): void { // GIVEN / WHEN - $amount = new MoneyAmount( - $this->money - ); + $amount = new MoneyAmount($money); // THEN expect($amount)->toBeInstanceOf(MoneyAmount::class) - ->and($amount->value)->toBe($this->money); + ->and($amount->value)->toBe($money); }); - it('implements equals by delegating to the wrapped Money', function (): void { + it('implements equals by delegating to the wrapped Money', function () use ($money, $secondMoney): void { // GIVEN / WHEN - $amount1 = new MoneyAmount( - $this->money - ); - $amount2 = new MoneyAmount( - $this->money - ); - $amount3 = new MoneyAmount( - $this->secondMoney - ); + $amount1 = new MoneyAmount($money); + $amount2 = new MoneyAmount($money); + $amount3 = new MoneyAmount($secondMoney); // THEN expect($amount1->equals($amount2))->toBeTrue() @@ -48,41 +38,39 @@ describe('money amount', function (): void { ->and($amount1->equals(null))->toBeFalse(); }); - it('converts to string by delegating to the wrapped Money', function (): void { + it('converts to string by delegating to the wrapped Money', function () use ($money): void { // GIVEN / WHEN - $amount = new MoneyAmount( - $this->money - ); + $amount = new MoneyAmount($money); // THEN expect((string) $amount)->toBe((string) $amount->value); }); - it('accepts the minimum amount of 0.01€', function (): void { + it('accepts the minimum amount of 0.01€', function () use ($minimumMoney): void { // GIVEN / WHEN - $amount = new MoneyAmount($this->minimumMoney); + $amount = new MoneyAmount($minimumMoney); // THEN - expect($amount->value)->toBe($this->minimumMoney); + expect($amount->value)->toBe($minimumMoney); }); - it('accepts the maximum amount of 3.00€', function (): void { + it('accepts the maximum amount of 3.00€', function () use ($money): void { // GIVEN / WHEN - $amount = new MoneyAmount($this->money); + $amount = new MoneyAmount($money); // THEN - expect($amount->value)->toBe($this->money); + expect($amount->value)->toBe($money); }); - it('throws when the amount is zero', function (): void { + it('throws when the amount is zero', function () use ($zeroMoney): void { // GIVEN / WHEN / THEN - expect(fn () => new MoneyAmount($this->zeroMoney)) + expect(fn (): MoneyAmount => new MoneyAmount($zeroMoney)) ->toThrow(\DomainException::class, 'Money amount must be between 0.01€ and 3.00€'); }); - it('throws when the amount is above 3.00€', function (): void { + it('throws when the amount is above 3.00€', function () use ($aboveLimitMoney): void { // GIVEN / WHEN / THEN - expect(fn () => new MoneyAmount($this->aboveLimitMoney)) + expect(fn (): MoneyAmount => new MoneyAmount($aboveLimitMoney)) ->toThrow(\DomainException::class, 'Money amount must be between 0.01€ and 3.00€'); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php index d45e709..e84fd8e 100644 --- a/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php @@ -9,48 +9,46 @@ 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; - }); + $maxPerCustomer = 3; + $minMaxPerCustomer = 1; + $zeroMaxPerCustomer = 0; + $cooldownDays = 30; + $minCooldownDays = 1; + $zeroCooldownDays = 0; - it('creates print rules with both fields set', function (): void { + it('creates print rules with both fields set', function () use ($maxPerCustomer, $cooldownDays): void { // GIVEN / WHEN $printRules = new PrintRules( - maxPerCustomer: $this->maxPerCustomer, - cooldownDays: $this->cooldownDays + maxPerCustomer: $maxPerCustomer, + cooldownDays: $cooldownDays ); // THEN expect($printRules)->toBeInstanceOf(PrintRules::class) - ->and($printRules->maxPerCustomer)->toBe($this->maxPerCustomer) - ->and($printRules->cooldownDays)->toBe($this->cooldownDays); + ->and($printRules->maxPerCustomer)->toBe($maxPerCustomer) + ->and($printRules->cooldownDays)->toBe($cooldownDays); }); - it('creates print rules with only maxPerCustomer set', function (): void { + it('creates print rules with only maxPerCustomer set', function () use ($maxPerCustomer): void { // GIVEN / WHEN $printRules = new PrintRules( - maxPerCustomer: $this->maxPerCustomer + maxPerCustomer: $maxPerCustomer ); // THEN - expect($printRules->maxPerCustomer)->toBe($this->maxPerCustomer) + expect($printRules->maxPerCustomer)->toBe($maxPerCustomer) ->and($printRules->cooldownDays)->toBeNull(); }); - it('creates print rules with only cooldownDays set', function (): void { + it('creates print rules with only cooldownDays set', function () use ($cooldownDays): void { // GIVEN / WHEN $printRules = new PrintRules( - cooldownDays: $this->cooldownDays + cooldownDays: $cooldownDays ); // THEN expect($printRules->maxPerCustomer)->toBeNull() - ->and($printRules->cooldownDays)->toBe($this->cooldownDays); + ->and($printRules->cooldownDays)->toBe($cooldownDays); }); it('rejects print rules without any field set', function (): void { @@ -59,29 +57,29 @@ describe('print rules', function (): void { ->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 { + it('accepts the minimum value of 1 for both fields', function () use ($minMaxPerCustomer, $minCooldownDays): void { // GIVEN / WHEN $printRules = new PrintRules( - maxPerCustomer: $this->minMaxPerCustomer, - cooldownDays: $this->minCooldownDays + maxPerCustomer: $minMaxPerCustomer, + cooldownDays: $minCooldownDays ); // THEN - expect($printRules->maxPerCustomer)->toBe($this->minMaxPerCustomer) - ->and($printRules->cooldownDays)->toBe($this->minCooldownDays); + expect($printRules->maxPerCustomer)->toBe($minMaxPerCustomer) + ->and($printRules->cooldownDays)->toBe($minCooldownDays); }); - it('rejects a maxPerCustomer equal to zero', function (): void { + it('rejects a maxPerCustomer equal to zero', function () use ($zeroMaxPerCustomer): void { // GIVEN / WHEN / THEN expect(fn (): PrintRules => new PrintRules( - maxPerCustomer: $this->zeroMaxPerCustomer + maxPerCustomer: $zeroMaxPerCustomer ))->toThrow(\InvalidArgumentException::class, 'Max per customer must be greater than 0.'); }); - it('rejects a cooldownDays equal to zero', function (): void { + it('rejects a cooldownDays equal to zero', function () use ($zeroCooldownDays): void { // GIVEN / WHEN / THEN expect(fn (): PrintRules => new PrintRules( - cooldownDays: $this->zeroCooldownDays + cooldownDays: $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 deleted file mode 100644 index 2946cd1..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/ProcessingCodeTest.php +++ /dev/null @@ -1,33 +0,0 @@ -value = '1010100101'; - $this->emptyValue = ''; - }); - - it('creates a valid processing code', function (): void { - // GIVEN / WHEN - $processingCode = new ProcessingCode( - $this->value - ); - - // THEN - expect($processingCode->value)->toBe($this->value); - }); - - it('rejects invalid processing code', function (): void { - // GIVEN / WHEN / THEN - expect(fn (): ProcessingCode => new ProcessingCode( - $this->emptyValue - ))->toThrow(\DomainException::class); - }); -}); diff --git a/tests/Unit/Domain/Models/ValueObjects/RawPictureTest.php b/tests/Unit/Domain/Models/ValueObjects/RawPictureTest.php index 8df5a9c..73e7858 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RawPictureTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RawPictureTest.php @@ -61,7 +61,7 @@ describe('raw picture', function (): void { it('measures size in raw bytes rather than characters', function (): void { // GIVEN - $halfLimit = (int) (RawPicturePath::MAX_SIZE_IN_BYTES / 2) + 1; + $halfLimit = RawPicturePath::MAX_SIZE_IN_BYTES / 2 + 1; $content = str_repeat('é', $halfLimit); // WHEN diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php index e161db9..22e3449 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php @@ -10,22 +10,17 @@ use DateTime; covers(Period::class); describe('period', function (): void { - beforeEach(function (): void { - $this->start = new DateTime('2026-01-01'); - $this->end = new DateTime('2026-01-31'); - }); + $start = new DateTime('2026-01-01'); + $end = new DateTime('2026-01-31'); - it('creates a valid period', function (): void { + it('creates a valid period', function () use ($start, $end): void { // GIVEN / WHEN - $period = new Period( - $this->start, - $this->end - ); + $period = new Period($start, $end); // THEN expect($period)->toBeInstanceOf(Period::class) - ->and($period->start)->toBe($this->start) - ->and($period->end)->toBe($this->end); + ->and($period->start)->toBe($start) + ->and($period->end)->toBe($end); }); it('accepts a period whose end equals its start', function (): void { @@ -43,7 +38,7 @@ describe('period', function (): void { 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'))) + expect(fn (): Period => 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/RequestedCouponQuantityTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php index e5f835c..e6bfc09 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponQuantityTest.php @@ -9,38 +9,30 @@ use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; covers(RequestedCouponQuantity::class); describe('quantity', function (): void { - beforeEach(function (): void { - $this->value = 10; - $this->zeroValue = 0; - $this->negativeValue = -1; - }); + $value = 10; + $zeroValue = 0; + $negativeValue = -1; - it('creates a valid quantity', function (): void { + it('creates a valid quantity', function () use ($value): void { // GIVEN / WHEN - $quantity = new RequestedCouponQuantity( - $this->value - ); + $quantity = new RequestedCouponQuantity($value); // THEN expect($quantity)->toBeInstanceOf(RequestedCouponQuantity::class) - ->and($quantity->value)->toBe($this->value); + ->and($quantity->value)->toBe($value); }); - it('rejects a quantity of zero', function (): void { + it('rejects a quantity of zero', function () use ($zeroValue): void { // GIVEN / WHEN - $action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity( - $this->zeroValue - ); + $action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity($zeroValue); // THEN expect($action)->toThrow(\DomainException::class, 'Quantity should be more than 0.'); }); - it('rejects a negative quantity', function (): void { + it('rejects a negative quantity', function () use ($negativeValue): void { // GIVEN / WHEN - $action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity( - $this->negativeValue - ); + $action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity($negativeValue); // THEN expect($action)->toThrow(\DomainException::class, 'Quantity should be more than 0.'); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index f55da32..fa7ed31 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -4,18 +4,21 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\RequestedCoupon; -use Carbon\CarbonImmutable; use DateTime; -use Domain\Enums\BarcodeFormat; -use Domain\Models\ValueObjects\Coupon\CouponValidity; +use Domain\Models\ValueObjects\Coupon\CouponIncoCode; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponName; +use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; +use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; +use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; -use Domain\Models\ValueObjects\CouponName; -use Domain\Models\ValueObjects\GS1; -use Domain\Models\ValueObjects\IncoCode; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; 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 Shared\Enums\Country; @@ -25,210 +28,265 @@ 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->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); - }); + $couponName = 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)); + $couponPicture = new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/mobileThumbnail.jpg') + ); - afterEach(function (): void { - CarbonImmutable::setTestNow(); - }); + $franceProcessingCode = new CouponProcessingCode('ABC1234567890', Country::FRANCE); + $beneluxProcessingCode = new CouponProcessingCode('XYZ9876543210', Country::BENELUX); + $couponProcessingCodes = new CouponProcessingCodeArray([ + $franceProcessingCode, + $beneluxProcessingCode, + ]); - it('hydrates a requested coupon with all its fields', function (): void { + $franceValidityInformation = new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE + ); + $beneluxValidityInformation = new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::BENELUX + ); + $couponValidityInformations = new CouponValidityInformationArray([ + $franceValidityInformation, + $beneluxValidityInformation, + ]); + + $franceLegalInformation = new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE + ); + $beneluxLegalInformation = new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::BENELUX + ); + $couponLegalInformations = new CouponLegalInformationArray([ + $franceLegalInformation, + $beneluxLegalInformation, + ]); + + $couponProducts = new CouponProductArray([ + new CouponIncoCode(35784), + new CouponIncoCode(15522), + ]); + $printRules = new PrintRules(maxPerCustomer: 3); + + it('hydrates a requested coupon with all its fields', function () use ( + $couponName, + $quantity, + $diffusion, + $moneyAmount, + $couponPicture, + $couponProcessingCodes, + $couponValidityInformations, + $couponLegalInformations, + $couponProducts, + $printRules, + ): void { // GIVEN / WHEN $coupon = new RequestedCoupon( - $this->name, - $this->quantity, - $this->diffusion, - $this->moneyAmount, - $this->processingCode, - $this->couponValidity, - $this->couponValidityInformation, - $this->products, - $this->printRules, - $this->gs1, + couponName: $couponName, + quantity: $quantity, + diffusion: $diffusion, + moneyAmount: $moneyAmount, + couponPicture: $couponPicture, + couponProcessingCodes: $couponProcessingCodes, + couponValidityInformations: $couponValidityInformations, + couponLegalInformations: $couponLegalInformations, + couponProducts: $couponProducts, + printRules: $printRules, ); // THEN - 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->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); + expect($coupon->couponName)->toBe($couponName) + ->and($coupon->quantity)->toBe($quantity) + ->and($coupon->diffusion)->toBe($diffusion) + ->and($coupon->moneyAmount)->toBe($moneyAmount) + ->and($coupon->couponPicture)->toBe($couponPicture) + ->and($coupon->couponProcessingCodes)->toBe($couponProcessingCodes) + ->and($coupon->couponValidityInformations)->toBe($couponValidityInformations) + ->and($coupon->couponLegalInformations)->toBe($couponLegalInformations) + ->and($coupon->couponProducts)->toBe($couponProducts) + ->and($coupon->printRules)->toBe($printRules); }); - it('hydrates a requested coupon without products', function (): void { + it('hydrates a requested coupon without products', function () use ( + $couponName, + $quantity, + $diffusion, + $moneyAmount, + $couponPicture, + $couponProcessingCodes, + $couponValidityInformations, + $couponLegalInformations, + $printRules, + ): 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, - printRules: $this->printRules, - gs1: $this->gs1 + couponName: $couponName, + quantity: $quantity, + diffusion: $diffusion, + moneyAmount: $moneyAmount, + couponPicture: $couponPicture, + couponProcessingCodes: $couponProcessingCodes, + couponValidityInformations: $couponValidityInformations, + couponLegalInformations: $couponLegalInformations, + couponProducts: new CouponProductArray(), + printRules: $printRules, ); // THEN - expect($coupon->products)->toBe([]) - ->and($coupon->printRules)->toBe($this->printRules) - ->and($coupon->gs1)->toBe($this->gs1); + expect($coupon->couponProducts->value)->toBe([]) + ->and($coupon->printRules)->toBe($printRules); }); - it('hydrates a requested coupon without print rules', function (): void { + it('hydrates a requested coupon without print rules', function () use ( + $couponName, + $quantity, + $diffusion, + $moneyAmount, + $couponPicture, + $couponProcessingCodes, + $couponValidityInformations, + $couponLegalInformations, + $couponProducts, + ): 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, - gs1: $this->gs1 + couponName: $couponName, + quantity: $quantity, + diffusion: $diffusion, + moneyAmount: $moneyAmount, + couponPicture: $couponPicture, + couponProcessingCodes: $couponProcessingCodes, + couponValidityInformations: $couponValidityInformations, + couponLegalInformations: $couponLegalInformations, + couponProducts: $couponProducts, ); // THEN expect($coupon->printRules)->toBeNull() - ->and($coupon->products)->toBe($this->products) - ->and($coupon->gs1)->toBe($this->gs1); + ->and($coupon->couponProducts)->toBe($couponProducts); }); - it('hydrates a requested coupon without gs1', function (): void { + it('accepts a france only coupon', function () use ( + $couponName, + $quantity, + $diffusion, + $moneyAmount, + $couponPicture, + $franceProcessingCode, + $franceValidityInformation, + $franceLegalInformation, + $couponProducts, + ): 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 + couponName: $couponName, + quantity: $quantity, + diffusion: $diffusion, + moneyAmount: $moneyAmount, + couponPicture: $couponPicture, + couponProcessingCodes: new CouponProcessingCodeArray([ + $franceProcessingCode, + ]), + couponValidityInformations: new CouponValidityInformationArray([ + $franceValidityInformation, + ]), + couponLegalInformations: new CouponLegalInformationArray([ + $franceLegalInformation, + ]), + couponProducts: $couponProducts, ); // THEN - expect($coupon->gs1)->toBeNull() - ->and($coupon->products)->toBe($this->products) - ->and($coupon->printRules)->toBe($this->printRules); + expect($coupon)->toBeInstanceOf(RequestedCoupon::class); }); - 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, - couponValidity: $this->couponValidity, - couponValidityInformation: $this->couponValidityInformation, - ); - - // THEN - expect($coupon->products)->toBe([]) - ->and($coupon->printRules)->toBeNull() - ->and($coupon->gs1)->toBeNull(); - }); - - it('throws when there is no validity information for france', function (): void { + it('throws when there is no processing code for france', function () use ( + $couponName, + $quantity, + $diffusion, + $moneyAmount, + $couponPicture, + $beneluxProcessingCode, + $couponValidityInformations, + $couponLegalInformations, + $couponProducts, + ): void { // GIVEN / WHEN / THEN - expect(fn () => new RequestedCoupon( - name: $this->name, - quantity: $this->quantity, - diffusion: $this->diffusion, - moneyAmount: $this->moneyAmount, - processingCode: $this->processingCode, - couponValidity: $this->couponValidity, - couponValidityInformation: [], - ))->toThrow(\DomainException::class, 'A coupon requires validity information for France.'); + expect(fn (): RequestedCoupon => new RequestedCoupon( + couponName: $couponName, + quantity: $quantity, + diffusion: $diffusion, + moneyAmount: $moneyAmount, + couponPicture: $couponPicture, + couponProcessingCodes: new CouponProcessingCodeArray([ + $beneluxProcessingCode, + ]), + couponValidityInformations: $couponValidityInformations, + couponLegalInformations: $couponLegalInformations, + couponProducts: $couponProducts, + ))->toThrow(\DomainException::class, 'CouponProcessingCodes requires a CouponProcessingCode for France.'); }); - it('throws when validity information contains an invalid item', function (): void { + it('throws when a benelux processing code has no benelux validity information', function () use ( + $couponName, + $quantity, + $diffusion, + $moneyAmount, + $couponPicture, + $couponProcessingCodes, + $franceValidityInformation, + $couponLegalInformations, + $couponProducts, + ): void { // GIVEN / WHEN / THEN - expect(fn () => new RequestedCoupon( - $this->name, - $this->quantity, - $this->diffusion, - $this->moneyAmount, - $this->processingCode, - $this->couponValidity, - ['Not a CouponValidityInformation'], - $this->products, - $this->printRules, - $this->gs1 - ))->toThrow(\DomainException::class, 'Coupon validity information must be CouponValidityInformation instances.'); + expect(fn (): RequestedCoupon => new RequestedCoupon( + couponName: $couponName, + quantity: $quantity, + diffusion: $diffusion, + moneyAmount: $moneyAmount, + couponPicture: $couponPicture, + couponProcessingCodes: $couponProcessingCodes, + couponValidityInformations: new CouponValidityInformationArray([ + $franceValidityInformation, + ]), + couponLegalInformations: $couponLegalInformations, + couponProducts: $couponProducts, + ))->toThrow(\DomainException::class, 'A coupon requires validity information for BENELUX.'); }); - it('throws when a gs1 coupon has no benelux validity information', function (): void { + it('throws when a benelux processing code has no benelux legal information', function () use ( + $couponName, + $quantity, + $diffusion, + $moneyAmount, + $couponPicture, + $couponProcessingCodes, + $couponValidityInformations, + $franceLegalInformation, + $couponProducts, + ): void { // GIVEN / WHEN / THEN - expect(fn () => new RequestedCoupon( - name: $this->name, - quantity: $this->quantity, - diffusion: $this->diffusion, - moneyAmount: $this->moneyAmount, - processingCode: $this->processingCode, - 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, - 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, - couponValidity: $this->couponValidity, - couponValidityInformation: [$this->franceValidityInformation], - ); - - // THEN - expect($coupon->gs1)->toBeNull(); + expect(fn (): RequestedCoupon => new RequestedCoupon( + couponName: $couponName, + quantity: $quantity, + diffusion: $diffusion, + moneyAmount: $moneyAmount, + couponPicture: $couponPicture, + couponProcessingCodes: $couponProcessingCodes, + couponValidityInformations: $couponValidityInformations, + couponLegalInformations: new CouponLegalInformationArray([ + $franceLegalInformation, + ]), + couponProducts: $couponProducts, + ))->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.'); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php index 42441f1..c897668 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAdjustmentTest.php @@ -10,73 +10,71 @@ 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'; - }); + $quantity = 10; + $zeroQuantity = 0; + $negativeQuantity = -5; + $reason = 'Inventory correction'; - it('creates a valid stock adjustment', function (): void { + it('creates a valid stock adjustment', function () use ($quantity, $reason): void { // GIVEN $actorId = ActorId::generate(); // WHEN $adjustment = new StockAdjustment( - $this->quantity, - $this->reason, - $actorId + $quantity, + $reason, + $actorId, ); // THEN expect($adjustment)->toBeInstanceOf(StockAdjustment::class) - ->and($adjustment->quantity)->toBe($this->quantity) - ->and($adjustment->reason)->toBe($this->reason) + ->and($adjustment->quantity)->toBe($quantity) + ->and($adjustment->reason)->toBe($reason) ->and($adjustment->actorId)->toBe($actorId) ->and($adjustment->createdAt)->toBeNull(); }); - it('creates a stock adjustment with an explicit createdAt date', function (): void { + it('creates a stock adjustment with an explicit createdAt date', function () use ($quantity, $reason): void { // GIVEN $actorId = ActorId::generate(); $createdAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); // WHEN $adjustment = new StockAdjustment( - $this->quantity, - $this->reason, + $quantity, + $reason, $actorId, - $createdAt + $createdAt, ); // THEN expect($adjustment->createdAt)->toBe($createdAt); }); - it('rejects a quantity of zero', function (): void { + it('rejects a quantity of zero', function () use ($zeroQuantity, $reason): void { // GIVEN $actorId = ActorId::generate(); // WHEN $action = fn (): StockAdjustment => new StockAdjustment( - $this->zeroQuantity, - $this->reason, - $actorId + $zeroQuantity, + $reason, + $actorId, ); // THEN expect($action)->toThrow(\DomainException::class, 'Stock Amount should be more than 0.'); }); - it('rejects a negative quantity', function (): void { + it('rejects a negative quantity', function () use ($negativeQuantity, $reason): void { // GIVEN $actorId = ActorId::generate(); // WHEN $action = fn (): StockAdjustment => new StockAdjustment( - $this->negativeQuantity, - $this->reason, - $actorId + $negativeQuantity, + $reason, + $actorId, ); // THEN diff --git a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php index 59a7753..539a44d 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Stock/StockAmountTest.php @@ -9,38 +9,30 @@ use Domain\Models\ValueObjects\Stock\StockAmount; covers(StockAmount::class); describe('stock amount', function (): void { - beforeEach(function (): void { - $this->value = 10; - $this->zeroValue = 0; - $this->negativeValue = -1; - }); + $value = 10; + $zeroValue = 0; + $negativeValue = -1; - it('creates a valid stock amount', function (): void { + it('creates a valid stock amount', function () use ($value): void { // GIVEN / WHEN - $amount = new StockAmount( - $this->value - ); + $amount = new StockAmount($value); // THEN expect($amount)->toBeInstanceOf(StockAmount::class) - ->and($amount->value)->toBe($this->value); + ->and($amount->value)->toBe($value); }); - it('accepts a zero value', function (): void { + it('accepts a zero value', function () use ($zeroValue): void { // GIVEN / WHEN - $amount = new StockAmount( - $this->zeroValue - ); + $amount = new StockAmount($zeroValue); // THEN - expect($amount->value)->toBe($this->zeroValue); + expect($amount->value)->toBe($zeroValue); }); - it('rejects a negative value', function (): void { + it('rejects a negative value', function () use ($negativeValue): void { // GIVEN / WHEN - $action = fn (): StockAmount => new StockAmount( - $this->negativeValue - ); + $action = fn (): StockAmount => new StockAmount($negativeValue); // THEN expect($action)->toThrow(\DomainException::class, 'Stock Amount should be positive.'); diff --git a/tests/Unit/Domain/Enums/CountryTest.php b/tests/Unit/Shared/Enums/CountryTest.php similarity index 57% rename from tests/Unit/Domain/Enums/CountryTest.php rename to tests/Unit/Shared/Enums/CountryTest.php index 5a6c7b8..e548d13 100644 --- a/tests/Unit/Domain/Enums/CountryTest.php +++ b/tests/Unit/Shared/Enums/CountryTest.php @@ -4,9 +4,10 @@ declare(strict_types=1); namespace Tests\Unit\Shared\Enums; +use Shared\DDD\Exceptions\UnknownCountryIsoCodeException; use Shared\Enums\Country; -covers(Country::class); +covers(Country::class, UnknownCountryIsoCodeException::class); describe('country', function (): void { it('has the expected cases with their backing values', function (): void { @@ -19,4 +20,12 @@ describe('country', function (): void { // THEN expect(Country::FRANCE->name)->toBe('FRANCE'); }); + + it('is a domain exception', function (): void { + // GIVEN / WHEN + $exception = UnknownCountryIsoCodeException::fromIsoCode('XX'); + + // THEN + expect($exception)->toBeInstanceOf(\DomainException::class); + }); }); diff --git a/tests/Unit/Shared/Helpers/DatesHelperTest.php b/tests/Unit/Shared/Helpers/DatesHelperTest.php index 9a683ab..e7ac93a 100644 --- a/tests/Unit/Shared/Helpers/DatesHelperTest.php +++ b/tests/Unit/Shared/Helpers/DatesHelperTest.php @@ -250,7 +250,7 @@ describe('dates helper', function (): void { $invalidInput = 'not-a-valid-date'; // WHEN / THEN - expect(fn () => DatesHelper::parseDateTime($invalidInput)) + expect(fn (): \DateTimeInterface => DatesHelper::parseDateTime($invalidInput)) ->toThrow(InvalidDateFormatException::class); }); @@ -259,7 +259,7 @@ describe('dates helper', function (): void { $emptyInput = ' '; // WHEN / THEN - expect(fn () => DatesHelper::parseDateTime($emptyInput)) + expect(fn (): \DateTimeInterface => DatesHelper::parseDateTime($emptyInput)) ->toThrow(InvalidDateFormatException::class); }); }); diff --git a/tests/Unit/Shared/Helpers/UuidHelperTest.php b/tests/Unit/Shared/Helpers/UuidHelperTest.php index 4e4d3ad..67985f5 100644 --- a/tests/Unit/Shared/Helpers/UuidHelperTest.php +++ b/tests/Unit/Shared/Helpers/UuidHelperTest.php @@ -11,7 +11,7 @@ use Ramsey\Uuid\Uuid; use Shared\Helpers\Exceptions\InvalidUuidException; use Shared\Helpers\UuidHelper; -covers(UuidHelper::class); +covers(UuidHelper::class, InvalidUuidException::class); describe('Uuid Helper Unit Tests', function (): void { it('prevents direct instantiation but covers constructor', function (): void { @@ -23,7 +23,7 @@ describe('Uuid Helper Unit Tests', function (): void { $constructor->invoke($reflection->newInstanceWithoutConstructor()); // THEN - expect(fn () => $reflection->newInstance()) + expect(fn (): UuidHelper => $reflection->newInstance()) ->toThrow(\ReflectionException::class); }); @@ -83,6 +83,7 @@ describe('Uuid Helper Unit Tests', function (): void { public function unserialize(string $data): void { + // Keep empty. } public function __serialize(): array @@ -92,6 +93,7 @@ describe('Uuid Helper Unit Tests', function (): void { public function __unserialize(array $data): void { + // Keep empty. } }; @@ -127,7 +129,15 @@ describe('Uuid Helper Unit Tests', function (): void { $uuidV4 = Uuid::uuid4(); // WHEN & THEN - expect(fn () => UuidHelper::toDateTime($uuidV4)) + expect(fn (): \DateTimeImmutable => UuidHelper::toDateTime($uuidV4)) ->toThrow(InvalidUuidException::class, 'Cannot extract datetime from non-V7 UUID'); }); + + it('is an invalid argument exception', function (): void { + // GIVEN / WHEN + $exception = InvalidUuidException::forValue('foo'); + + // THEN + expect($exception)->toBeInstanceOf(\InvalidArgumentException::class); + }); }); diff --git a/tests/Unit/Shared/ValueObjects/Pagination/PageNumberTest.php b/tests/Unit/Shared/ValueObjects/Pagination/PageNumberTest.php index f23a423..4dfdff5 100644 --- a/tests/Unit/Shared/ValueObjects/Pagination/PageNumberTest.php +++ b/tests/Unit/Shared/ValueObjects/Pagination/PageNumberTest.php @@ -46,7 +46,7 @@ describe('PageNumber Value Object Unit Tests', function (): void { it('rejects page number less than or equal to zero', function (int $invalidValue): void { // WHEN / THEN - expect(fn () => new PageNumber($invalidValue)) + expect(fn (): PageNumber => new PageNumber($invalidValue)) ->toThrow(\InvalidArgumentException::class, 'Page number must be greater than 0'); })->with([ 'zero' => [0], diff --git a/tests/Unit/Shared/ValueObjects/Pagination/PageSizeTest.php b/tests/Unit/Shared/ValueObjects/Pagination/PageSizeTest.php index fa96ee3..09cbdad 100644 --- a/tests/Unit/Shared/ValueObjects/Pagination/PageSizeTest.php +++ b/tests/Unit/Shared/ValueObjects/Pagination/PageSizeTest.php @@ -54,7 +54,7 @@ describe('PageSize Value Object Unit Tests', function (): void { it('rejects page size less than or equal to zero', function (int $invalidValue): void { // WHEN / THEN - expect(fn () => new PageSize($invalidValue)) + expect(fn (): PageSize => new PageSize($invalidValue)) ->toThrow(\InvalidArgumentException::class, 'Page size must be greater than 0'); })->with([ 'zero' => [0], @@ -64,7 +64,7 @@ describe('PageSize Value Object Unit Tests', function (): void { it('rejects page size exceeding maximum page size', function (): void { // WHEN / THEN - expect(fn () => new PageSize(PageSize::MAX_PAGE_SIZE + 1)) + expect(fn (): PageSize => new PageSize(PageSize::MAX_PAGE_SIZE + 1)) ->toThrow(\InvalidArgumentException::class, 'Page size must be less than or equal to 10000'); }); From a21415269cc097b9b5697a9774ef0e6000131e7f Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 12 Oct 2026 12:27:44 +0200 Subject: [PATCH 03/21] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20CreateCoupon=20?= =?UTF-8?q?use=20case=20tests=20and=20check=20brand=20existence=20when=20c?= =?UTF-8?q?reating=20a=20coupon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UseCases/Coupon/Create/CreateCoupon.php | 80 ++++++++++ .../UseCases/Coupon/Create/Input.php | 40 +++++ .../UseCases/Coupon/Create/Output.php | 45 ++++++ .../Ports/Repositories/CouponRepository.php | 7 + .../Coupon/Create/CreateCouponTest.php | 151 ++++++++++++++++++ 5 files changed, 323 insertions(+) create mode 100644 src/Application/UseCases/Coupon/Create/CreateCoupon.php create mode 100644 src/Application/UseCases/Coupon/Create/Input.php create mode 100644 src/Application/UseCases/Coupon/Create/Output.php create mode 100644 tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php diff --git a/src/Application/UseCases/Coupon/Create/CreateCoupon.php b/src/Application/UseCases/Coupon/Create/CreateCoupon.php new file mode 100644 index 0000000..32409a1 --- /dev/null +++ b/src/Application/UseCases/Coupon/Create/CreateCoupon.php @@ -0,0 +1,80 @@ + */ +final readonly class CreateCoupon implements UseCase, Transactional +{ + public function __construct( + private CouponRepository $couponRepository, + private BrandRepository $brandRepository, + ) { + } + + #[\Override] + public function execute(BaseInput $input): Output + { + Assert::isInstanceOf($input, Input::class); + + if (! $this->brandRepository->existsByCode($input->brandCode)) { + throw new \DomainException(sprintf('Brand code "%s" was not found.', $input->brandCode->value)); + } + + if ($this->couponRepository->existsByCode($input->code)) { + throw new \DomainException(sprintf('Coupon code "%s" already exists.', $input->code->value)); + } + + $couponMovement = new CouponMovement($input->quantity->value); + $couponMovements = new CouponMovementArray([$couponMovement]); + + $coupon = Coupon::create( + code: $input->code, + name: $input->name, + amount: $input->amount, + picture: $input->picture, + diffusion: $input->diffusion, + validityInformations: $input->validityInformations, + legalInformations: $input->legalInformations, + processingCode: $input->processingCode, + brandCode: $input->brandCode, + createdBy: $input->createdBy, + products: $input->products, + movements: $couponMovements, + quantity: $input->quantity, + availabilityCalendar: $input->availabilityCalendar, + ); + + $this->couponRepository->save($coupon); + + return new Output( + id: $coupon->id, + code: $coupon->code, + name: $coupon->name, + amount: $coupon->amount, + picture: $coupon->picture, + diffusion: $coupon->diffusion, + validityInformations: $coupon->validityInformations, + legalInformations: $coupon->legalInformations, + processingCode: $coupon->processingCode, + brandCode: $coupon->brandCode, + createdBy: $coupon->createdBy, + products: $coupon->products, + movements: $coupon->movements, + quantity: $coupon->quantity, + availabilityCalendar: $coupon->availabilityCalendar, + createdAt: $coupon->createdAt(), + ); + } +} diff --git a/src/Application/UseCases/Coupon/Create/Input.php b/src/Application/UseCases/Coupon/Create/Input.php new file mode 100644 index 0000000..0061c7c --- /dev/null +++ b/src/Application/UseCases/Coupon/Create/Input.php @@ -0,0 +1,40 @@ + new Input( + code: new CouponCode('COU01'), + name: new CouponName('Coupon name'), + amount: new MoneyAmount(new Money(Currency::EUR, 100)), + picture: new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/mobileThumbnail.jpg'), + ), + diffusion: new CouponDiffusion( + new Period( + new DateTime('2026-01-01'), + new DateTime('2026-02-01'), + ), + ), + validityInformations: new CouponValidityInformationArray([ + new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE, + ), + ]), + legalInformations: new CouponLegalInformationArray([ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE, + ), + ]), + processingCode: new CouponProcessingCodeArray([ + new CouponProcessingCode('5425003944130', Country::FRANCE), + ]), + brandCode: new BrandCode('TST89'), + createdBy: ActorId::generate(), + products: new CouponProductArray(), + quantity: new CouponQuantity(1000), + ); + + it('creates a coupon when the brand exists and the code is unique', function () use ($makeInput): void { + // GIVEN + $input = $makeInput(); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('existsByCode')->once()->with($input->code)->andReturn(false); + $couponRepository->shouldReceive('save')->once()->with(\Mockery::type(Coupon::class)); + + $useCase = new CreateCoupon($couponRepository, $brandRepository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(Output::class) + ->and($result->id)->toBeInstanceOf(CouponId::class) + ->and($result->code)->toEqual($input->code) + ->and($result->name)->toEqual($input->name) + ->and($result->amount)->toEqual($input->amount) + ->and($result->picture)->toEqual($input->picture) + ->and($result->diffusion)->toEqual($input->diffusion) + ->and($result->validityInformations)->toEqual($input->validityInformations) + ->and($result->legalInformations)->toEqual($input->legalInformations) + ->and($result->processingCode)->toEqual($input->processingCode) + ->and($result->brandCode)->toEqual($input->brandCode) + ->and($result->createdBy)->toEqual($input->createdBy) + ->and($result->products)->toEqual($input->products) + ->and($result->movements)->toBeInstanceOf(CouponMovementArray::class) + ->and($result->quantity)->toEqual($input->quantity) + ->and($result->availabilityCalendar)->toBeNull() + ->and($result->createdAt)->toBeInstanceOf(\DateTimeInterface::class); + }); + + it('throws when the brand does not exist', function () use ($makeInput): void { + // GIVEN + $input = $makeInput(); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(false); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldNotReceive('existsByCode'); + $couponRepository->shouldNotReceive('save'); + + $useCase = new CreateCoupon($couponRepository, $brandRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Brand code "TST89" was not found.'); + }); + + it('throws when the coupon code already exists', function () use ($makeInput): void { + // GIVEN + $input = $makeInput(); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('existsByCode')->once()->with($input->code)->andReturn(true); + $couponRepository->shouldNotReceive('save'); + + $useCase = new CreateCoupon($couponRepository, $brandRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Coupon code "COU01" already exists.'); + }); +}); From cbd5a56e48b3e7312aef1dd6f2b2fd6bd8caa69b Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 13 Oct 2026 09:59:06 +0200 Subject: [PATCH 04/21] feat: :sparkles: add use case to find coupon details by code --- .../UseCases/Coupon/Find/FindCoupon.php | 68 ++++++ .../UseCases/Coupon/Find/Input.php | 16 ++ .../Coupon/Find/Output/CouponFound.php | 49 ++++ .../Coupon/Find/Output/CouponNotFound.php | 9 + .../UseCases/Coupon/Find/Output/Output.php | 11 + .../Ports/Repositories/CampaignRepository.php | 8 + .../UseCases/Coupon/Find/FindCouponTest.php | 209 ++++++++++++++++++ 7 files changed, 370 insertions(+) create mode 100644 src/Application/UseCases/Coupon/Find/FindCoupon.php create mode 100644 src/Application/UseCases/Coupon/Find/Input.php create mode 100644 src/Application/UseCases/Coupon/Find/Output/CouponFound.php create mode 100644 src/Application/UseCases/Coupon/Find/Output/CouponNotFound.php create mode 100644 src/Application/UseCases/Coupon/Find/Output/Output.php create mode 100644 tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php diff --git a/src/Application/UseCases/Coupon/Find/FindCoupon.php b/src/Application/UseCases/Coupon/Find/FindCoupon.php new file mode 100644 index 0000000..44683f8 --- /dev/null +++ b/src/Application/UseCases/Coupon/Find/FindCoupon.php @@ -0,0 +1,68 @@ + */ +final readonly class FindCoupon implements UseCase +{ + public function __construct( + private CouponRepository $couponRepository, + private BrandRepository $brandRepository, + private CampaignRepository $campaignRepository, + ) { + } + + #[\Override] + public function execute(BaseInput $input): Output + { + Assert::isInstanceOf($input, Input::class); + + $coupon = $this->couponRepository->findByCode($input->code); + + if (! $coupon instanceof Coupon) { + return new CouponNotFound(); + } + + $brand = $this->brandRepository->findByCode($coupon->brandCode); + + if (! $brand instanceof Brand) { + throw new \DomainException(sprintf('Brand code "%s" was not found.', $coupon->brandCode->value)); + } + + $campaigns = $this->campaignRepository->findByCouponCode($coupon->code); + + return new CouponFound( + code: $coupon->code, + name: $coupon->name, + amount: $coupon->amount, + picture: $coupon->picture, + diffusion: $coupon->diffusion, + availabilityCalendar: $coupon->availabilityCalendar, + validityInformations: $coupon->validityInformations, + legalInformations: $coupon->legalInformations, + processingCode: $coupon->processingCode, + brand: $brand, + createdBy: $coupon->createdBy, + products: $coupon->products, + movements: $coupon->movements, + quantity: $coupon->quantity, + campaigns: $campaigns, + createdAt: $coupon->createdAt(), + updatedAt: $coupon->updatedAt(), + ); + } +} diff --git a/src/Application/UseCases/Coupon/Find/Input.php b/src/Application/UseCases/Coupon/Find/Input.php new file mode 100644 index 0000000..37782f2 --- /dev/null +++ b/src/Application/UseCases/Coupon/Find/Input.php @@ -0,0 +1,16 @@ + $campaigns + */ + + public function __construct( + public CouponCode $code, + public CouponName $name, + public MoneyAmount $amount, + public CouponPicture $picture, + public CouponDiffusion $diffusion, + public ?CouponAvailabilityCalendar $availabilityCalendar, + public CouponValidityInformationArray $validityInformations, + public CouponLegalInformationArray $legalInformations, + public CouponProcessingCodeArray $processingCode, + public Brand $brand, + public ActorId $createdBy, + public CouponProductArray $products, + public CouponMovementArray $movements, + public CouponQuantity $quantity, + public array $campaigns, + public \DateTimeInterface $createdAt, + public ?\DateTimeInterface $updatedAt, + ) { + } +} diff --git a/src/Application/UseCases/Coupon/Find/Output/CouponNotFound.php b/src/Application/UseCases/Coupon/Find/Output/CouponNotFound.php new file mode 100644 index 0000000..2e9d3bf --- /dev/null +++ b/src/Application/UseCases/Coupon/Find/Output/CouponNotFound.php @@ -0,0 +1,9 @@ + + */ + public function findByCouponCode(CouponCode $couponCode): array; } diff --git a/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php new file mode 100644 index 0000000..712bd45 --- /dev/null +++ b/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php @@ -0,0 +1,209 @@ +code, + createdBy: ActorId::generate(), + products: new CouponProductArray([ + new CouponIncoCode(35784), + new CouponIncoCode(15522), + ]), + movements: new CouponMovementArray([ + new CouponMovement(1000) + ]), + quantity: new CouponQuantity(1000), + ); + $campaign = Campaign::create( + code: new CampaignCode('CMP01'), + name: new CampaignName('Campaign name'), + publication: true, + printAllAtOnce: true, + coupons: new CampaignCouponArray([ + new CampaignCoupon($coupon->code), + ]), + brandCode: $brand->code, + actorId: ActorId::generate(), + ); + + it('finds a coupon by its code', function () use ($brand, $coupon, $campaign): void { + // GIVEN + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('findByCode')->once()->with($coupon->brandCode)->andReturn($brand); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCouponCode')->once()->with($coupon->code)->andReturn([$campaign]); + + $useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository); + + // WHEN + $result = $useCase->execute(new Input(code: $coupon->code)); + + // THEN + expect($result)->toBeInstanceOf(CouponFound::class) + ->and($result->code)->toBe($coupon->code) + ->and($result->name)->toBe($coupon->name) + ->and($result->amount)->toBe($coupon->amount) + ->and($result->picture)->toBe($coupon->picture) + ->and($result->diffusion)->toBe($coupon->diffusion) + ->and($result->availabilityCalendar)->toBe($coupon->availabilityCalendar) + ->and($result->validityInformations)->toBe($coupon->validityInformations) + ->and($result->legalInformations)->toBe($coupon->legalInformations) + ->and($result->processingCode)->toBe($coupon->processingCode) + ->and($result->brand)->toBe($brand) + ->and($result->createdBy)->toBe($coupon->createdBy) + ->and($result->products)->toBe($coupon->products) + ->and($result->movements)->toBe($coupon->movements) + ->and($result->quantity)->toBe($coupon->quantity) + ->and($result->campaigns)->toBe([$campaign]) + ->and($result->createdAt)->toBeInstanceOf(\DateTimeInterface::class) + ->and($result->updatedAt)->toBeNull(); + }); + + it('returns CouponNotFound when the code is not found', function (): void { + // GIVEN + $code = new CouponCode('COU02'); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(null); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldNotReceive('findByCode'); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldNotReceive('findByCouponCode'); + + $useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository); + + // WHEN + $result = $useCase->execute(new Input(code: $code)); + + // THEN + expect($result)->toBeInstanceOf(CouponNotFound::class); + }); + + it('throws when the brand of the coupon is not found', function () use ($coupon): void { + // GIVEN + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('findByCode')->once()->with($coupon->brandCode)->andReturn(null); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldNotReceive('findByCouponCode'); + + $useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository); + + // WHEN / THEN + expect(fn () => $useCase->execute(new Input(code: $coupon->code))) + ->toThrow(\DomainException::class); + }); + + it('finds a coupon not attached to any campaign', function () use ($brand, $coupon): void { + // GIVEN + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('findByCode')->once()->with($coupon->brandCode)->andReturn($brand); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCouponCode')->once()->with($coupon->code)->andReturn([]); + + $useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository); + + // WHEN + $result = $useCase->execute(new Input(code: $coupon->code)); + + // THEN + expect($result)->toBeInstanceOf(CouponFound::class) + ->and($result->campaigns)->toBe([]); + }); +}); From 5a3815579d99b0abfb30e64d3151e64160d129ba Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Tue, 13 Oct 2026 11:34:05 +0200 Subject: [PATCH 05/21] test: :white_check_mark: cover country rules in coupon use case and value objects --- src/Domain/Models/Entities/Coupon.php | 6 + .../Coupon/CouponCountryCoverage.php | 42 +++ .../Coupon/CouponLegalInformationArray.php | 10 + .../Coupon/CouponMovementArray.php | 6 +- .../Coupon/CouponProcessingCodeArray.php | 10 + .../Coupon/CouponValidityInformationArray.php | 16 +- .../RequestedCoupon/RequestedCoupon.php | 42 +-- .../Coupon/Create/CreateCouponTest.php | 270 ++++++++++++++++-- .../Coupon/CouponCountryCoverageTest.php | 104 +++++++ .../CouponLegalInformationArrayTest.php | 21 +- .../Coupon/CouponMovementArrayTest.php | 2 +- .../Coupon/CouponProcessingCodeArrayTest.php | 18 ++ .../CouponValidityInformationArrayTest.php | 21 +- 13 files changed, 494 insertions(+), 74 deletions(-) create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index 347e3d0..07d1988 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -8,6 +8,7 @@ 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\CouponCountryCoverage; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; use Domain\Models\ValueObjects\Coupon\CouponId; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; @@ -45,6 +46,11 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier public ?CouponAvailabilityCalendar $availabilityCalendar = null, public ?\DateTimeInterface $updatedAt = null, ) { + CouponCountryCoverage::assert( + $this->processingCode, + $this->validityInformations, + $this->legalInformations, + ); } public function id(): CouponId diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php new file mode 100644 index 0000000..7a33d25 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php @@ -0,0 +1,42 @@ +value as $processingCode) { + $country = $processingCode->availableIn; + + if (! self::hasInformationFor($country, $validityInformations)) { + throw new \DomainException(sprintf('A coupon requires validity information for %s.', $country->name)); + } + + if (! self::hasInformationFor($country, $legalInformations)) { + throw new \DomainException(sprintf('A coupon requires legal information for %s.', $country->name)); + } + } + } + + private static function hasInformationFor( + Country $country, + CouponValidityInformationArray | CouponLegalInformationArray $informations, + ): bool { + return array_any( + $informations->value, + fn (CouponValidityInformation | CouponLegalInformation $information): bool => $information->forLocale === $country, + ); + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArray.php index 9fd1189..0925404 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArray.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArray.php @@ -17,12 +17,22 @@ final readonly class CouponLegalInformationArray } $hasFrance = false; + $countries = []; foreach ($this->value as $couponLegalInformation) { if (! $couponLegalInformation instanceof CouponLegalInformation) { throw new \DomainException('Value of CouponLegalInformationArray must be CouponLegalInformation instances.'); } + if (isset($countries[$couponLegalInformation->forLocale->name])) { + throw new \DomainException(sprintf( + 'Only one CouponLegalInformation is allowed per country, duplicate for %s.', + $couponLegalInformation->forLocale->name, + )); + } + + $countries[$couponLegalInformation->forLocale->name] = true; + if ($couponLegalInformation->forLocale === Country::FRANCE) { $hasFrance = true; } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php index 7cfda38..7552374 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php @@ -8,13 +8,13 @@ final readonly class CouponMovementArray { public function __construct( /** @var CouponMovement[] */ - public array $couponMovements, + public array $value, ) { - if ($this->couponMovements === []) { + if ($this->value === []) { throw new \DomainException('CouponMovementArray requires at least one CouponMovement.'); } - foreach ($this->couponMovements as $couponMovement) { + foreach ($this->value as $couponMovement) { if (! $couponMovement instanceof CouponMovement) { throw new \DomainException('Value of CouponMovementArray must be CouponMovement instances.'); } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php index a07baff..7249c40 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php @@ -17,12 +17,22 @@ final readonly class CouponProcessingCodeArray } $hasFrance = false; + $countries = []; foreach ($this->value as $processingCode) { if (! $processingCode instanceof CouponProcessingCode) { throw new \DomainException('Value of CouponProcessingCodeArray must be CouponProcessingCode instances.'); } + if (isset($countries[$processingCode->availableIn->name])) { + throw new \DomainException(sprintf( + 'Only one CouponProcessingCode is allowed per country, duplicate for %s.', + $processingCode->availableIn->name, + )); + } + + $countries[$processingCode->availableIn->name] = true; + if ($processingCode->availableIn === Country::FRANCE) { $hasFrance = true; } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArray.php index 23e415d..d1a9ff8 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArray.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArray.php @@ -17,13 +17,23 @@ final readonly class CouponValidityInformationArray } $hasFrance = false; + $countries = []; - foreach ($this->value as $couponLegalInformation) { - if (! $couponLegalInformation instanceof CouponValidityInformation) { + foreach ($this->value as $couponValidityInformation) { + if (! $couponValidityInformation instanceof CouponValidityInformation) { throw new \DomainException('Value of CouponValidityInformationArray must be CouponValidityInformation instances.'); } - if ($couponLegalInformation->forLocale === Country::FRANCE) { + if (isset($countries[$couponValidityInformation->forLocale->name])) { + throw new \DomainException(sprintf( + 'Only one CouponValidityInformation is allowed per country, duplicate for %s.', + $couponValidityInformation->forLocale->name, + )); + } + + $countries[$couponValidityInformation->forLocale->name] = true; + + if ($couponValidityInformation->forLocale === Country::FRANCE) { $hasFrance = true; } } diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php index 17f67c3..f88965f 100644 --- a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -4,19 +4,16 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\RequestedCoupon; -use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; +use Domain\Models\ValueObjects\Coupon\CouponCountryCoverage; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PrintRules; -use Shared\Enums\Country; final readonly class RequestedCoupon { @@ -32,39 +29,10 @@ final readonly class RequestedCoupon public CouponProductArray $couponProducts, public ?PrintRules $printRules = null, ) { - if ($this->hasProcessingCodeFor(Country::BENELUX)) { - $this->assertCountryIsFullyCovered(Country::BENELUX); - } - } - - private function assertCountryIsFullyCovered(Country $country): void - { - if (! $this->hasProcessingCodeFor($country)) { - throw new \DomainException(sprintf('A coupon requires a processing code for %s.', $country->name)); - } - - if (! $this->hasInformationFor($country, $this->couponValidityInformations)) { - throw new \DomainException(sprintf('A coupon requires validity information for %s.', $country->name)); - } - - if (! $this->hasInformationFor($country, $this->couponLegalInformations)) { - throw new \DomainException(sprintf('A coupon requires legal information for %s.', $country->name)); - } - } - - private function hasProcessingCodeFor(Country $country): bool - { - return array_any( - $this->couponProcessingCodes->value, - fn (CouponProcessingCode $processingCode): bool => $processingCode->availableIn === $country, - ); - } - - private function hasInformationFor(Country $country, CouponValidityInformationArray | CouponLegalInformationArray $informations): bool - { - return array_any( - $informations->value, - fn (CouponValidityInformation | CouponLegalInformation $information): bool => $information->forLocale === $country, + CouponCountryCoverage::assert( + $this->couponProcessingCodes, + $this->couponValidityInformations, + $this->couponLegalInformations, ); } } diff --git a/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php index 81902ac..44b09ca 100644 --- a/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php @@ -11,11 +11,13 @@ use DateTime; 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\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponMovement; use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; @@ -30,6 +32,7 @@ use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CouponRepository; +use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Country; use Shared\Enums\Currency; use Shared\ValueObjects\Money; @@ -37,41 +40,33 @@ use Shared\ValueObjects\Money; covers(CreateCoupon::class, Input::class, Output::class); describe('create coupon use case', function (): void { - $makeInput = fn (): Input => new Input( - code: new CouponCode('COU01'), - name: new CouponName('Coupon name'), - amount: new MoneyAmount(new Money(Currency::EUR, 100)), - picture: new CouponPicture( + $makeInput = fn (array $overrides = []): Input => new Input(...array_merge([ + 'code' => new CouponCode('COU01'), + 'name' => new CouponName('Coupon name'), + 'amount' => new MoneyAmount(new Money(Currency::EUR, 100)), + 'picture' => new CouponPicture( new PicturePath('files/product.jpg'), new PicturePath('files/thumbnail.jpg'), new PicturePath('files/mobileThumbnail.jpg'), ), - diffusion: new CouponDiffusion( - new Period( - new DateTime('2026-01-01'), - new DateTime('2026-02-01'), - ), + 'diffusion' => new CouponDiffusion( + new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), ), - validityInformations: new CouponValidityInformationArray([ - new CouponValidityInformation( - 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - Country::FRANCE, - ), + 'validityInformations' => new CouponValidityInformationArray([ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), ]), - legalInformations: new CouponLegalInformationArray([ - new CouponLegalInformation( - 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', - Country::FRANCE, - ), + 'legalInformations' => new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), ]), - processingCode: new CouponProcessingCodeArray([ + 'processingCode' => new CouponProcessingCodeArray([ new CouponProcessingCode('5425003944130', Country::FRANCE), ]), - brandCode: new BrandCode('TST89'), - createdBy: ActorId::generate(), - products: new CouponProductArray(), - quantity: new CouponQuantity(1000), - ); + 'brandCode' => new BrandCode('TST89'), + 'createdBy' => ActorId::generate(), + 'products' => new CouponProductArray(), + 'quantity' => new CouponQuantity(1000), + 'availabilityCalendar' => null, + ], $overrides)); it('creates a coupon when the brand exists and the code is unique', function () use ($makeInput): void { // GIVEN @@ -148,4 +143,227 @@ describe('create coupon use case', function (): void { // THEN expect($action)->toThrow(\DomainException::class, 'Coupon code "COU01" already exists.'); }); + + it('initialises the stock with a first movement equal to the initial quantity', function () use ($makeInput): void { + // GIVEN + $input = $makeInput(); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('existsByCode')->once()->andReturn(true); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('existsByCode')->once()->andReturn(false); + $couponRepository->shouldReceive('save')->once()->with(\Mockery::on( + fn (Coupon $saved): bool => count($saved->movements->value) === 1 + && $saved->movements->value[0] instanceof CouponMovement + && $saved->movements->value[0]->quantity === $input->quantity->value, + )); + + $useCase = new CreateCoupon($couponRepository, $brandRepository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result->movements)->toBeInstanceOf(CouponMovementArray::class) + ->and($result->movements->value)->toHaveCount(1); + }); + + it('creates a coupon with an availability calendar', function () use ($makeInput): void { + // GIVEN + $calendar = new CouponAvailabilityCalendar([ + new Period(new DateTime('2026-01-05'), new DateTime('2026-01-15')), + new Period(new DateTime('2026-01-20'), new DateTime('2026-01-25')), + ]); + $input = $makeInput(['availabilityCalendar' => $calendar]); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('existsByCode')->once()->andReturn(true); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('existsByCode')->once()->andReturn(false); + $couponRepository->shouldReceive('save')->once()->with(\Mockery::on( + fn (Coupon $saved): bool => $saved->availabilityCalendar === $calendar, + )); + + $useCase = new CreateCoupon($couponRepository, $brandRepository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result->availabilityCalendar)->toBe($calendar); + }); + + it('creates a GS1 coupon with France and Benelux', function () use ($makeInput): void { + // GIVEN + $processingCode = new CouponProcessingCodeArray([ + new CouponProcessingCode('5425003944130', Country::FRANCE), + new CouponProcessingCode('5425003944147', Country::BENELUX), + ]); + $validityInformations = new CouponValidityInformationArray([ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX), + ]); + $legalInformations = new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX), + ]); + $input = $makeInput([ + 'processingCode' => $processingCode, + 'validityInformations' => $validityInformations, + 'legalInformations' => $legalInformations, + ]); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('existsByCode')->once()->andReturn(true); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('existsByCode')->once()->andReturn(false); + $couponRepository->shouldReceive('save')->once()->with(\Mockery::type(Coupon::class)); + + $useCase = new CreateCoupon($couponRepository, $brandRepository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result->processingCode)->toBe($processingCode) + ->and($result->validityInformations)->toBe($validityInformations) + ->and($result->legalInformations)->toBe($legalInformations); + }); + + it('rejects a coupon that targets only benelux', function () use ($makeInput): void { + // GIVEN / WHEN + $action = fn (): Input => $makeInput([ + 'processingCode' => new CouponProcessingCodeArray([ + new CouponProcessingCode('5425003944147', Country::BENELUX), + ]), + 'validityInformations' => new CouponValidityInformationArray([ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX), + ]), + 'legalInformations' => new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX), + ]), + ]); + + // THEN + expect($action)->toThrow(\DomainException::class); + }); + + it('rejects a benelux processing code without benelux legal information', function () use ($makeInput): void { + // GIVEN + $input = $makeInput([ + 'processingCode' => new CouponProcessingCodeArray([ + new CouponProcessingCode('5425003944130', Country::FRANCE), + new CouponProcessingCode('5425003944147', Country::BENELUX), + ]), + 'validityInformations' => new CouponValidityInformationArray([ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX), + ]), + ]); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('existsByCode')->andReturn(true); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('existsByCode')->andReturn(false); + $couponRepository->shouldNotReceive('save'); + + $useCase = new CreateCoupon($couponRepository, $brandRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.'); + }); + + it('rejects a benelux processing code without benelux validity information', function () use ($makeInput): void { + // GIVEN + $input = $makeInput([ + 'processingCode' => new CouponProcessingCodeArray([ + new CouponProcessingCode('5425003944130', Country::FRANCE), + new CouponProcessingCode('5425003944147', Country::BENELUX), + ]), + 'legalInformations' => new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX), + ]), + ]); + + $brandRepository = \Mockery::mock(BrandRepository::class); + $brandRepository->shouldReceive('existsByCode')->andReturn(true); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('existsByCode')->andReturn(false); + $couponRepository->shouldNotReceive('save'); + + $useCase = new CreateCoupon($couponRepository, $brandRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, 'A coupon requires validity information for BENELUX.'); + }); + + it('rejects two processing codes for the same country', function () use ($makeInput): void { + // GIVEN / WHEN + $action = fn (): Input => $makeInput([ + 'processingCode' => new CouponProcessingCodeArray([ + new CouponProcessingCode('5425003944130', Country::FRANCE), + new CouponProcessingCode('5425003944147', Country::FRANCE), + ]), + ]); + + // THEN + expect($action)->toThrow(\DomainException::class); + }); + + it('rejects two legal informations for the same country', function () use ($makeInput): void { + // GIVEN / WHEN + $action = fn (): Input => $makeInput([ + 'legalInformations' => new CouponLegalInformationArray([ + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + ]), + ]); + + // THEN + expect($action)->toThrow(\DomainException::class); + }); + + it('rejects two validity informations for the same country', function () use ($makeInput): void { + // GIVEN / WHEN + $action = fn (): Input => $makeInput([ + 'validityInformations' => new CouponValidityInformationArray([ + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + ]), + ]); + + // THEN + expect($action)->toThrow(\DomainException::class); + }); +}); + +describe('create coupon use case assertions', function (): void { + it('rejects an input of the wrong type', function (): void { + // GIVEN + $useCase = new CreateCoupon( + \Mockery::mock(CouponRepository::class), + \Mockery::mock(BrandRepository::class), + ); + $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/Coupon/CouponCountryCoverageTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php new file mode 100644 index 0000000..d7b9b33 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php @@ -0,0 +1,104 @@ + CouponCountryCoverage::assert( + new CouponProcessingCodeArray([$franceCode]), + new CouponValidityInformationArray([$franceValidity]), + new CouponLegalInformationArray([$franceLegal]), + ); + + // THEN + expect($action)->not->toThrow(\DomainException::class); + }); + + it('accepts a coupon fully covered for france and benelux', function () use ( + $franceCode, + $beneluxCode, + $franceValidity, + $beneluxValidity, + $franceLegal, + $beneluxLegal, + ): void { + // GIVEN / WHEN + $action = fn () => CouponCountryCoverage::assert( + new CouponProcessingCodeArray([$franceCode, $beneluxCode]), + new CouponValidityInformationArray([$franceValidity, $beneluxValidity]), + new CouponLegalInformationArray([$franceLegal, $beneluxLegal]), + ); + + // THEN + expect($action)->not->toThrow(\DomainException::class); + }); + + it('rejects a benelux processing code without benelux validity information', function () use ( + $franceCode, + $beneluxCode, + $franceValidity, + $franceLegal, + $beneluxLegal, + ): void { + // GIVEN / WHEN + $action = fn () => CouponCountryCoverage::assert( + new CouponProcessingCodeArray([$franceCode, $beneluxCode]), + new CouponValidityInformationArray([$franceValidity]), + new CouponLegalInformationArray([$franceLegal, $beneluxLegal]), + ); + + // THEN + expect($action)->toThrow(\DomainException::class, 'A coupon requires validity information for BENELUX.'); + }); + + it('rejects a benelux processing code without benelux legal information', function () use ( + $franceCode, + $beneluxCode, + $franceValidity, + $beneluxValidity, + $franceLegal, + ): void { + // GIVEN / WHEN + $action = fn () => CouponCountryCoverage::assert( + new CouponProcessingCodeArray([$franceCode, $beneluxCode]), + new CouponValidityInformationArray([$franceValidity, $beneluxValidity]), + new CouponLegalInformationArray([$franceLegal]), + ); + + // THEN + expect($action)->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.'); + }); + + it('cannot be instantiated from the outside', function (): void { + // GIVEN + $reflection = new \ReflectionClass(CouponCountryCoverage::class); + $constructor = $reflection->getConstructor(); + + // WHEN + $constructor?->invoke($reflection->newInstanceWithoutConstructor()); + + // THEN + expect($constructor?->isPrivate())->toBeTrue(); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArrayTest.php index 6bcc31f..dab8bcc 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArrayTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponLegalInformationArrayTest.php @@ -11,8 +11,8 @@ use Shared\Enums\Country; covers(CouponLegalInformationArray::class); describe('coupon legal information array', function (): void { - $franceInformation = new CouponLegalInformation('Mentions légales France.', Country::FRANCE); - $beneluxInformation = new CouponLegalInformation('Legal information Benelux.', Country::BENELUX); + $franceInformation = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE); + $beneluxInformation = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX); it('creates an array containing a France legal information', function () use ($franceInformation): void { // GIVEN @@ -47,6 +47,23 @@ describe('coupon legal information array', function (): void { ); }); + it('rejects two legal informations for the same country', function () use ($franceInformation): void { + // GIVEN + $informations = [ + $franceInformation, + new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + ]; + + // WHEN + $action = fn (): CouponLegalInformationArray => new CouponLegalInformationArray($informations); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Only one CouponLegalInformation is allowed per country, duplicate for FRANCE.', + ); + }); + it('rejects a non legal information element', function (): void { // GIVEN / WHEN $action = fn (): CouponLegalInformationArray => new CouponLegalInformationArray(['legal']); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementArrayTest.php index 183b1f5..c152156 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementArrayTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponMovementArrayTest.php @@ -20,7 +20,7 @@ describe('coupon movement array', function (): void { $array = new CouponMovementArray($couponMovements); // THEN - expect($array->couponMovements)->toBe($couponMovements); + expect($array->value)->toBe($couponMovements); }); it('rejects an empty array', function (): void { diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php index 6896093..14b931b 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php @@ -47,6 +47,24 @@ describe('coupon processing code array', function (): void { ); }); + + it('rejects two legal informations for the same country', function () use ($franceCode): void { + // GIVEN + $informations = [ + $franceCode, + new CouponProcessingCode('XYZ0987654321', Country::FRANCE), + ]; + + // WHEN + $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray($informations); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Only one CouponProcessingCode is allowed per country, duplicate for FRANCE.', + ); + }); + it('rejects a non processing code element', function (): void { // GIVEN / WHEN $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray(['ABC1234567890']); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArrayTest.php index 1a31273..c2b81fa 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArrayTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponValidityInformationArrayTest.php @@ -11,8 +11,8 @@ use Shared\Enums\Country; covers(CouponValidityInformationArray::class); describe('coupon validity information array', function (): void { - $franceInformation = new CouponValidityInformation('Valable en France.', Country::FRANCE); - $beneluxInformation = new CouponValidityInformation('Valid in Benelux.', Country::BENELUX); + $franceInformation = new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE); + $beneluxInformation = new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX); it('creates an array containing a France validity information', function () use ($franceInformation): void { // GIVEN @@ -47,6 +47,23 @@ describe('coupon validity information array', function (): void { ); }); + it('rejects two legal informations for the same country', function () use ($franceInformation): void { + // GIVEN + $informations = [ + $franceInformation, + new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), + ]; + + // WHEN + $action = fn (): CouponValidityInformationArray => new CouponValidityInformationArray($informations); + + // THEN + expect($action)->toThrow( + \DomainException::class, + 'Only one CouponValidityInformation is allowed per country, duplicate for FRANCE.', + ); + }); + it('rejects a non validity information element', function (): void { // GIVEN / WHEN $action = fn (): CouponValidityInformationArray => new CouponValidityInformationArray(['validity']); From e4a0ad5b73ddfa1c832fff811ab28ebaa00c73e1 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 13 Oct 2026 15:56:11 +0200 Subject: [PATCH 06/21] fix: :bug: restrict processing code to 13 digits --- .../Coupon/CouponProcessingCode.php | 4 +-- .../Create/CreateCampaignRequestTest.php | 2 +- .../Find/FindCampaignRequestTest.php | 2 +- .../List/ListCampaignsRequestTest.php | 4 +-- .../Reject/RejectCampaignRequestTest.php | 2 +- .../Models/Entities/CampaignRequestTest.php | 2 +- .../Coupon/CouponCountryCoverageTest.php | 4 +-- .../Coupon/CouponProcessingCode.php | 2 +- .../Coupon/CouponProcessingCodeArrayTest.php | 6 ++-- .../Coupon/CouponProcessingCodeTest.php | 28 ++++++------------- .../RequestedCoupon/RequestedCouponTest.php | 4 +-- 11 files changed, 25 insertions(+), 35 deletions(-) diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php b/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php index b464eb3..55ef067 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php @@ -9,7 +9,7 @@ use Shared\Enums\Country; final readonly class CouponProcessingCode implements \Stringable, Equatable { - private const string PATTERN = '/^[A-Za-z0-9]{13}\z/'; + private const string PATTERN = '/^\d{13}\z/'; public function __construct( public string $value, @@ -17,7 +17,7 @@ final readonly class CouponProcessingCode implements \Stringable, Equatable ) { if (preg_match(self::PATTERN, $this->value) !== 1) { throw new \DomainException( - 'Processing Code must be exactly 13 alphanumeric characters, e.g. ABC1234567890.', + 'Processing Code must be exactly 13 numeric characters, e.g. 5425003944130.', ); } } diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php index 99d8350..4cacc2b 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php @@ -56,7 +56,7 @@ describe('create campaign request use case', function (): void { new PicturePath('files/mobileThumbnail.jpg') ), couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('ABC1234567890', Country::FRANCE), + new CouponProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ 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 84f7b5b..0f60f77 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -60,7 +60,7 @@ describe('find campaign request use case', function (): void { new PicturePath('files/mobileThumbnail.jpg') ), couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('ABC1234567890', Country::FRANCE), + new CouponProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ 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 3f5f2e4..e253772 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -54,7 +54,7 @@ describe('list campaigns request use case', function (): void { new PicturePath('files/mobileThumbnail.jpg') ), couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('ABC1234567890', Country::FRANCE), + new CouponProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -79,7 +79,7 @@ describe('list campaigns request use case', function (): void { new PicturePath('files/mobileThumbnail.jpg') ), couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('ABC1234567890', Country::FRANCE), + new CouponProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ 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 e0e11a9..25521e1 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -55,7 +55,7 @@ describe('reject campaign request use case', function (): void { new PicturePath('files/mobileThumbnail.jpg') ), couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('ABC1234567890', Country::FRANCE), + new CouponProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ 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 0790326..896514f 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -50,7 +50,7 @@ describe('campaign request', function (): void { new PicturePath('files/mobileThumbnail.jpg') ), couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('ABC1234567890', Country::FRANCE), + new CouponProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php index d7b9b33..c09fd08 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php @@ -16,8 +16,8 @@ use Shared\Enums\Country; covers(CouponCountryCoverage::class); describe('coupon country coverage', function (): void { - $franceCode = new CouponProcessingCode('ABC1234567890', Country::FRANCE); - $beneluxCode = new CouponProcessingCode('XYZ0987654321', Country::BENELUX); + $franceCode = new CouponProcessingCode('5425003944130', Country::FRANCE); + $beneluxCode = new CouponProcessingCode('5425003944147', Country::BENELUX); $franceValidity = new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE); $beneluxValidity = new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX); $franceLegal = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php index 7b1012a..cb83c24 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php @@ -10,7 +10,7 @@ use Shared\Enums\Country; covers(CouponProcessingCode::class); describe('processing code', function (): void { - $value = 'ABC1234567890'; + $value = '5425003944130'; $emptyValue = ''; it('creates a valid processing code FRANCE', function () use ($value): void { diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php index 14b931b..cbeb252 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php @@ -11,8 +11,8 @@ use Shared\Enums\Country; covers(CouponProcessingCodeArray::class); describe('coupon processing code array', function (): void { - $franceCode = new CouponProcessingCode('ABC1234567890', Country::FRANCE); - $beneluxCode = new CouponProcessingCode('XYZ0987654321', Country::BENELUX); + $franceCode = new CouponProcessingCode('5425003944130', Country::FRANCE); + $beneluxCode = new CouponProcessingCode('5425003944147', Country::BENELUX); it('creates an array containing a France processing code', function () use ($franceCode): void { // GIVEN @@ -52,7 +52,7 @@ describe('coupon processing code array', function (): void { // GIVEN $informations = [ $franceCode, - new CouponProcessingCode('XYZ0987654321', Country::FRANCE), + new CouponProcessingCode('5425003944130', Country::FRANCE), ]; // WHEN diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php index fb61519..ec58e64 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php @@ -10,7 +10,7 @@ use Shared\Enums\Country; covers(CouponProcessingCode::class); describe('coupon processing code', function (): void { - $value = 'ABC1234567890'; + $value = '5425003944130'; it('creates a valid processing code', function () use ($value): void { // GIVEN / WHEN @@ -21,30 +21,20 @@ describe('coupon processing code', function (): void { ->and($code->availableIn)->toBe(Country::FRANCE); }); - it('accepts lowercase and uppercase letters and digits', function (): void { - // GIVEN / WHEN - $code = new CouponProcessingCode('aBc1234567xYz', Country::FRANCE); - - // THEN - expect($code->value)->toBe('aBc1234567xYz'); - }); - it('rejects an invalid value', function (string $invalidValue): void { // GIVEN / WHEN $action = fn (): CouponProcessingCode => new CouponProcessingCode($invalidValue, Country::FRANCE); // THEN - expect($action)->toThrow( - \DomainException::class, - 'Processing Code must be exactly 13 alphanumeric characters, e.g. ABC1234567890.', - ); + expect($action)->toThrow(\DomainException::class, 'Processing Code must be exactly 13 numeric characters'); })->with([ 'empty' => '', - 'too short' => 'ABC123456789', - 'too long' => 'ABC12345678901', - 'special char' => 'ABC123456789!', - 'with space' => 'ABC 123456789', - 'trailing break' => "ABC1234567890\n", + 'too short' => '542500394413', + 'too long' => '54250039441301', + 'with letters' => 'ABC1234567890', + 'special char' => '542500394413!', + 'with space' => '5425 03944130', + 'trailing break' => "5425003944130\n", ]); it('is equal to another processing code with the same value', function () use ($value): void { @@ -68,7 +58,7 @@ describe('coupon processing code', function (): void { it('is not equal to a processing code with a different value', function () use ($value): void { // GIVEN $code = new CouponProcessingCode($value, Country::FRANCE); - $other = new CouponProcessingCode('XYZ0987654321', Country::FRANCE); + $other = new CouponProcessingCode('4006381333931', Country::FRANCE); // WHEN / THEN expect($code->equals($other))->toBeFalse(); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index fa7ed31..06368aa 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -38,8 +38,8 @@ describe('requested coupon', function (): void { new PicturePath('files/mobileThumbnail.jpg') ); - $franceProcessingCode = new CouponProcessingCode('ABC1234567890', Country::FRANCE); - $beneluxProcessingCode = new CouponProcessingCode('XYZ9876543210', Country::BENELUX); + $franceProcessingCode = new CouponProcessingCode('5425003944130', Country::FRANCE); + $beneluxProcessingCode = new CouponProcessingCode('5425003944147', Country::BENELUX); $couponProcessingCodes = new CouponProcessingCodeArray([ $franceProcessingCode, $beneluxProcessingCode, From 5c6c1b12226efc4c31518f16884a9fa70641738a Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 12 Oct 2026 17:26:14 +0200 Subject: [PATCH 07/21] feat: :sparkles: add ListCoupons Use Case and tests --- .../UseCases/Coupon/Create/CreateCoupon.php | 2 +- .../UseCases/Coupon/List/CouponListItem.php | 25 ++ .../UseCases/Coupon/List/Input.php | 20 ++ .../UseCases/Coupon/List/ListCoupons.php | 53 ++++ .../UseCases/Coupon/List/Output.php | 17 ++ .../Coupon/CouponSortCriteria.php | 14 ++ .../ValueObjects/Coupon/CouponSortField.php | 12 + .../Ports/Repositories/CouponRepository.php | 13 + .../UseCases/Coupon/List/ListCouponTest.php | 231 ++++++++++++++++++ 9 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 src/Application/UseCases/Coupon/List/CouponListItem.php create mode 100644 src/Application/UseCases/Coupon/List/Input.php create mode 100644 src/Application/UseCases/Coupon/List/ListCoupons.php create mode 100644 src/Application/UseCases/Coupon/List/Output.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponSortField.php create mode 100644 tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php diff --git a/src/Application/UseCases/Coupon/Create/CreateCoupon.php b/src/Application/UseCases/Coupon/Create/CreateCoupon.php index 32409a1..287275e 100644 --- a/src/Application/UseCases/Coupon/Create/CreateCoupon.php +++ b/src/Application/UseCases/Coupon/Create/CreateCoupon.php @@ -73,8 +73,8 @@ final readonly class CreateCoupon implements UseCase, Transactional products: $coupon->products, movements: $coupon->movements, quantity: $coupon->quantity, - availabilityCalendar: $coupon->availabilityCalendar, createdAt: $coupon->createdAt(), + availabilityCalendar: $coupon->availabilityCalendar, ); } } diff --git a/src/Application/UseCases/Coupon/List/CouponListItem.php b/src/Application/UseCases/Coupon/List/CouponListItem.php new file mode 100644 index 0000000..2439e48 --- /dev/null +++ b/src/Application/UseCases/Coupon/List/CouponListItem.php @@ -0,0 +1,25 @@ + */ +final readonly class ListCoupons implements UseCase +{ + public function __construct( + private CouponRepository $repository, + ) { + } + + #[\Override] + public function execute(BaseInput $input): Output + { + Assert::isInstanceOf($input, Input::class); + + $result = $this->repository->findAll( + pagination: $input->pagination, + sortCriteria: $input->sort, + brandCode: $input->brandCode, + ); + + $items = array_map( + static fn (Coupon $coupon): CouponListItem => new CouponListItem( + code: $coupon->code, + name: $coupon->name, + brandCode: $coupon->brandCode, + picture: $coupon->picture, + diffusion: $coupon->diffusion, + availabilityCalendar: $coupon->availabilityCalendar, + ), + $result->getItems(), + ); + + return new Output( + coupons: new Paginated( + pagination: $result->getPagination(), + items: $items, + total: $result->getTotal(), + ), + ); + } +} diff --git a/src/Application/UseCases/Coupon/List/Output.php b/src/Application/UseCases/Coupon/List/Output.php new file mode 100644 index 0000000..27dc8db --- /dev/null +++ b/src/Application/UseCases/Coupon/List/Output.php @@ -0,0 +1,17 @@ + $coupons */ + public function __construct( + public Paginated $coupons, + ) { + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php b/src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php new file mode 100644 index 0000000..8385df0 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php @@ -0,0 +1,14 @@ + @@ -17,5 +21,14 @@ interface CouponRepository extends Repository public function findByCode(CouponCode $code): ?Coupon; + /** + * @return Paginated + */ + public function findAll( + Pagination $pagination, + ?CouponSortCriteria $sortCriteria = null, + ?BrandCode $brandCode = null, + ): Paginated; + public function save(Coupon $brand): void; } diff --git a/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php b/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php new file mode 100644 index 0000000..a3106e6 --- /dev/null +++ b/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php @@ -0,0 +1,231 @@ +shouldReceive('findAll') + ->once() + ->with($pagination, CouponSortCriteria::class, $brandCode) + ->andReturn($paginated); + + $useCase = new ListCoupons($repository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(Output::class); + + $items = $result->coupons->getItems(); + expect($items)->toHaveCount(2) + ->and($items[0])->toBeInstanceOf(CouponListItem::class) + ->and($items[0]->code)->toBe($coupon1->code) + ->and($items[0]->name)->toBe($coupon1->name) + ->and($items[0]->brandCode)->toBe($coupon1->brandCode) + ->and($items[0]->picture)->toBe($coupon1->picture) + ->and($items[0]->diffusion)->toBe($coupon1->diffusion) + ->and($items[0]->availabilityCalendar)->toBe($coupon1->availabilityCalendar) + ->and($items[1]->code)->toBe($coupon2->code) + ->and($items[1]->brandCode)->toBe($coupon2->brandCode) + ->and($items[1]->availabilityCalendar)->toBeNull() + ->and($result->coupons->getTotal())->toBe(2) + ->and($result->coupons->getPagination())->toBe($pagination); + }); + + it('maps repository coupons to list items, preserving specific pagination and sort', function () use ( + $coupon1, + $coupon2, + $pagination, + ): void { + // GIVEN + $paginated = new Paginated($pagination, [$coupon2, $coupon1], 2); + $sort = new CouponSortCriteria(field: CouponSortField::BrandCode, direction: \SortDirection::Descending); + $input = new Input(pagination: $pagination, sort: $sort); + + $repository = \Mockery::mock(CouponRepository::class); + $repository->shouldReceive('findAll') + ->once() + ->with($pagination, CouponSortCriteria::class, null) + ->andReturn($paginated); + + $useCase = new ListCoupons($repository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(Output::class); + + $items = $result->coupons->getItems(); + expect($items)->toHaveCount(2) + ->and($items[1])->toBeInstanceOf(CouponListItem::class) + ->and($items[1]->code)->toBe($coupon1->code) + ->and($items[1]->name)->toBe($coupon1->name) + ->and($items[1]->brandCode)->toBe($coupon1->brandCode) + ->and($items[1]->picture)->toBe($coupon1->picture) + ->and($items[1]->diffusion)->toBe($coupon1->diffusion) + ->and($items[1]->availabilityCalendar)->toBe($coupon1->availabilityCalendar) + ->and($items[0]->code)->toBe($coupon2->code) + ->and($items[0]->brandCode)->toBe($coupon2->brandCode) + ->and($items[0]->availabilityCalendar)->toBeNull() + ->and($result->coupons->getTotal())->toBe(2) + ->and($result->coupons->getPagination())->toBe($pagination); + }); + + it('returns an empty list without error when there are no coupons', function (): void { + // GIVEN + $pagination = Pagination::firstPage(); + $paginated = new Paginated($pagination, [], 0); + $input = new Input(pagination: $pagination); + + $repository = \Mockery::mock(CouponRepository::class); + $repository->shouldReceive('findAll') + ->once() + ->with($pagination, null, null) + ->andReturn($paginated); + + $useCase = new ListCoupons($repository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result->coupons->getItems())->toBe([]) + ->and($result->coupons->getTotal())->toBe(0); + }); +}); + +describe('list coupons use case assertions', function (): void { + it('rejects an input of the wrong type', function (): void { + // GIVEN + $repository = \Mockery::mock(CouponRepository::class); + $useCase = new ListCoupons($repository); + $invalidInput = new class () implements BaseInput + { + }; + + // WHEN + $action = fn (): Output => $useCase->execute($invalidInput); + + // THEN + expect($action)->toThrow(\InvalidArgumentException::class); + }); +}); From d75f132ee180efd202def3e4c5f296b5cb3293f5 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Tue, 13 Oct 2026 15:41:21 +0200 Subject: [PATCH 08/21] fix: :truck: renaming classes that do not belong directly to Coupons --- .../UseCases/Coupon/Create/Input.php | 4 +-- .../UseCases/Coupon/Create/Output.php | 4 +-- .../Coupon/Find/Output/CouponFound.php | 4 +-- src/Domain/Models/Entities/Coupon.php | 8 ++--- .../Coupon/CouponCountryCoverage.php | 3 +- .../Coupon/CouponProductArray.php | 8 +++-- .../CouponIncoCode.php => IncoCode.php} | 4 +-- ...nProcessingCode.php => ProcessingCode.php} | 4 +-- ...gCodeArray.php => ProcessingCodeArray.php} | 16 ++++----- .../RequestedCoupon/RequestedCoupon.php | 4 +-- .../Create/CreateCampaignRequestTest.php | 12 +++---- .../Find/FindCampaignRequestTest.php | 12 +++---- .../List/ListCampaignsRequestTest.php | 18 +++++----- .../Reject/RejectCampaignRequestTest.php | 12 +++---- .../Coupon/Create/CreateCouponTest.php | 36 +++++++++---------- .../UseCases/Coupon/Find/FindCouponTest.php | 14 ++++---- .../UseCases/Coupon/List/ListCouponTest.php | 18 +++++----- .../Models/Entities/CampaignRequestTest.php | 12 +++---- .../Domain/Models/Entities/CouponTest.php | 14 ++++---- .../Coupon/CouponCountryCoverageTest.php | 16 ++++----- .../Coupon/CouponProcessingCodeTest.php | 24 ++++++------- .../Coupon/CouponProductArrayTest.php | 10 +++--- ...ouponIncoCodeTest.php => IncoCodeTest.php} | 8 ++--- ...nProcessingCode.php => ProcessingCode.php} | 10 +++--- ...ayTest.php => ProcessingCodeArrayTest.php} | 36 +++++++++---------- .../RequestedCoupon/RequestedCouponTest.php | 22 ++++++------ 26 files changed, 168 insertions(+), 165 deletions(-) rename src/Domain/Models/ValueObjects/{Coupon/CouponIncoCode.php => IncoCode.php} (77%) rename src/Domain/Models/ValueObjects/{Coupon/CouponProcessingCode.php => ProcessingCode.php} (85%) rename src/Domain/Models/ValueObjects/{Coupon/CouponProcessingCodeArray.php => ProcessingCodeArray.php} (59%) rename tests/Unit/Domain/Models/ValueObjects/{Coupon/CouponIncoCodeTest.php => IncoCodeTest.php} (71%) rename tests/Unit/Domain/Models/ValueObjects/{Coupon/CouponProcessingCode.php => ProcessingCode.php} (77%) rename tests/Unit/Domain/Models/ValueObjects/{Coupon/CouponProcessingCodeArrayTest.php => ProcessingCodeArrayTest.php} (57%) diff --git a/src/Application/UseCases/Coupon/Create/Input.php b/src/Application/UseCases/Coupon/Create/Input.php index 0061c7c..2fe9c91 100644 --- a/src/Application/UseCases/Coupon/Create/Input.php +++ b/src/Application/UseCases/Coupon/Create/Input.php @@ -12,11 +12,11 @@ use Domain\Models\ValueObjects\Coupon\CouponDiffusion; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; +use Domain\Models\ValueObjects\ProcessingCodeArray; use Shared\DDD\UseCases\Input as UseCaseInput; final readonly class Input implements UseCaseInput @@ -29,7 +29,7 @@ final readonly class Input implements UseCaseInput public CouponDiffusion $diffusion, public CouponValidityInformationArray $validityInformations, public CouponLegalInformationArray $legalInformations, - public CouponProcessingCodeArray $processingCode, + public ProcessingCodeArray $processingCode, public BrandCode $brandCode, public ActorId $createdBy, public CouponProductArray $products, diff --git a/src/Application/UseCases/Coupon/Create/Output.php b/src/Application/UseCases/Coupon/Create/Output.php index 0a665ba..a1ae503 100644 --- a/src/Application/UseCases/Coupon/Create/Output.php +++ b/src/Application/UseCases/Coupon/Create/Output.php @@ -14,11 +14,11 @@ use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; +use Domain\Models\ValueObjects\ProcessingCodeArray; use Shared\DDD\UseCases\Output as UseCaseOutput; final readonly class Output implements UseCaseOutput @@ -32,7 +32,7 @@ final readonly class Output implements UseCaseOutput public CouponDiffusion $diffusion, public CouponValidityInformationArray $validityInformations, public CouponLegalInformationArray $legalInformations, - public CouponProcessingCodeArray $processingCode, + public ProcessingCodeArray $processingCode, public BrandCode $brandCode, public ActorId $createdBy, public CouponProductArray $products, diff --git a/src/Application/UseCases/Coupon/Find/Output/CouponFound.php b/src/Application/UseCases/Coupon/Find/Output/CouponFound.php index dc4d18c..2147114 100644 --- a/src/Application/UseCases/Coupon/Find/Output/CouponFound.php +++ b/src/Application/UseCases/Coupon/Find/Output/CouponFound.php @@ -14,11 +14,11 @@ use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; +use Domain\Models\ValueObjects\ProcessingCodeArray; final readonly class CouponFound implements Output { @@ -35,7 +35,7 @@ final readonly class CouponFound implements Output public ?CouponAvailabilityCalendar $availabilityCalendar, public CouponValidityInformationArray $validityInformations, public CouponLegalInformationArray $legalInformations, - public CouponProcessingCodeArray $processingCode, + public ProcessingCodeArray $processingCode, public Brand $brand, public ActorId $createdBy, public CouponProductArray $products, diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index 07d1988..4995461 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -15,11 +15,11 @@ use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; +use Domain\Models\ValueObjects\ProcessingCodeArray; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; @@ -37,7 +37,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier public CouponDiffusion $diffusion, public CouponValidityInformationArray $validityInformations, public CouponLegalInformationArray $legalInformations, - public CouponProcessingCodeArray $processingCode, + public ProcessingCodeArray $processingCode, public BrandCode $brandCode, public ActorId $createdBy, public CouponProductArray $products, @@ -76,7 +76,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier CouponDiffusion $diffusion, CouponValidityInformationArray $validityInformations, CouponLegalInformationArray $legalInformations, - CouponProcessingCodeArray $processingCode, + ProcessingCodeArray $processingCode, BrandCode $brandCode, ActorId $createdBy, CouponProductArray $products, @@ -114,7 +114,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier CouponDiffusion $diffusion, CouponValidityInformationArray $validityInformations, CouponLegalInformationArray $legalInformations, - CouponProcessingCodeArray $processingCode, + ProcessingCodeArray $processingCode, BrandCode $brandCode, ActorId $createdBy, CouponProductArray $products, diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php index 7a33d25..b00aca7 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Coupon; +use Domain\Models\ValueObjects\ProcessingCodeArray; use Shared\Enums\Country; final readonly class CouponCountryCoverage @@ -13,7 +14,7 @@ final readonly class CouponCountryCoverage } public static function assert( - CouponProcessingCodeArray $processingCodes, + ProcessingCodeArray $processingCodes, CouponValidityInformationArray $validityInformations, CouponLegalInformationArray $legalInformations, ): void { diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProductArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponProductArray.php index e1f9cbb..60d3005 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProductArray.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponProductArray.php @@ -4,15 +4,17 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects\Coupon; +use Domain\Models\ValueObjects\IncoCode; + final readonly class CouponProductArray { public function __construct( - /** @var CouponIncoCode[] */ + /** @var IncoCode[] */ public array $value = [], ) { foreach ($this->value as $product) { - if (! $product instanceof CouponIncoCode) { - throw new \DomainException('Value of CouponProductArray must be CouponIncoCode instances.'); + if (! $product instanceof IncoCode) { + throw new \DomainException('Value of CouponProductArray must be IncoCode instances.'); } } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponIncoCode.php b/src/Domain/Models/ValueObjects/IncoCode.php similarity index 77% rename from src/Domain/Models/ValueObjects/Coupon/CouponIncoCode.php rename to src/Domain/Models/ValueObjects/IncoCode.php index 070fc5a..0055e87 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponIncoCode.php +++ b/src/Domain/Models/ValueObjects/IncoCode.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Domain\Models\ValueObjects\Coupon; +namespace Domain\Models\ValueObjects; -final readonly class CouponIncoCode +final readonly class IncoCode { /** * @throws \DomainException diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php b/src/Domain/Models/ValueObjects/ProcessingCode.php similarity index 85% rename from src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php rename to src/Domain/Models/ValueObjects/ProcessingCode.php index 55ef067..bad00ff 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php +++ b/src/Domain/Models/ValueObjects/ProcessingCode.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Domain\Models\ValueObjects\Coupon; +namespace Domain\Models\ValueObjects; use Shared\DDD\Models\Equatable; use Shared\Enums\Country; -final readonly class CouponProcessingCode implements \Stringable, Equatable +final readonly class ProcessingCode implements \Stringable, Equatable { private const string PATTERN = '/^\d{13}\z/'; diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php b/src/Domain/Models/ValueObjects/ProcessingCodeArray.php similarity index 59% rename from src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php rename to src/Domain/Models/ValueObjects/ProcessingCodeArray.php index 7249c40..e95d1fe 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArray.php +++ b/src/Domain/Models/ValueObjects/ProcessingCodeArray.php @@ -2,31 +2,31 @@ declare(strict_types=1); -namespace Domain\Models\ValueObjects\Coupon; +namespace Domain\Models\ValueObjects; use Shared\Enums\Country; -final readonly class CouponProcessingCodeArray +final readonly class ProcessingCodeArray { public function __construct( - /** @var CouponProcessingCode[] */ + /** @var ProcessingCode[] */ public array $value, ) { if ($this->value === []) { - throw new \DomainException('ProcessingCodes requires at least one CouponProcessingCode.'); + throw new \DomainException('ProcessingCodes requires at least one ProcessingCode.'); } $hasFrance = false; $countries = []; foreach ($this->value as $processingCode) { - if (! $processingCode instanceof CouponProcessingCode) { - throw new \DomainException('Value of CouponProcessingCodeArray must be CouponProcessingCode instances.'); + if (! $processingCode instanceof ProcessingCode) { + throw new \DomainException('Value of ProcessingCodeArray must be ProcessingCode instances.'); } if (isset($countries[$processingCode->availableIn->name])) { throw new \DomainException(sprintf( - 'Only one CouponProcessingCode is allowed per country, duplicate for %s.', + 'Only one ProcessingCode is allowed per country, duplicate for %s.', $processingCode->availableIn->name, )); } @@ -39,7 +39,7 @@ final readonly class CouponProcessingCodeArray } if (! $hasFrance) { - throw new \DomainException('CouponProcessingCodes requires a CouponProcessingCode for France.'); + throw new \DomainException('ProcessingCodes requires a ProcessingCode for France.'); } } } diff --git a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php index f88965f..d589b30 100644 --- a/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php +++ b/src/Domain/Models/ValueObjects/RequestedCoupon/RequestedCoupon.php @@ -8,12 +8,12 @@ use Domain\Models\ValueObjects\Coupon\CouponCountryCoverage; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PrintRules; +use Domain\Models\ValueObjects\ProcessingCodeArray; final readonly class RequestedCoupon { @@ -23,7 +23,7 @@ final readonly class RequestedCoupon public Period $diffusion, public MoneyAmount $moneyAmount, public CouponPicture $couponPicture, - public CouponProcessingCodeArray $couponProcessingCodes, + public ProcessingCodeArray $couponProcessingCodes, public CouponValidityInformationArray $couponValidityInformations, public CouponLegalInformationArray $couponLegalInformations, public CouponProductArray $couponProducts, diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php index 4cacc2b..60e8429 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php @@ -14,20 +14,20 @@ 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\ValueObjects\Coupon\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +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\ProcessingCodeArray; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Ports\Repositories\BrandRepository; @@ -55,8 +55,8 @@ describe('create campaign request use case', function (): void { new PicturePath('files/thumbnail.jpg'), new PicturePath('files/mobileThumbnail.jpg') ), - couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), + couponProcessingCodes: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -66,7 +66,7 @@ describe('create campaign request use case', function (): void { new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) ]), - couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]), 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 0f60f77..ef0ec38 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -21,20 +21,20 @@ 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\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +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\ProcessingCodeArray; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Ports\Repositories\BrandRepository; @@ -59,8 +59,8 @@ describe('find campaign request use case', function (): void { new PicturePath('files/thumbnail.jpg'), new PicturePath('files/mobileThumbnail.jpg') ), - couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), + couponProcessingCodes: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -70,7 +70,7 @@ describe('find campaign request use case', function (): void { new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) ]), - couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]), printRules: new PrintRules(maxPerCustomer: 3), ); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php index e253772..c4d8b81 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -16,20 +16,20 @@ 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\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +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\ProcessingCodeArray; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; @@ -53,8 +53,8 @@ describe('list campaigns request use case', function (): void { new PicturePath('files/thumbnail.jpg'), new PicturePath('files/mobileThumbnail.jpg') ), - couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), + couponProcessingCodes: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -64,7 +64,7 @@ describe('list campaigns request use case', function (): void { new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) ]), - couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]), printRules: new PrintRules(maxPerCustomer: 3), ); @@ -78,8 +78,8 @@ describe('list campaigns request use case', function (): void { new PicturePath('files/thumbnail.jpg'), new PicturePath('files/mobileThumbnail.jpg') ), - couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), + couponProcessingCodes: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -89,7 +89,7 @@ describe('list campaigns request use case', function (): void { new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) ]), - couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]), printRules: new PrintRules(maxPerCustomer: 3), ); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index 25521e1..3c54d9c 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -19,20 +19,20 @@ 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\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +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\ProcessingCodeArray; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Domain\Ports\Repositories\CampaignRequestRepository; @@ -54,8 +54,8 @@ describe('reject campaign request use case', function (): void { new PicturePath('files/thumbnail.jpg'), new PicturePath('files/mobileThumbnail.jpg') ), - couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), + couponProcessingCodes: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -65,7 +65,7 @@ describe('reject campaign request use case', function (): void { new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) ]), - couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]), printRules: new PrintRules(maxPerCustomer: 3), ); diff --git a/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php index 44b09ca..dbad074 100644 --- a/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php @@ -21,8 +21,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovement; use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; @@ -30,6 +28,8 @@ use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\Period; use Domain\Models\ValueObjects\PicturePath; +use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\ProcessingCodeArray; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CouponRepository; use Shared\DDD\UseCases\Input as BaseInput; @@ -58,8 +58,8 @@ describe('create coupon use case', function (): void { 'legalInformations' => new CouponLegalInformationArray([ new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), ]), - 'processingCode' => new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), + 'processingCode' => new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]), 'brandCode' => new BrandCode('TST89'), 'createdBy' => ActorId::generate(), @@ -197,9 +197,9 @@ describe('create coupon use case', function (): void { it('creates a GS1 coupon with France and Benelux', function () use ($makeInput): void { // GIVEN - $processingCode = new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), - new CouponProcessingCode('5425003944147', Country::BENELUX), + $processingCode = new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('5425003944147', Country::BENELUX), ]); $validityInformations = new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -236,8 +236,8 @@ describe('create coupon use case', function (): void { it('rejects a coupon that targets only benelux', function () use ($makeInput): void { // GIVEN / WHEN $action = fn (): Input => $makeInput([ - 'processingCode' => new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944147', Country::BENELUX), + 'processingCode' => new ProcessingCodeArray([ + new ProcessingCode('5425003944147', Country::BENELUX), ]), 'validityInformations' => new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX), @@ -254,9 +254,9 @@ describe('create coupon use case', function (): void { it('rejects a benelux processing code without benelux legal information', function () use ($makeInput): void { // GIVEN $input = $makeInput([ - 'processingCode' => new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), - new CouponProcessingCode('5425003944147', Country::BENELUX), + 'processingCode' => new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('5425003944147', Country::BENELUX), ]), 'validityInformations' => new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -283,9 +283,9 @@ describe('create coupon use case', function (): void { it('rejects a benelux processing code without benelux validity information', function () use ($makeInput): void { // GIVEN $input = $makeInput([ - 'processingCode' => new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), - new CouponProcessingCode('5425003944147', Country::BENELUX), + 'processingCode' => new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('5425003944147', Country::BENELUX), ]), 'legalInformations' => new CouponLegalInformationArray([ new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -312,9 +312,9 @@ describe('create coupon use case', function (): void { it('rejects two processing codes for the same country', function () use ($makeInput): void { // GIVEN / WHEN $action = fn (): Input => $makeInput([ - 'processingCode' => new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), - new CouponProcessingCode('5425003944147', Country::FRANCE), + 'processingCode' => new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('5425003944147', Country::FRANCE), ]), ]); diff --git a/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php index 712bd45..305fd76 100644 --- a/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php @@ -21,22 +21,22 @@ use Domain\Models\ValueObjects\Campaign\CampaignCouponArray; use Domain\Models\ValueObjects\Campaign\CampaignName; use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; -use Domain\Models\ValueObjects\Coupon\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponMovement; use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +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 Domain\Models\ValueObjects\ProcessingCodeArray; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CampaignRepository; use Domain\Ports\Repositories\CouponRepository; @@ -83,14 +83,14 @@ describe('find coupon use case', function (): void { Country::FRANCE ), ]), - processingCode: new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), + processingCode: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]), brandCode: $brand->code, createdBy: ActorId::generate(), products: new CouponProductArray([ - new CouponIncoCode(35784), - new CouponIncoCode(15522), + new IncoCode(35784), + new IncoCode(15522), ]), movements: new CouponMovementArray([ new CouponMovement(1000) diff --git a/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php b/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php index a3106e6..205eb6f 100644 --- a/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php @@ -16,24 +16,24 @@ 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\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponMovement; use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponSortCriteria; use Domain\Models\ValueObjects\Coupon\CouponSortField; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +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 Domain\Models\ValueObjects\ProcessingCodeArray; use Domain\Ports\Repositories\CouponRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\Enums\Country; @@ -65,12 +65,12 @@ describe('list coupons use case', function (): void { legalInformations: new CouponLegalInformationArray([ new CouponLegalInformation('Lorem ipsum dolor sit amet.', Country::FRANCE), ]), - processingCode: new CouponProcessingCodeArray([ - new CouponProcessingCode('ABC1234567890', Country::FRANCE), + processingCode: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]), brandCode: new BrandCode('AAA01'), createdBy: ActorId::generate(), - products: new CouponProductArray([new CouponIncoCode(35784)]), + products: new CouponProductArray([new IncoCode(35784)]), movements: new CouponMovementArray([new CouponMovement(100)]), quantity: new CouponQuantity(100), availabilityCalendar: new CouponAvailabilityCalendar([ @@ -97,12 +97,12 @@ describe('list coupons use case', function (): void { legalInformations: new CouponLegalInformationArray([ new CouponLegalInformation('Lorem ipsum dolor sit amet.', Country::FRANCE), ]), - processingCode: new CouponProcessingCodeArray([ - new CouponProcessingCode('DEF0123456789', Country::FRANCE), + processingCode: new ProcessingCodeArray([ + new ProcessingCode('5425003944147', Country::FRANCE), ]), brandCode: new BrandCode('BBB02'), createdBy: ActorId::generate(), - products: new CouponProductArray([new CouponIncoCode(35784)]), + products: new CouponProductArray([new IncoCode(35784)]), movements: new CouponMovementArray([new CouponMovement(100)]), quantity: new CouponQuantity(100), ); diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index 896514f..edf6a44 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -14,20 +14,20 @@ 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\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +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\ProcessingCodeArray; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Ramsey\Uuid\Uuid; @@ -49,8 +49,8 @@ describe('campaign request', function (): void { new PicturePath('files/thumbnail.jpg'), new PicturePath('files/mobileThumbnail.jpg') ), - couponProcessingCodes: new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), + couponProcessingCodes: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]), couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -60,7 +60,7 @@ describe('campaign request', function (): void { new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX) ]), - couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]), + couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]), printRules: new PrintRules(maxPerCustomer: 3), ); $code = new BrandCode('AAA01'); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index 003084b..1e77108 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -6,7 +6,6 @@ namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; use DateTime; -use Shared\Enums\Country; use Domain\Models\Entities\Coupon; use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; @@ -14,23 +13,24 @@ 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\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponMovement; use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +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 Domain\Models\ValueObjects\ProcessingCodeArray; use Ramsey\Uuid\Uuid; +use Shared\Enums\Country; use Shared\Enums\Currency; use Shared\ValueObjects\Money; @@ -58,12 +58,12 @@ describe('coupon', function (): void { Country::FRANCE, ) ]); - $processingCode = new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), + $processingCode = new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]); $brandCode = new BrandCode('AAA01'); $createdBy = ActorId::generate(); - $products = new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]); + $products = new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]); $movements = new CouponMovementArray([new CouponMovement(100)]); $quantity = new CouponQuantity(100); $availabilityCalendar = new CouponAvailabilityCalendar([ diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php index c09fd08..1a7bcf0 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php @@ -7,17 +7,17 @@ namespace Tests\Unit\Domain\Models\ValueObjects\Coupon; use Domain\Models\ValueObjects\Coupon\CouponCountryCoverage; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\ProcessingCodeArray; use Shared\Enums\Country; covers(CouponCountryCoverage::class); describe('coupon country coverage', function (): void { - $franceCode = new CouponProcessingCode('5425003944130', Country::FRANCE); - $beneluxCode = new CouponProcessingCode('5425003944147', Country::BENELUX); + $franceCode = new ProcessingCode('5425003944130', Country::FRANCE); + $beneluxCode = new ProcessingCode('5425003944147', Country::BENELUX); $franceValidity = new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE); $beneluxValidity = new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX); $franceLegal = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE); @@ -26,7 +26,7 @@ describe('coupon country coverage', function (): void { it('accepts a france only coupon', function () use ($franceCode, $franceValidity, $franceLegal): void { // GIVEN / WHEN $action = fn () => CouponCountryCoverage::assert( - new CouponProcessingCodeArray([$franceCode]), + new ProcessingCodeArray([$franceCode]), new CouponValidityInformationArray([$franceValidity]), new CouponLegalInformationArray([$franceLegal]), ); @@ -45,7 +45,7 @@ describe('coupon country coverage', function (): void { ): void { // GIVEN / WHEN $action = fn () => CouponCountryCoverage::assert( - new CouponProcessingCodeArray([$franceCode, $beneluxCode]), + new ProcessingCodeArray([$franceCode, $beneluxCode]), new CouponValidityInformationArray([$franceValidity, $beneluxValidity]), new CouponLegalInformationArray([$franceLegal, $beneluxLegal]), ); @@ -63,7 +63,7 @@ describe('coupon country coverage', function (): void { ): void { // GIVEN / WHEN $action = fn () => CouponCountryCoverage::assert( - new CouponProcessingCodeArray([$franceCode, $beneluxCode]), + new ProcessingCodeArray([$franceCode, $beneluxCode]), new CouponValidityInformationArray([$franceValidity]), new CouponLegalInformationArray([$franceLegal, $beneluxLegal]), ); @@ -81,7 +81,7 @@ describe('coupon country coverage', function (): void { ): void { // GIVEN / WHEN $action = fn () => CouponCountryCoverage::assert( - new CouponProcessingCodeArray([$franceCode, $beneluxCode]), + new ProcessingCodeArray([$franceCode, $beneluxCode]), new CouponValidityInformationArray([$franceValidity, $beneluxValidity]), new CouponLegalInformationArray([$franceLegal]), ); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php index ec58e64..3302b48 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeTest.php @@ -4,17 +4,17 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\Coupon; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; +use Domain\Models\ValueObjects\ProcessingCode; use Shared\Enums\Country; -covers(CouponProcessingCode::class); +covers(ProcessingCode::class); describe('coupon processing code', function (): void { $value = '5425003944130'; it('creates a valid processing code', function () use ($value): void { // GIVEN / WHEN - $code = new CouponProcessingCode($value, Country::FRANCE); + $code = new ProcessingCode($value, Country::FRANCE); // THEN expect($code->value)->toBe($value) @@ -23,7 +23,7 @@ describe('coupon processing code', function (): void { it('rejects an invalid value', function (string $invalidValue): void { // GIVEN / WHEN - $action = fn (): CouponProcessingCode => new CouponProcessingCode($invalidValue, Country::FRANCE); + $action = fn (): ProcessingCode => new ProcessingCode($invalidValue, Country::FRANCE); // THEN expect($action)->toThrow(\DomainException::class, 'Processing Code must be exactly 13 numeric characters'); @@ -39,8 +39,8 @@ describe('coupon processing code', function (): void { it('is equal to another processing code with the same value', function () use ($value): void { // GIVEN - $code = new CouponProcessingCode($value, Country::FRANCE); - $other = new CouponProcessingCode($value, Country::FRANCE); + $code = new ProcessingCode($value, Country::FRANCE); + $other = new ProcessingCode($value, Country::FRANCE); // WHEN / THEN expect($code->equals($other))->toBeTrue(); @@ -48,8 +48,8 @@ describe('coupon processing code', function (): void { it('ignores the country when comparing', function () use ($value): void { // GIVEN - $france = new CouponProcessingCode($value, Country::FRANCE); - $benelux = new CouponProcessingCode($value, Country::BENELUX); + $france = new ProcessingCode($value, Country::FRANCE); + $benelux = new ProcessingCode($value, Country::BENELUX); // WHEN / THEN expect($france->equals($benelux))->toBeTrue(); @@ -57,8 +57,8 @@ describe('coupon processing code', function (): void { it('is not equal to a processing code with a different value', function () use ($value): void { // GIVEN - $code = new CouponProcessingCode($value, Country::FRANCE); - $other = new CouponProcessingCode('4006381333931', Country::FRANCE); + $code = new ProcessingCode($value, Country::FRANCE); + $other = new ProcessingCode('4006381333931', Country::FRANCE); // WHEN / THEN expect($code->equals($other))->toBeFalse(); @@ -66,7 +66,7 @@ describe('coupon processing code', function (): void { it('is not equal to a value that is not a processing code', function () use ($value): void { // GIVEN - $code = new CouponProcessingCode($value, Country::FRANCE); + $code = new ProcessingCode($value, Country::FRANCE); // WHEN / THEN expect($code->equals($value))->toBeFalse() @@ -75,7 +75,7 @@ describe('coupon processing code', function (): void { it('is converted to its value as a string', function () use ($value): void { // GIVEN - $code = new CouponProcessingCode($value, Country::FRANCE); + $code = new ProcessingCode($value, Country::FRANCE); // WHEN / THEN expect((string) $code)->toBe($value); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductArrayTest.php index 342c6f5..ac43e87 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductArrayTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProductArrayTest.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\Coupon; -use Domain\Models\ValueObjects\Coupon\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponProductArray; +use Domain\Models\ValueObjects\IncoCode; covers(CouponProductArray::class); describe('coupon product array', function (): void { - $firstProduct = new CouponIncoCode(35784); - $secondProduct = new CouponIncoCode(15522); + $firstProduct = new IncoCode(35784); + $secondProduct = new IncoCode(15522); it('is empty by default', function (): void { // GIVEN / WHEN @@ -47,7 +47,7 @@ describe('coupon product array', function (): void { // THEN expect($action)->toThrow( \DomainException::class, - 'Value of CouponProductArray must be CouponIncoCode instances.', + 'Value of CouponProductArray must be IncoCode instances.', ); }); @@ -61,7 +61,7 @@ describe('coupon product array', function (): void { // THEN expect($action)->toThrow( \DomainException::class, - 'Value of CouponProductArray must be CouponIncoCode instances.', + 'Value of CouponProductArray must be IncoCode instances.', ); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIncoCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php similarity index 71% rename from tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIncoCodeTest.php rename to tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php index 3302c49..026260e 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponIncoCodeTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/IncoCodeTest.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; -use Domain\Models\ValueObjects\Coupon\CouponIncoCode; +use Domain\Models\ValueObjects\IncoCode; -covers(CouponIncoCode::class); +covers(IncoCode::class); describe('inco code', function (): void { $value = 1010100101; @@ -14,7 +14,7 @@ describe('inco code', function (): void { it('creates a valid inco code', function () use ($value): void { // GIVEN / WHEN - $incoCode = new CouponIncoCode($value); + $incoCode = new IncoCode($value); // THEN expect($incoCode->value)->toBe($value); @@ -22,7 +22,7 @@ describe('inco code', function (): void { it('rejects invalid inco code', function () use ($zeroValue): void { // GIVEN / WHEN / THEN - expect(fn (): CouponIncoCode => new CouponIncoCode($zeroValue)) + expect(fn (): IncoCode => new IncoCode($zeroValue)) ->toThrow(\DomainException::class); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php b/tests/Unit/Domain/Models/ValueObjects/ProcessingCode.php similarity index 77% rename from tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php rename to tests/Unit/Domain/Models/ValueObjects/ProcessingCode.php index cb83c24..64b7681 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCode.php +++ b/tests/Unit/Domain/Models/ValueObjects/ProcessingCode.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; +use Domain\Models\ValueObjects\ProcessingCode; use Shared\Enums\Country; -covers(CouponProcessingCode::class); +covers(ProcessingCode::class); describe('processing code', function (): void { $value = '5425003944130'; @@ -15,7 +15,7 @@ describe('processing code', function (): void { it('creates a valid processing code FRANCE', function () use ($value): void { // GIVEN / WHEN - $processingCode = new CouponProcessingCode( + $processingCode = new ProcessingCode( $value, Country::FRANCE, ); @@ -26,7 +26,7 @@ describe('processing code', function (): void { it('creates a valid processing code BENELUX', function () use ($value): void { // GIVEN / WHEN - $processingCode = new CouponProcessingCode( + $processingCode = new ProcessingCode( $value, Country::BENELUX, ); @@ -37,7 +37,7 @@ describe('processing code', function (): void { it('rejects invalid processing code', function () use ($emptyValue): void { // GIVEN / WHEN / THEN - expect(fn (): CouponProcessingCode => new CouponProcessingCode( + expect(fn (): ProcessingCode => new ProcessingCode( $emptyValue, Country::FRANCE, ))->toThrow(\DomainException::class); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php b/tests/Unit/Domain/Models/ValueObjects/ProcessingCodeArrayTest.php similarity index 57% rename from tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php rename to tests/Unit/Domain/Models/ValueObjects/ProcessingCodeArrayTest.php index cbeb252..c71da19 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponProcessingCodeArrayTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/ProcessingCodeArrayTest.php @@ -4,22 +4,22 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\Coupon; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; +use Domain\Models\ValueObjects\ProcessingCode; +use Domain\Models\ValueObjects\ProcessingCodeArray; use Shared\Enums\Country; -covers(CouponProcessingCodeArray::class); +covers(ProcessingCodeArray::class); describe('coupon processing code array', function (): void { - $franceCode = new CouponProcessingCode('5425003944130', Country::FRANCE); - $beneluxCode = new CouponProcessingCode('5425003944147', Country::BENELUX); + $franceCode = new ProcessingCode('5425003944130', Country::FRANCE); + $beneluxCode = new ProcessingCode('5425003944147', Country::BENELUX); it('creates an array containing a France processing code', function () use ($franceCode): void { // GIVEN $processingCodes = [$franceCode]; // WHEN - $array = new CouponProcessingCodeArray($processingCodes); + $array = new ProcessingCodeArray($processingCodes); // THEN expect($array->value)->toBe($processingCodes); @@ -30,7 +30,7 @@ describe('coupon processing code array', function (): void { $processingCodes = [$beneluxCode, $franceCode]; // WHEN - $array = new CouponProcessingCodeArray($processingCodes); + $array = new ProcessingCodeArray($processingCodes); // THEN expect($array->value)->toBe($processingCodes); @@ -38,12 +38,12 @@ describe('coupon processing code array', function (): void { it('rejects an empty array', function (): void { // GIVEN / WHEN - $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray([]); + $action = fn (): ProcessingCodeArray => new ProcessingCodeArray([]); // THEN expect($action)->toThrow( \DomainException::class, - 'ProcessingCodes requires at least one CouponProcessingCode.', + 'ProcessingCodes requires at least one ProcessingCode.', ); }); @@ -52,27 +52,27 @@ describe('coupon processing code array', function (): void { // GIVEN $informations = [ $franceCode, - new CouponProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('5425003944130', Country::FRANCE), ]; // WHEN - $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray($informations); + $action = fn (): ProcessingCodeArray => new ProcessingCodeArray($informations); // THEN expect($action)->toThrow( \DomainException::class, - 'Only one CouponProcessingCode is allowed per country, duplicate for FRANCE.', + 'Only one ProcessingCode is allowed per country, duplicate for FRANCE.', ); }); it('rejects a non processing code element', function (): void { // GIVEN / WHEN - $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray(['ABC1234567890']); + $action = fn (): ProcessingCodeArray => new ProcessingCodeArray(['ABC1234567890']); // THEN expect($action)->toThrow( \DomainException::class, - 'Value of CouponProcessingCodeArray must be CouponProcessingCode instances.', + 'Value of ProcessingCodeArray must be ProcessingCode instances.', ); }); @@ -81,12 +81,12 @@ describe('coupon processing code array', function (): void { $processingCodes = [$franceCode, 'ABC1234567890']; // WHEN - $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray($processingCodes); + $action = fn (): ProcessingCodeArray => new ProcessingCodeArray($processingCodes); // THEN expect($action)->toThrow( \DomainException::class, - 'Value of CouponProcessingCodeArray must be CouponProcessingCode instances.', + 'Value of ProcessingCodeArray must be ProcessingCode instances.', ); }); @@ -95,12 +95,12 @@ describe('coupon processing code array', function (): void { $processingCodes = [$beneluxCode]; // WHEN - $action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray($processingCodes); + $action = fn (): ProcessingCodeArray => new ProcessingCodeArray($processingCodes); // THEN expect($action)->toThrow( \DomainException::class, - 'CouponProcessingCodes requires a CouponProcessingCode for France.', + 'ProcessingCodes requires a ProcessingCode for France.', ); }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php index 06368aa..392fa6a 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/RequestedCouponTest.php @@ -5,20 +5,20 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\ValueObjects\RequestedCoupon; use DateTime; -use Domain\Models\ValueObjects\Coupon\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +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\ProcessingCodeArray; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon; use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity; use Shared\Enums\Country; @@ -38,9 +38,9 @@ describe('requested coupon', function (): void { new PicturePath('files/mobileThumbnail.jpg') ); - $franceProcessingCode = new CouponProcessingCode('5425003944130', Country::FRANCE); - $beneluxProcessingCode = new CouponProcessingCode('5425003944147', Country::BENELUX); - $couponProcessingCodes = new CouponProcessingCodeArray([ + $franceProcessingCode = new ProcessingCode('5425003944130', Country::FRANCE); + $beneluxProcessingCode = new ProcessingCode('5425003944147', Country::BENELUX); + $couponProcessingCodes = new ProcessingCodeArray([ $franceProcessingCode, $beneluxProcessingCode, ]); @@ -72,8 +72,8 @@ describe('requested coupon', function (): void { ]); $couponProducts = new CouponProductArray([ - new CouponIncoCode(35784), - new CouponIncoCode(15522), + new IncoCode(35784), + new IncoCode(15522), ]); $printRules = new PrintRules(maxPerCustomer: 3); @@ -193,7 +193,7 @@ describe('requested coupon', function (): void { diffusion: $diffusion, moneyAmount: $moneyAmount, couponPicture: $couponPicture, - couponProcessingCodes: new CouponProcessingCodeArray([ + couponProcessingCodes: new ProcessingCodeArray([ $franceProcessingCode, ]), couponValidityInformations: new CouponValidityInformationArray([ @@ -227,13 +227,13 @@ describe('requested coupon', function (): void { diffusion: $diffusion, moneyAmount: $moneyAmount, couponPicture: $couponPicture, - couponProcessingCodes: new CouponProcessingCodeArray([ + couponProcessingCodes: new ProcessingCodeArray([ $beneluxProcessingCode, ]), couponValidityInformations: $couponValidityInformations, couponLegalInformations: $couponLegalInformations, couponProducts: $couponProducts, - ))->toThrow(\DomainException::class, 'CouponProcessingCodes requires a CouponProcessingCode for France.'); + ))->toThrow(\DomainException::class, 'ProcessingCodes requires a ProcessingCode for France.'); }); it('throws when a benelux processing code has no benelux validity information', function () use ( From b1b7857ddc597386ce222190a3b0706974bca810 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 13 Oct 2026 16:17:02 +0200 Subject: [PATCH 09/21] refactor: :recycle: return brand code instead of brand in coupon details --- .../UseCases/Coupon/Find/FindCoupon.php | 7 ++----- .../UseCases/Coupon/Find/Output/CouponFound.php | 4 ++-- .../UseCases/Coupon/Find/FindCouponTest.php | 14 +++++++------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Application/UseCases/Coupon/Find/FindCoupon.php b/src/Application/UseCases/Coupon/Find/FindCoupon.php index 44683f8..169063a 100644 --- a/src/Application/UseCases/Coupon/Find/FindCoupon.php +++ b/src/Application/UseCases/Coupon/Find/FindCoupon.php @@ -36,10 +36,7 @@ final readonly class FindCoupon implements UseCase if (! $coupon instanceof Coupon) { return new CouponNotFound(); } - - $brand = $this->brandRepository->findByCode($coupon->brandCode); - - if (! $brand instanceof Brand) { + if (! $this->brandRepository->existsByCode($coupon->brandCode)) { throw new \DomainException(sprintf('Brand code "%s" was not found.', $coupon->brandCode->value)); } @@ -55,7 +52,7 @@ final readonly class FindCoupon implements UseCase validityInformations: $coupon->validityInformations, legalInformations: $coupon->legalInformations, processingCode: $coupon->processingCode, - brand: $brand, + brandCode: $coupon->brandCode, createdBy: $coupon->createdBy, products: $coupon->products, movements: $coupon->movements, diff --git a/src/Application/UseCases/Coupon/Find/Output/CouponFound.php b/src/Application/UseCases/Coupon/Find/Output/CouponFound.php index 2147114..e8e6afb 100644 --- a/src/Application/UseCases/Coupon/Find/Output/CouponFound.php +++ b/src/Application/UseCases/Coupon/Find/Output/CouponFound.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Application\UseCases\Coupon\Find\Output; -use Domain\Models\Entities\Brand; use Domain\Models\Entities\Campaign; 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; @@ -36,7 +36,7 @@ final readonly class CouponFound implements Output public CouponValidityInformationArray $validityInformations, public CouponLegalInformationArray $legalInformations, public ProcessingCodeArray $processingCode, - public Brand $brand, + public BrandCode $brandCode, public ActorId $createdBy, public CouponProductArray $products, public CouponMovementArray $movements, diff --git a/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php index 305fd76..215f2d6 100644 --- a/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php @@ -109,10 +109,10 @@ describe('find coupon use case', function (): void { actorId: ActorId::generate(), ); - it('finds a coupon by its code', function () use ($brand, $coupon, $campaign): void { + it('finds a coupon by its code', function () use ($coupon, $campaign): void { // GIVEN $brandRepository = \Mockery::mock(BrandRepository::class); - $brandRepository->shouldReceive('findByCode')->once()->with($coupon->brandCode)->andReturn($brand); + $brandRepository->shouldReceive('existsByCode')->once()->with($coupon->brandCode)->andReturnTrue(); $couponRepository = \Mockery::mock(CouponRepository::class); $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); @@ -136,7 +136,7 @@ describe('find coupon use case', function (): void { ->and($result->validityInformations)->toBe($coupon->validityInformations) ->and($result->legalInformations)->toBe($coupon->legalInformations) ->and($result->processingCode)->toBe($coupon->processingCode) - ->and($result->brand)->toBe($brand) + ->and($result->brandCode)->toBe($coupon->brandCode) ->and($result->createdBy)->toBe($coupon->createdBy) ->and($result->products)->toBe($coupon->products) ->and($result->movements)->toBe($coupon->movements) @@ -154,7 +154,7 @@ describe('find coupon use case', function (): void { $couponRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(null); $brandRepository = \Mockery::mock(BrandRepository::class); - $brandRepository->shouldNotReceive('findByCode'); + $brandRepository->shouldNotReceive('existsByCode'); $campaignRepository = \Mockery::mock(CampaignRepository::class); $campaignRepository->shouldNotReceive('findByCouponCode'); @@ -174,7 +174,7 @@ describe('find coupon use case', function (): void { $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); $brandRepository = \Mockery::mock(BrandRepository::class); - $brandRepository->shouldReceive('findByCode')->once()->with($coupon->brandCode)->andReturn(null); + $brandRepository->shouldReceive('existsByCode')->once()->with($coupon->brandCode)->andReturnFalse(); $campaignRepository = \Mockery::mock(CampaignRepository::class); $campaignRepository->shouldNotReceive('findByCouponCode'); @@ -186,13 +186,13 @@ describe('find coupon use case', function (): void { ->toThrow(\DomainException::class); }); - it('finds a coupon not attached to any campaign', function () use ($brand, $coupon): void { + it('finds a coupon not attached to any campaign', function () use ($coupon): void { // GIVEN $couponRepository = \Mockery::mock(CouponRepository::class); $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); $brandRepository = \Mockery::mock(BrandRepository::class); - $brandRepository->shouldReceive('findByCode')->once()->with($coupon->brandCode)->andReturn($brand); + $brandRepository->shouldReceive('existsByCode')->once()->with($coupon->brandCode)->andReturnTrue(); $campaignRepository = \Mockery::mock(CampaignRepository::class); $campaignRepository->shouldReceive('findByCouponCode')->once()->with($coupon->code)->andReturn([]); From 6d932429c414ed88c97b8040318f2366bdedd28a Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 13 Oct 2026 16:25:13 +0200 Subject: [PATCH 10/21] chore: :rotating_light: resolve pint error --- src/Application/UseCases/Coupon/Find/FindCoupon.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Application/UseCases/Coupon/Find/FindCoupon.php b/src/Application/UseCases/Coupon/Find/FindCoupon.php index 169063a..9db5b36 100644 --- a/src/Application/UseCases/Coupon/Find/FindCoupon.php +++ b/src/Application/UseCases/Coupon/Find/FindCoupon.php @@ -7,7 +7,6 @@ namespace Application\UseCases\Coupon\Find; use Application\UseCases\Coupon\Find\Output\Output; use Application\UseCases\Coupon\Find\Output\CouponFound; use Application\UseCases\Coupon\Find\Output\CouponNotFound; -use Domain\Models\Entities\Brand; use Domain\Models\Entities\Coupon; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CampaignRepository; From 8d974b5161875b598182f9c7ae86121035c60bd9 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 13 Oct 2026 12:12:18 +0200 Subject: [PATCH 11/21] feat: :sparkles: add use case to update coupon pictures --- .../UseCases/Coupon/Update/Input.php | 23 +++ .../Coupon/Update/Output/CouponNotFound.php | 9 + .../Coupon/Update/Output/CouponUpdated.php | 43 +++++ .../UseCases/Coupon/Update/Output/Output.php | 11 ++ .../UseCases/Coupon/Update/UpdateCoupon.php | 62 +++++++ src/Domain/Models/Entities/Coupon.php | 23 +++ .../ValueObjects/Coupon/CouponPicture.php | 40 +++++ tests/Fixtures/Coupon/decoration.jpg | Bin 0 -> 2142 bytes tests/Fixtures/Coupon/invalid_decoration.jpg | Bin 0 -> 2354 bytes .../Coupon/Update/UpdateCouponTest.php | 155 ++++++++++++++++++ .../ValueObjects/Coupon/CouponPictureTest.php | 85 ++++++++++ 11 files changed, 451 insertions(+) create mode 100644 src/Application/UseCases/Coupon/Update/Input.php create mode 100644 src/Application/UseCases/Coupon/Update/Output/CouponNotFound.php create mode 100644 src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php create mode 100644 src/Application/UseCases/Coupon/Update/Output/Output.php create mode 100644 src/Application/UseCases/Coupon/Update/UpdateCoupon.php create mode 100644 tests/Fixtures/Coupon/decoration.jpg create mode 100644 tests/Fixtures/Coupon/invalid_decoration.jpg create mode 100644 tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponPictureTest.php diff --git a/src/Application/UseCases/Coupon/Update/Input.php b/src/Application/UseCases/Coupon/Update/Input.php new file mode 100644 index 0000000..fc8f41b --- /dev/null +++ b/src/Application/UseCases/Coupon/Update/Input.php @@ -0,0 +1,23 @@ +picture instanceof CouponPicture, + 'At least one field must be provided to update the coupon.', + ); + } +} diff --git a/src/Application/UseCases/Coupon/Update/Output/CouponNotFound.php b/src/Application/UseCases/Coupon/Update/Output/CouponNotFound.php new file mode 100644 index 0000000..aeae2df --- /dev/null +++ b/src/Application/UseCases/Coupon/Update/Output/CouponNotFound.php @@ -0,0 +1,9 @@ + */ +final readonly class UpdateCoupon implements UseCase, Transactional +{ + public function __construct( + private CouponRepository $repository, + ) { + } + + #[\Override] + public function execute(BaseInput $input): Output + { + Assert::isInstanceOf($input, Input::class); + + $coupon = $this->repository->findByCode($input->code); + + if (! $coupon instanceof Coupon) { + return new CouponNotFound(); + } + + if ($input->picture instanceof CouponPicture) { + $coupon = $coupon->changePicture($input->picture); + } + + $this->repository->save($coupon); + + return new CouponUpdated( + code: $coupon->code, + name: $coupon->name, + amount: $coupon->amount, + picture: $coupon->picture, + diffusion: $coupon->diffusion, + availabilityCalendar: $coupon->availabilityCalendar, + validityInformations: $coupon->validityInformations, + legalInformations: $coupon->legalInformations, + processingCode: $coupon->processingCode, + brandCode: $coupon->brandCode, + createdBy: $coupon->createdBy, + products: $coupon->products, + movements: $coupon->movements, + quantity: $coupon->quantity, + createdAt: $coupon->createdAt(), + updatedAt: $coupon->updatedAt(), + ); + } +} diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index 4995461..f7d8fc9 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -22,6 +22,7 @@ use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\ProcessingCodeArray; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; +use Shared\Helpers\DatesHelper; /** * @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Coupon\CouponId> @@ -142,4 +143,26 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier updatedAt: $updatedAt, ); } + + public function changePicture(CouponPicture $picture): self + { + return new self( + id: $this->id, + code: $this->code, + name: $this->name, + amount: $this->amount, + picture: $picture, + diffusion: $this->diffusion, + validityInformations: $this->validityInformations, + legalInformations: $this->legalInformations, + processingCode: $this->processingCode, + brandCode: $this->brandCode, + createdBy: $this->createdBy, + products: $this->products, + movements: $this->movements, + quantity: $this->quantity, + availabilityCalendar: $this->availabilityCalendar, + updatedAt: DatesHelper::now(), + ); + } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponPicture.php b/src/Domain/Models/ValueObjects/Coupon/CouponPicture.php index 7d84384..21f5e61 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponPicture.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponPicture.php @@ -8,10 +8,50 @@ use Domain\Models\ValueObjects\PicturePath; final readonly class CouponPicture { + private const array ALLOWED_EXTENSIONS = ['jpg', 'jpeg']; + + public const int DECORATION_WIDTH = 280; + public const int DECORATION_HEIGHT = 260; + public function __construct( public PicturePath $decoration, public PicturePath $thumbnail, public PicturePath $mobileThumbnail, ) { + foreach ([$decoration, $thumbnail, $mobileThumbnail] as $picture) { + $extension = strtolower(pathinfo($picture->value, PATHINFO_EXTENSION)); + + if (! in_array($extension, self::ALLOWED_EXTENSIONS, true)) { + throw new \DomainException(sprintf('Coupon picture "%s" must be a JPG file.', $picture)); + } + } + } + + public static function create( + PicturePath $decoration, + PicturePath $thumbnail, + PicturePath $mobileThumbnail, + ): self { + $picture = new self($decoration, $thumbnail, $mobileThumbnail); + + $size = is_file($decoration->value) ? getimagesize($decoration->value) : false; + + if ($size === false) { + throw new \DomainException(sprintf('Unable to read the decoration picture "%s".', $decoration)); + } + + [$width, $height] = $size; + + if ($width !== self::DECORATION_WIDTH || $height !== self::DECORATION_HEIGHT) { + throw new \DomainException(sprintf( + 'The decoration picture must be %d x %d px, %d x %d px given.', + self::DECORATION_WIDTH, + self::DECORATION_HEIGHT, + $width, + $height, + )); + } + + return $picture; } } diff --git a/tests/Fixtures/Coupon/decoration.jpg b/tests/Fixtures/Coupon/decoration.jpg new file mode 100644 index 0000000000000000000000000000000000000000..271a50f327b838e6ef8cfb22c055e77f5a7aa8c2 GIT binary patch literal 2142 zcmds0dsNzo75*WL3A0I(FqDUI6oOPzSN_7pLR>`7)#F&6Ovsb*QI0N;h2s(>t_q1ptpnn$527fj)Dab-ktiafAP6&EPuqX@=l1<|&wYINp6}jk zpRp@|Ujqvg6(0{vd{1d6uP1bj-sv29CmUoKk|AA~^{7i%s zjp^3y0M}-5AjO}y)BV*g24vqG|>(qTDcX!LtkFK zdSGq3sH|&dC7{3JH`@lRQe{{)i<@uBIyf>#94$Xs(!Y!OEyZyDfp_X~!vv02rSOBL!(s3)Vxx$Z}E6FE0g zy0IwVzRSrGESc)@n6W)hZf?s@N?Thc((Y}i)Y=Y2;Ly+Tyssx-`Q0C__+SQvmu$p( z=SD6IFCRN=V21UYu~OS91X|4JMk#Q$j^3}KmZOM~msDw&t z4SBOZ(c&9E$U6hy;-5$h%KdZ*>621!9pGm5idnZi(~gFoY|B{-^Igv##vSL~47=O) zh<>p(2hWI=SXeF09fM+I%MR!+@!SgVz1`kL{14^sxw53W`2>juljtPz7$>Rt?X`r= z2v$FTIPY!%{I-EKSZOXigQ)BLX0H5}^{vlPMp9-{{5yhr!dR|rOzYpcLxh-)u-r?M zkBgfX2JN!#B|DhbtTWSsHUCB-Cp8!Oh&pctRP?Kj{mvtRH`hAz}Nvq>eOwzv3;Ssg+Ep7nnk? zM3aeKqLXBd7`e8;Fq0&*{*fex58bILZBE(Q)qFd3o^$$wSMgACXVy@Zdp+}ZTs)~F z>C6+Wi))UJT<{O4CDYI^#2i z_Y+2UG3re8=7xz(hm3b?8`x`@Jz>g8wzE2Hg#W@fPpRjo_!N&83$GlvODsVLf1&ps z_mrp3MB@K7Z}hRMFDPFh?2mdzNvoTEgu{m;8f{YBYGwP~of%%{tu|M`iS*HRy+A~g z36V!46Uq>+%zGW&wfj=_yW!}JXhY82Hg|n8*VPAWxgtTB`lE#zC1V9d-R8>q?()H;Xp;P(R?Lk z06DZiJCMy|A&HImu>$TrmC3Nu|HrerUh=4p( zg)mni(U3&Gc4un%pQgg_(=js6&X$0RRN^t8w&r(T8LB@1HT4K>>$8Y5YK*qS(llBg zq!B+2>r6&xPsqiCO6{vCTU(+0hW@B5q<_C$#(gF-`lYWi3;HySs`*9zOL^6`#(;Q_ zip07er;gB*`cu>9jLyjCI%dRmy*RGyDb)_-RI}TY$*d*g!#4H@^5MjdNm;eIw0yqh i$RabdIGq zvS#b}D45CA8coJ{_|_aXwtS`d1hY{R1bl)bV#wO=o|*pLKX<=>-E$wGd(XM|>LzqT zz#o9<8$b;7ArJ!y6bjj8w8zNE(9p=#WcMd~%uM(0H8VvbEiCP&M z;P_`pb8GYwH0p>Q$`SPe0)j%JM!SsQMn-Uy1=0fbUrYBKfb9b5gARc~Hh>-s1crfh z-M~Ho1b{(10)7`*A7Y>fg6;${dq9952&|{C2Zb0I{3`*Z2i7-$LH_iq%ZV`fMVr$p z-xmJVX^OBtT0eyREd1`u!M_&GtnQSWeoXu~`hyqtQJ?>K1;8iZoi;Eq4DbWAE5DEN zzi)s<9@q;#l%^yE@1&UT`!`i(YkXYFYhkE)wJA0hAKR+r{pe|~VLAu1&O?gY>*Yjz zoTQtCUeE@G;2$B3c{qfV)E_|hSXXC{%o}zePp&hn433T*k48c zbTKI7^?>LnZrRZR6uZ0sAx5zV1&582zQLExyFFN^;Zj}wd~IJQ#>=n?>j=KAp0b}y zcZz2iGLnm`SxJqm{(~1EZA{0Nzb&vIez|@wQtOYER;XE~xgxo8X7j4CAV=90D|BO+ z9n6T1%L}I;uS^arE2Eh8mJOZT*-(^sb6GNuJvSz2L^i9e4<0)<M!dE?-KJy%O#o;$fVKwld*>9|p4U$4i3#A)}fkT@$+V``hgt zTAn#l}W1XK=>LE7anp+}o0&Vu@cck@G0J>FTr%e=W284C7UJ()l1Nvdm4; zXVzA)-Am7;`&8Lshn~pJ3mWc(2P0-#zW12IB}0waPtG2v&2gP=vNyLEi_3Q$bhc|`Z_;&C9cHVdYb}UF;OrACx3V8TXmpBaZ-rV#&@#enRIO!wp6g!Z z{%5lV=qM#8J0?zM3^(r30cJIhUu6zvdug|=-^2xurrruXNB-W2NiP(>LC&Ii9V|)6 ze5$B38Ik3m6tBzE$AOXl4nY$T?%bmcbr>*;IiDR%13S)th-T#16g z^*ND0m9o7y<&NRDxwj*1o0Tz2ih6U)?TuZ{@a5NMAoRj~4^Q#hI$N8}n3pAK4tW{7 zUnR(UE)7tVd=(YceCRTTyl{r=n7hProIx@+#U_oImhHJMmj7yE^uiBxLBM9}@`3!v z$L?q0tpq952+{nwP~w~syl9Rt-zc8xn`_-T%~h`I05=vnBt5Z?%f#iSpT#WQ<}?;i zC0&Y)>n}WXz%$zW4vhi5X#`v5<7+gC=p~d>)S@osdxi(Gd^qMrzH=SA$Qj%79_@n2 zRVZ+>^Trz?Ok{`;sW)^svYiFDa#3w&n?wpnbU?k-v1(MRI29Z=3AeI>&Gnr`Es?X* zwv~AroPf|B-8xw9>(1yrd`4@zjQn!@EDskr*;gXnIh5%pW&xp!33HWVdDXrw=kdtJ zj4|nx^3dpo36i*8lJLI$dA#ilOwPWa{qjfHm6W74r;R}!5b0(a;YGE)DoazfUr7s04c!thE01pBxvK^2jp07aHT@=(UAWKCR8{1PihdXdjJ3c literal 0 HcmV?d00001 diff --git a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php new file mode 100644 index 0000000..5792109 --- /dev/null +++ b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php @@ -0,0 +1,155 @@ + Coupon::hydrate( + id: CouponId::generate(), + code: new CouponCode('COU01'), + name: new CouponName('Coupon name'), + amount: new MoneyAmount(new Money(Currency::EUR, 100)), + picture: new CouponPicture( + new PicturePath('files/decoration-old.jpg'), + new PicturePath('files/thumbnail-old.jpg'), + new PicturePath('files/mobile-thumbnail-old.jpg'), + ), + diffusion: new CouponDiffusion( + new Period( + new DateTime('2026-01-01'), + new DateTime('2026-02-01'), + ), + ), + validityInformations: new CouponValidityInformationArray([ + new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE, + ), + ]), + legalInformations: new CouponLegalInformationArray([ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE, + ), + ]), + processingCode: new CouponProcessingCodeArray([ + new CouponProcessingCode('5425003944130', Country::FRANCE), + ]), + brandCode: new BrandCode('AAA01'), + createdBy: ActorId::generate(), + products: new CouponProductArray([ + new CouponIncoCode(35784), + new CouponIncoCode(15522), + ]), + movements: new CouponMovementArray([ + new CouponMovement(1000), + ]), + quantity: new CouponQuantity(1000), + ); + + it('updates the picture and keeps the other fields', function () use ($makeCoupon): void { + // GIVEN + $coupon = $makeCoupon(); + $newPicture = new CouponPicture( + new PicturePath('files/decoration-new.jpg'), + new PicturePath('files/thumbnail-new.jpg'), + new PicturePath('files/mobile-thumbnail-new.jpg'), + ); + $input = new Input(code: $coupon->code, picture: $newPicture); + + $repository = \Mockery::mock(CouponRepository::class); + $repository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + $repository->shouldReceive('save')->once()->with(\Mockery::on( + fn (Coupon $saved): bool => $saved->picture === $newPicture + && $saved->id === $coupon->id + && $saved->code === $coupon->code + && $saved->name === $coupon->name + && $saved->amount === $coupon->amount + && $saved->diffusion === $coupon->diffusion + && $saved->quantity === $coupon->quantity + && $saved->updatedAt instanceof \DateTimeInterface, + )); + + $useCase = new UpdateCoupon($repository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(CouponUpdated::class) + ->and($result->code)->toBe($coupon->code) + ->and($result->picture)->toBe($newPicture) + ->and($result->updatedAt)->not->toBeNull(); + }); + + it('returns CouponNotFound when the coupon does not exist', function (): void { + // GIVEN + $code = new CouponCode('COU01'); + $input = new Input( + code: $code, + picture: new CouponPicture( + new PicturePath('files/decoration-new.jpg'), + new PicturePath('files/thumbnail-new.jpg'), + new PicturePath('files/mobile-thumbnail-new.jpg'), + ), + ); + + $repository = \Mockery::mock(CouponRepository::class); + $repository->shouldReceive('findByCode')->once()->with($code)->andReturn(null); + $repository->shouldNotReceive('save'); + + $useCase = new UpdateCoupon($repository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(CouponNotFound::class); + }); + + it('rejects an input with nothing to update', function (): void { + // GIVEN + $code = new CouponCode('COU01'); + + // WHEN + $action = fn (): Input => new Input(code: $code); + + // THEN + expect($action)->toThrow(\InvalidArgumentException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponPictureTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponPictureTest.php new file mode 100644 index 0000000..c8e2903 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponPictureTest.php @@ -0,0 +1,85 @@ +decoration)->toBe($decoration) + ->and($picture->thumbnail)->toBe($thumbnail) + ->and($picture->mobileThumbnail)->toBe($mobileThumbnail); + })->with(['jpg', 'jpeg']); + + it('refuses files that are not jpg', function (string $decoration, string $thumbnail, string $mobileThumbnail, string $invalid): void { + // GIVEN + $decorationPath = new PicturePath($decoration); + $thumbnailPath = new PicturePath($thumbnail); + $mobileThumbnailPath = new PicturePath($mobileThumbnail); + + // WHEN + $build = fn (): CouponPicture => new CouponPicture($decorationPath, $thumbnailPath, $mobileThumbnailPath); + + // THEN + expect($build)->toThrow(\DomainException::class, sprintf('Coupon picture "%s" must be a JPG file.', $invalid)); + })->with([ + 'decoration' => ['files/decoration.png', 'files/thumbnail.jpg', 'files/mobile_thumbnail.jpg', 'files/decoration.png'], + 'thumbnail' => ['files/decoration.jpg', 'files/thumbnail.webp', 'files/mobile_thumbnail.jpg', 'files/thumbnail.webp'], + 'mobile thumbnail' => ['files/decoration.jpg', 'files/thumbnail.jpg', 'files/mobile_thumbnail.png', 'files/mobile_thumbnail.png'], + ]); + + it('creates a picture when the decoration is 280 x 260', function (): void { + // GIVEN + $decoration = new PicturePath('tests/Fixtures/Coupon/decoration.jpg'); + $thumbnail = new PicturePath('files/thumbnail.jpg'); + $mobileThumbnail = new PicturePath('files/mobile_thumbnail.jpg'); + + // WHEN + $picture = CouponPicture::create($decoration, $thumbnail, $mobileThumbnail); + + // THEN + expect($picture->decoration)->toBe($decoration) + ->and($picture->thumbnail)->toBe($thumbnail) + ->and($picture->mobileThumbnail)->toBe($mobileThumbnail); + }); + + it('refuses a decoration with the wrong size', function (): void { + // GIVEN + $decoration = new PicturePath('tests/Fixtures/Coupon/invalid_decoration.jpg'); + $thumbnail = new PicturePath('files/thumbnail.jpg'); + $mobileThumbnail = new PicturePath('files/mobile_thumbnail.jpg'); + + // WHEN + $create = fn (): CouponPicture => CouponPicture::create($decoration, $thumbnail, $mobileThumbnail); + + // THEN + expect($create)->toThrow(\DomainException::class, 'The decoration picture must be 280 x 260 px'); + }); + + it('refuses a decoration file that does not exist', function (): void { + // GIVEN + $decoration = new PicturePath('files/missing.jpg'); + $thumbnail = new PicturePath('files/thumbnail.jpg'); + $mobileThumbnail = new PicturePath('files/mobile_thumbnail.jpg'); + + // WHEN + $create = fn (): CouponPicture => CouponPicture::create($decoration, $thumbnail, $mobileThumbnail); + + // THEN + expect($create)->toThrow(\DomainException::class, 'Unable to read the decoration picture "files/missing.jpg".'); + }); +}); From 620e4ef44c489d619c8b05f0979217f4c9cda1ee Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 14 Oct 2026 09:36:42 +0200 Subject: [PATCH 12/21] chore: :rotating_light: resolve linters error --- .../Coupon/Update/Output/CouponUpdated.php | 4 ++-- .../UseCases/Coupon/Update/UpdateCouponTest.php | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php b/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php index d9c88b3..d2204b5 100644 --- a/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php +++ b/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php @@ -13,11 +13,11 @@ use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; +use Domain\Models\ValueObjects\ProcessingCodeArray; final readonly class CouponUpdated implements Output { @@ -30,7 +30,7 @@ final readonly class CouponUpdated implements Output public ?CouponAvailabilityCalendar $availabilityCalendar, public CouponValidityInformationArray $validityInformations, public CouponLegalInformationArray $legalInformations, - public CouponProcessingCodeArray $processingCode, + public ProcessingCodeArray $processingCode, public BrandCode $brandCode, public ActorId $createdBy, public CouponProductArray $products, diff --git a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php index 5792109..0ef1d32 100644 --- a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php @@ -15,22 +15,22 @@ use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; use Domain\Models\ValueObjects\Coupon\CouponId; -use Domain\Models\ValueObjects\Coupon\CouponIncoCode; use Domain\Models\ValueObjects\Coupon\CouponLegalInformation; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponMovement; use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCode; -use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray; use Domain\Models\ValueObjects\Coupon\CouponProductArray; use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; +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 Domain\Models\ValueObjects\ProcessingCodeArray; use Domain\Ports\Repositories\CouponRepository; use Shared\Enums\Country; use Shared\Enums\Currency; @@ -67,14 +67,14 @@ describe('update coupon use case', function (): void { Country::FRANCE, ), ]), - processingCode: new CouponProcessingCodeArray([ - new CouponProcessingCode('5425003944130', Country::FRANCE), + processingCode: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), ]), brandCode: new BrandCode('AAA01'), createdBy: ActorId::generate(), products: new CouponProductArray([ - new CouponIncoCode(35784), - new CouponIncoCode(15522), + new IncoCode(35784), + new IncoCode(15522), ]), movements: new CouponMovementArray([ new CouponMovement(1000), From 5bb806399934ace279a35a775c19e4e7565ef40e Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 13 Oct 2026 17:30:01 +0200 Subject: [PATCH 13/21] feat: :sparkles: update legal and validity information of a coupon --- .../UseCases/Coupon/Update/Input.php | 8 +++- .../UseCases/Coupon/Update/UpdateCoupon.php | 12 ++++++ src/Domain/Models/Entities/Coupon.php | 22 ++++++++++ .../Coupon/CouponCountryCoverage.php | 8 ++++ .../Coupon/Update/UpdateCouponTest.php | 40 +++++++++++++++++++ .../Coupon/CouponCountryCoverageTest.php | 34 ++++++++++++++++ 6 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/Application/UseCases/Coupon/Update/Input.php b/src/Application/UseCases/Coupon/Update/Input.php index fc8f41b..421b17a 100644 --- a/src/Application/UseCases/Coupon/Update/Input.php +++ b/src/Application/UseCases/Coupon/Update/Input.php @@ -5,7 +5,9 @@ declare(strict_types=1); namespace Application\UseCases\Coupon\Update; use Domain\Models\ValueObjects\Coupon\CouponCode; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Shared\DDD\UseCases\Input as UseCaseInput; use Webmozart\Assert\Assert; @@ -14,9 +16,13 @@ final readonly class Input implements UseCaseInput public function __construct( public CouponCode $code, public ?CouponPicture $picture = null, + public ?CouponValidityInformationArray $validityInformations = null, + public ?CouponLegalInformationArray $legalInformations = null, ) { Assert::true( - $this->picture instanceof CouponPicture, + $this->picture instanceof CouponPicture + || $this->validityInformations instanceof CouponValidityInformationArray + || $this->legalInformations instanceof CouponLegalInformationArray, 'At least one field must be provided to update the coupon.', ); } diff --git a/src/Application/UseCases/Coupon/Update/UpdateCoupon.php b/src/Application/UseCases/Coupon/Update/UpdateCoupon.php index a3b50ed..6d44e0b 100644 --- a/src/Application/UseCases/Coupon/Update/UpdateCoupon.php +++ b/src/Application/UseCases/Coupon/Update/UpdateCoupon.php @@ -8,7 +8,9 @@ use Application\UseCases\Coupon\Update\Output\CouponNotFound; use Application\UseCases\Coupon\Update\Output\CouponUpdated; use Application\UseCases\Coupon\Update\Output\Output; use Domain\Models\Entities\Coupon; +use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; use Domain\Models\ValueObjects\Coupon\CouponPicture; +use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Ports\Repositories\CouponRepository; use Shared\DDD\UseCases\Input as BaseInput; use Shared\DDD\UseCases\Transactional; @@ -38,6 +40,16 @@ final readonly class UpdateCoupon implements UseCase, Transactional $coupon = $coupon->changePicture($input->picture); } + if ( + $input->validityInformations instanceof CouponValidityInformationArray + || $input->legalInformations instanceof CouponLegalInformationArray + ) { + $coupon = $coupon->changeInformations( + $input->validityInformations ?? $coupon->validityInformations, + $input->legalInformations ?? $coupon->legalInformations, + ); + } + $this->repository->save($coupon); return new CouponUpdated( diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index f7d8fc9..6648fb4 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -165,4 +165,26 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier updatedAt: DatesHelper::now(), ); } + + public function changeInformations(CouponValidityInformationArray $validityInformations, CouponLegalInformationArray $legalInformations): self + { + return new self( + id: $this->id, + code: $this->code, + name: $this->name, + amount: $this->amount, + picture: $this->picture, + diffusion: $this->diffusion, + validityInformations: $validityInformations, + legalInformations: $legalInformations, + processingCode: $this->processingCode, + brandCode: $this->brandCode, + createdBy: $this->createdBy, + products: $this->products, + movements: $this->movements, + quantity: $this->quantity, + availabilityCalendar: $this->availabilityCalendar, + updatedAt: DatesHelper::now(), + ); + } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php index b00aca7..f336808 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php @@ -29,6 +29,14 @@ final readonly class CouponCountryCoverage throw new \DomainException(sprintf('A coupon requires legal information for %s.', $country->name)); } } + + if (count($validityInformations->value) !== count($processingCodes->value)) { + throw new \DomainException('A coupon requires exactly one validity information per diffusion country.'); + } + + if (count($legalInformations->value) !== count($processingCodes->value)) { + throw new \DomainException('A coupon requires exactly one legal information per diffusion country.'); + } } private static function hasInformationFor( diff --git a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php index 0ef1d32..e37cdca 100644 --- a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php @@ -142,6 +142,46 @@ describe('update coupon use case', function (): void { expect($result)->toBeInstanceOf(CouponNotFound::class); }); + it('updates the legal and validity information and keeps the other fields', function () use ($makeCoupon): void { + // GIVEN + $coupon = $makeCoupon(); + $newValidityInformations = new CouponValidityInformationArray([ + new CouponValidityInformation('New validity information.', Country::FRANCE), + ]); + $newLegalInformations = new CouponLegalInformationArray([ + new CouponLegalInformation('New legal information.', Country::FRANCE), + ]); + $input = new Input( + code: $coupon->code, + validityInformations: $newValidityInformations, + legalInformations: $newLegalInformations, + ); + + $repository = \Mockery::mock(CouponRepository::class); + $repository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + $repository->shouldReceive('save')->once()->with(\Mockery::on( + fn (Coupon $saved): bool => $saved->validityInformations === $newValidityInformations + && $saved->legalInformations === $newLegalInformations + && $saved->id === $coupon->id + && $saved->name === $coupon->name + && $saved->amount === $coupon->amount + && $saved->picture === $coupon->picture + && $saved->diffusion === $coupon->diffusion + && $saved->movements === $coupon->movements + && $saved->updatedAt instanceof \DateTimeInterface, + )); + + $useCase = new UpdateCoupon($repository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(CouponUpdated::class) + ->and($result->validityInformations)->toBe($newValidityInformations) + ->and($result->legalInformations)->toBe($newLegalInformations); + }); + it('rejects an input with nothing to update', function (): void { // GIVEN $code = new CouponCode('COU01'); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php index 1a7bcf0..d6dbe67 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php @@ -90,6 +90,40 @@ describe('coupon country coverage', function (): void { expect($action)->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.'); }); + it('rejects validity information for a country where the coupon is not diffused', function () use ( + $franceCode, + $franceValidity, + $beneluxValidity, + $franceLegal, + ): void { + // GIVEN / WHEN + $action = fn () => CouponCountryCoverage::assert( + new ProcessingCodeArray([$franceCode]), + new CouponValidityInformationArray([$franceValidity, $beneluxValidity]), + new CouponLegalInformationArray([$franceLegal]), + ); + + // THEN + expect($action)->toThrow(\DomainException::class, 'A coupon requires exactly one validity information per diffusion country.'); + }); + + it('rejects legal information for a country where the coupon is not diffused', function () use ( + $franceCode, + $franceValidity, + $franceLegal, + $beneluxLegal, + ): void { + // GIVEN / WHEN + $action = fn () => CouponCountryCoverage::assert( + new ProcessingCodeArray([$franceCode]), + new CouponValidityInformationArray([$franceValidity]), + new CouponLegalInformationArray([$franceLegal, $beneluxLegal]), + ); + + // THEN + expect($action)->toThrow(\DomainException::class, 'A coupon requires exactly one legal information per diffusion country.'); + }); + it('cannot be instantiated from the outside', function (): void { // GIVEN $reflection = new \ReflectionClass(CouponCountryCoverage::class); From 44e3a646a337c5a2515e84797e1fac6f31c9b646 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Tue, 13 Oct 2026 17:33:16 +0200 Subject: [PATCH 14/21] fix: :bug: correct imports and use ean13 processing codes --- .../CampaignRequest/Create/CreateCampaignRequestTest.php | 1 + .../UseCases/CampaignRequest/Find/FindCampaignRequestTest.php | 1 + .../UseCases/CampaignRequest/List/ListCampaignsRequestTest.php | 2 ++ .../CampaignRequest/Reject/RejectCampaignRequestTest.php | 1 + tests/Unit/Domain/Models/Entities/CampaignRequestTest.php | 1 + 5 files changed, 6 insertions(+) diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php index 60e8429..4bb30ed 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Create/CreateCampaignRequestTest.php @@ -57,6 +57,7 @@ describe('create campaign request use case', function (): void { ), couponProcessingCodes: new ProcessingCodeArray([ new ProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('4006381333931', Country::BENELUX), ]), couponValidityInformations: new CouponValidityInformationArray([ 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 ef0ec38..dd00697 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Find/FindCampaignRequestTest.php @@ -61,6 +61,7 @@ describe('find campaign request use case', function (): void { ), couponProcessingCodes: new ProcessingCodeArray([ new ProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('4006381333931', Country::BENELUX), ]), couponValidityInformations: new CouponValidityInformationArray([ 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 c4d8b81..e849099 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/List/ListCampaignsRequestTest.php @@ -55,6 +55,7 @@ describe('list campaigns request use case', function (): void { ), couponProcessingCodes: new ProcessingCodeArray([ new ProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('4006381333931', Country::BENELUX), ]), couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), @@ -80,6 +81,7 @@ describe('list campaigns request use case', function (): void { ), couponProcessingCodes: new ProcessingCodeArray([ new ProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('4006381333931', Country::BENELUX), ]), couponValidityInformations: new CouponValidityInformationArray([ 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 3c54d9c..b4de50e 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -56,6 +56,7 @@ describe('reject campaign request use case', function (): void { ), couponProcessingCodes: new ProcessingCodeArray([ new ProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('4006381333931', Country::BENELUX), ]), couponValidityInformations: new CouponValidityInformationArray([ 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 edf6a44..a47b3f2 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -51,6 +51,7 @@ describe('campaign request', function (): void { ), couponProcessingCodes: new ProcessingCodeArray([ new ProcessingCode('5425003944130', Country::FRANCE), + new ProcessingCode('4006381333931', Country::BENELUX), ]), couponValidityInformations: new CouponValidityInformationArray([ new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE), From 5b2da5859b94bd6fc3021c1e7fb4979448263922 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 14 Oct 2026 10:15:35 +0200 Subject: [PATCH 15/21] refactor: :recycle: extract diffusion country count check --- .../Coupon/CouponCountryCoverage.php | 18 +++++++++------- .../Coupon/CouponCountryCoverageTest.php | 21 ++----------------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php index f336808..b64b92a 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponCountryCoverage.php @@ -30,13 +30,8 @@ final readonly class CouponCountryCoverage } } - if (count($validityInformations->value) !== count($processingCodes->value)) { - throw new \DomainException('A coupon requires exactly one validity information per diffusion country.'); - } - - if (count($legalInformations->value) !== count($processingCodes->value)) { - throw new \DomainException('A coupon requires exactly one legal information per diffusion country.'); - } + self::countDiffusionCountries($validityInformations, $processingCodes); + self::countDiffusionCountries($legalInformations, $processingCodes); } private static function hasInformationFor( @@ -48,4 +43,13 @@ final readonly class CouponCountryCoverage fn (CouponValidityInformation | CouponLegalInformation $information): bool => $information->forLocale === $country, ); } + + private static function countDiffusionCountries( + CouponValidityInformationArray | CouponLegalInformationArray $informations, + ProcessingCodeArray $processingCodes, + ): void { + if (count($informations->value) !== count($processingCodes->value)) { + throw new \DomainException('A coupon requires exactly one information per diffusion country.'); + } + } } diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php index d6dbe67..47c19d2 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponCountryCoverageTest.php @@ -90,7 +90,7 @@ describe('coupon country coverage', function (): void { expect($action)->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.'); }); - it('rejects validity information for a country where the coupon is not diffused', function () use ( + it('rejects information for a country where the coupon is not diffused', function () use ( $franceCode, $franceValidity, $beneluxValidity, @@ -104,24 +104,7 @@ describe('coupon country coverage', function (): void { ); // THEN - expect($action)->toThrow(\DomainException::class, 'A coupon requires exactly one validity information per diffusion country.'); - }); - - it('rejects legal information for a country where the coupon is not diffused', function () use ( - $franceCode, - $franceValidity, - $franceLegal, - $beneluxLegal, - ): void { - // GIVEN / WHEN - $action = fn () => CouponCountryCoverage::assert( - new ProcessingCodeArray([$franceCode]), - new CouponValidityInformationArray([$franceValidity]), - new CouponLegalInformationArray([$franceLegal, $beneluxLegal]), - ); - - // THEN - expect($action)->toThrow(\DomainException::class, 'A coupon requires exactly one legal information per diffusion country.'); + expect($action)->toThrow(\DomainException::class, 'A coupon requires exactly one information per diffusion country.'); }); it('cannot be instantiated from the outside', function (): void { From 0dba81c214db3f64e48b01f72d34f16f2a29575b Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 14 Oct 2026 11:53:24 +0200 Subject: [PATCH 16/21] refactor: :recycle: compute quantity from stock movements --- .../UseCases/Coupon/Create/CreateCoupon.php | 9 +-- .../UseCases/Coupon/Create/Input.php | 4 +- .../UseCases/Coupon/Create/Output.php | 2 - .../UseCases/Coupon/Find/FindCoupon.php | 2 +- .../Coupon/Find/Output/CouponFound.php | 3 +- .../Coupon/Update/Output/CouponUpdated.php | 2 - .../UseCases/Coupon/Update/UpdateCoupon.php | 1 - src/Domain/Models/Entities/Coupon.php | 13 ++-- .../Coupon/CouponMovementArray.php | 5 ++ .../ValueObjects/Coupon/CouponQuantity.php | 16 ----- .../Coupon/Create/CreateCouponTest.php | 33 ++-------- .../UseCases/Coupon/Find/FindCouponTest.php | 4 +- .../UseCases/Coupon/List/ListCouponTest.php | 3 - .../Coupon/Update/UpdateCouponTest.php | 3 - .../Domain/Models/Entities/CouponTest.php | 61 ++++++++++++------- .../Coupon/CouponQuantityTest.php | 40 ------------ 16 files changed, 59 insertions(+), 142 deletions(-) delete mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php delete mode 100644 tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php diff --git a/src/Application/UseCases/Coupon/Create/CreateCoupon.php b/src/Application/UseCases/Coupon/Create/CreateCoupon.php index 287275e..ffa6f15 100644 --- a/src/Application/UseCases/Coupon/Create/CreateCoupon.php +++ b/src/Application/UseCases/Coupon/Create/CreateCoupon.php @@ -5,8 +5,6 @@ declare(strict_types=1); namespace Application\UseCases\Coupon\Create; use Domain\Models\Entities\Coupon; -use Domain\Models\ValueObjects\Coupon\CouponMovement; -use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Ports\Repositories\BrandRepository; use Domain\Ports\Repositories\CouponRepository; use Shared\DDD\UseCases\Input as BaseInput; @@ -36,9 +34,6 @@ final readonly class CreateCoupon implements UseCase, Transactional throw new \DomainException(sprintf('Coupon code "%s" already exists.', $input->code->value)); } - $couponMovement = new CouponMovement($input->quantity->value); - $couponMovements = new CouponMovementArray([$couponMovement]); - $coupon = Coupon::create( code: $input->code, name: $input->name, @@ -51,8 +46,7 @@ final readonly class CreateCoupon implements UseCase, Transactional brandCode: $input->brandCode, createdBy: $input->createdBy, products: $input->products, - movements: $couponMovements, - quantity: $input->quantity, + movements: $input->movements, availabilityCalendar: $input->availabilityCalendar, ); @@ -72,7 +66,6 @@ final readonly class CreateCoupon implements UseCase, Transactional createdBy: $coupon->createdBy, products: $coupon->products, movements: $coupon->movements, - quantity: $coupon->quantity, createdAt: $coupon->createdAt(), availabilityCalendar: $coupon->availabilityCalendar, ); diff --git a/src/Application/UseCases/Coupon/Create/Input.php b/src/Application/UseCases/Coupon/Create/Input.php index 2fe9c91..9fc1bb4 100644 --- a/src/Application/UseCases/Coupon/Create/Input.php +++ b/src/Application/UseCases/Coupon/Create/Input.php @@ -10,10 +10,10 @@ use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar; use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\Coupon\CouponDiffusion; use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray; +use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\ProcessingCodeArray; @@ -33,7 +33,7 @@ final readonly class Input implements UseCaseInput public BrandCode $brandCode, public ActorId $createdBy, public CouponProductArray $products, - public CouponQuantity $quantity, + public CouponMovementArray $movements, public ?CouponAvailabilityCalendar $availabilityCalendar = null, ) { } diff --git a/src/Application/UseCases/Coupon/Create/Output.php b/src/Application/UseCases/Coupon/Create/Output.php index a1ae503..e359990 100644 --- a/src/Application/UseCases/Coupon/Create/Output.php +++ b/src/Application/UseCases/Coupon/Create/Output.php @@ -15,7 +15,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\ProcessingCodeArray; @@ -37,7 +36,6 @@ final readonly class Output implements UseCaseOutput public ActorId $createdBy, public CouponProductArray $products, public CouponMovementArray $movements, - public CouponQuantity $quantity, public \DateTimeInterface $createdAt, public ?CouponAvailabilityCalendar $availabilityCalendar = null, ) { diff --git a/src/Application/UseCases/Coupon/Find/FindCoupon.php b/src/Application/UseCases/Coupon/Find/FindCoupon.php index 9db5b36..f165576 100644 --- a/src/Application/UseCases/Coupon/Find/FindCoupon.php +++ b/src/Application/UseCases/Coupon/Find/FindCoupon.php @@ -55,7 +55,7 @@ final readonly class FindCoupon implements UseCase createdBy: $coupon->createdBy, products: $coupon->products, movements: $coupon->movements, - quantity: $coupon->quantity, + quantity: $coupon->quantity(), campaigns: $campaigns, createdAt: $coupon->createdAt(), updatedAt: $coupon->updatedAt(), diff --git a/src/Application/UseCases/Coupon/Find/Output/CouponFound.php b/src/Application/UseCases/Coupon/Find/Output/CouponFound.php index e8e6afb..ba12015 100644 --- a/src/Application/UseCases/Coupon/Find/Output/CouponFound.php +++ b/src/Application/UseCases/Coupon/Find/Output/CouponFound.php @@ -15,7 +15,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\ProcessingCodeArray; @@ -40,7 +39,7 @@ final readonly class CouponFound implements Output public ActorId $createdBy, public CouponProductArray $products, public CouponMovementArray $movements, - public CouponQuantity $quantity, + public int $quantity, public array $campaigns, public \DateTimeInterface $createdAt, public ?\DateTimeInterface $updatedAt, diff --git a/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php b/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php index d2204b5..775ba23 100644 --- a/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php +++ b/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php @@ -14,7 +14,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\ProcessingCodeArray; @@ -35,7 +34,6 @@ final readonly class CouponUpdated implements Output public ActorId $createdBy, public CouponProductArray $products, public CouponMovementArray $movements, - public CouponQuantity $quantity, public \DateTimeInterface $createdAt, public ?\DateTimeInterface $updatedAt, ) { diff --git a/src/Application/UseCases/Coupon/Update/UpdateCoupon.php b/src/Application/UseCases/Coupon/Update/UpdateCoupon.php index 6d44e0b..425eb0e 100644 --- a/src/Application/UseCases/Coupon/Update/UpdateCoupon.php +++ b/src/Application/UseCases/Coupon/Update/UpdateCoupon.php @@ -66,7 +66,6 @@ final readonly class UpdateCoupon implements UseCase, Transactional createdBy: $coupon->createdBy, products: $coupon->products, movements: $coupon->movements, - quantity: $coupon->quantity, createdAt: $coupon->createdAt(), updatedAt: $coupon->updatedAt(), ); diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index 6648fb4..213eab0 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -16,7 +16,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; use Domain\Models\ValueObjects\ProcessingCodeArray; @@ -43,7 +42,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier public ActorId $createdBy, public CouponProductArray $products, public CouponMovementArray $movements, - public CouponQuantity $quantity, public ?CouponAvailabilityCalendar $availabilityCalendar = null, public ?\DateTimeInterface $updatedAt = null, ) { @@ -69,6 +67,11 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier return $this->updatedAt; } + public function quantity(): int + { + return $this->movements->quantity(); + } + public static function create( CouponCode $code, CouponName $name, @@ -82,7 +85,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier ActorId $createdBy, CouponProductArray $products, CouponMovementArray $movements, - CouponQuantity $quantity, ?CouponAvailabilityCalendar $availabilityCalendar = null, ?\DateTimeInterface $updatedAt = null, ): Coupon { @@ -100,7 +102,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, updatedAt: $updatedAt, ); @@ -120,7 +121,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier ActorId $createdBy, CouponProductArray $products, CouponMovementArray $movements, - CouponQuantity $quantity, ?CouponAvailabilityCalendar $availabilityCalendar = null, ?\DateTimeInterface $updatedAt = null, ): self { @@ -138,7 +138,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, updatedAt: $updatedAt, ); @@ -160,7 +159,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier createdBy: $this->createdBy, products: $this->products, movements: $this->movements, - quantity: $this->quantity, availabilityCalendar: $this->availabilityCalendar, updatedAt: DatesHelper::now(), ); @@ -182,7 +180,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier createdBy: $this->createdBy, products: $this->products, movements: $this->movements, - quantity: $this->quantity, availabilityCalendar: $this->availabilityCalendar, updatedAt: DatesHelper::now(), ); diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php b/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php index 7552374..20ffd65 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponMovementArray.php @@ -20,4 +20,9 @@ final readonly class CouponMovementArray } } } + + public function quantity(): int + { + return array_sum(array_column($this->value, 'quantity')); + } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php b/src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php deleted file mode 100644 index 0e6a1a0..0000000 --- a/src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php +++ /dev/null @@ -1,16 +0,0 @@ -value <= 0) { - throw new \DomainException('CouponQuantity quantity must be positive.'); - } - } -} diff --git a/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php index dbad074..e550347 100644 --- a/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php @@ -22,7 +22,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\MoneyAmount; @@ -64,7 +63,9 @@ describe('create coupon use case', function (): void { 'brandCode' => new BrandCode('TST89'), 'createdBy' => ActorId::generate(), 'products' => new CouponProductArray(), - 'quantity' => new CouponQuantity(1000), + 'movements' => new CouponMovementArray([ + new CouponMovement(100), + ]), 'availabilityCalendar' => null, ], $overrides)); @@ -98,8 +99,7 @@ describe('create coupon use case', function (): void { ->and($result->brandCode)->toEqual($input->brandCode) ->and($result->createdBy)->toEqual($input->createdBy) ->and($result->products)->toEqual($input->products) - ->and($result->movements)->toBeInstanceOf(CouponMovementArray::class) - ->and($result->quantity)->toEqual($input->quantity) + ->and($result->movements)->toEqual($input->movements) ->and($result->availabilityCalendar)->toBeNull() ->and($result->createdAt)->toBeInstanceOf(\DateTimeInterface::class); }); @@ -144,31 +144,6 @@ describe('create coupon use case', function (): void { expect($action)->toThrow(\DomainException::class, 'Coupon code "COU01" already exists.'); }); - it('initialises the stock with a first movement equal to the initial quantity', function () use ($makeInput): void { - // GIVEN - $input = $makeInput(); - - $brandRepository = \Mockery::mock(BrandRepository::class); - $brandRepository->shouldReceive('existsByCode')->once()->andReturn(true); - - $couponRepository = \Mockery::mock(CouponRepository::class); - $couponRepository->shouldReceive('existsByCode')->once()->andReturn(false); - $couponRepository->shouldReceive('save')->once()->with(\Mockery::on( - fn (Coupon $saved): bool => count($saved->movements->value) === 1 - && $saved->movements->value[0] instanceof CouponMovement - && $saved->movements->value[0]->quantity === $input->quantity->value, - )); - - $useCase = new CreateCoupon($couponRepository, $brandRepository); - - // WHEN - $result = $useCase->execute($input); - - // THEN - expect($result->movements)->toBeInstanceOf(CouponMovementArray::class) - ->and($result->movements->value)->toHaveCount(1); - }); - it('creates a coupon with an availability calendar', function () use ($makeInput): void { // GIVEN $calendar = new CouponAvailabilityCalendar([ diff --git a/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php index 215f2d6..eb64946 100644 --- a/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php @@ -28,7 +28,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\IncoCode; @@ -95,7 +94,6 @@ describe('find coupon use case', function (): void { movements: new CouponMovementArray([ new CouponMovement(1000) ]), - quantity: new CouponQuantity(1000), ); $campaign = Campaign::create( code: new CampaignCode('CMP01'), @@ -140,7 +138,7 @@ describe('find coupon use case', function (): void { ->and($result->createdBy)->toBe($coupon->createdBy) ->and($result->products)->toBe($coupon->products) ->and($result->movements)->toBe($coupon->movements) - ->and($result->quantity)->toBe($coupon->quantity) + ->and($result->quantity)->toBe($coupon->quantity()) ->and($result->campaigns)->toBe([$campaign]) ->and($result->createdAt)->toBeInstanceOf(\DateTimeInterface::class) ->and($result->updatedAt)->toBeNull(); diff --git a/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php b/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php index 205eb6f..ce75463 100644 --- a/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php @@ -23,7 +23,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponSortCriteria; use Domain\Models\ValueObjects\Coupon\CouponSortField; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; @@ -72,7 +71,6 @@ describe('list coupons use case', function (): void { createdBy: ActorId::generate(), products: new CouponProductArray([new IncoCode(35784)]), movements: new CouponMovementArray([new CouponMovement(100)]), - quantity: new CouponQuantity(100), availabilityCalendar: new CouponAvailabilityCalendar([ new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')), ]), @@ -104,7 +102,6 @@ describe('list coupons use case', function (): void { createdBy: ActorId::generate(), products: new CouponProductArray([new IncoCode(35784)]), movements: new CouponMovementArray([new CouponMovement(100)]), - quantity: new CouponQuantity(100), ); $pagination = Pagination::firstPage(); $brandCode = new BrandCode('AAA01'); diff --git a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php index e37cdca..21aa859 100644 --- a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php @@ -22,7 +22,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\IncoCode; @@ -79,7 +78,6 @@ describe('update coupon use case', function (): void { movements: new CouponMovementArray([ new CouponMovement(1000), ]), - quantity: new CouponQuantity(1000), ); it('updates the picture and keeps the other fields', function () use ($makeCoupon): void { @@ -101,7 +99,6 @@ describe('update coupon use case', function (): void { && $saved->name === $coupon->name && $saved->amount === $coupon->amount && $saved->diffusion === $coupon->diffusion - && $saved->quantity === $coupon->quantity && $saved->updatedAt instanceof \DateTimeInterface, )); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index 1e77108..b1940a0 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -20,7 +20,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\IncoCode; @@ -65,7 +64,6 @@ describe('coupon', function (): void { $createdBy = ActorId::generate(); $products = new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]); $movements = new CouponMovementArray([new CouponMovement(100)]); - $quantity = new CouponQuantity(100); $availabilityCalendar = new CouponAvailabilityCalendar([ new Period( new DateTime('2026-08-25'), @@ -89,7 +87,6 @@ describe('coupon', function (): void { $createdBy, $products, $movements, - $quantity, $availabilityCalendar, ): void { // GIVEN @@ -110,7 +107,6 @@ describe('coupon', function (): void { createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, ); @@ -146,7 +142,6 @@ describe('coupon', function (): void { $createdBy, $products, $movements, - $quantity, ): void { // GIVEN $id = CouponId::generate(); @@ -166,7 +161,6 @@ describe('coupon', function (): void { createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, ); // THEN @@ -186,7 +180,6 @@ describe('coupon', function (): void { $brandCode, $createdBy, $movements, - $quantity, $availabilityCalendar, ): void { // GIVEN @@ -207,7 +200,6 @@ describe('coupon', function (): void { createdBy: $createdBy, products: new CouponProductArray(), movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, ); @@ -228,7 +220,6 @@ describe('coupon', function (): void { $createdBy, $products, $movements, - $quantity, $availabilityCalendar, ): void { // GIVEN @@ -249,7 +240,6 @@ describe('coupon', function (): void { createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, ))->toThrow(\DomainException::class, 'Value of CouponValidityInformationArray must be CouponValidityInformation instances.'); }); @@ -266,7 +256,6 @@ describe('coupon', function (): void { $createdBy, $products, $movements, - $quantity, $availabilityCalendar, ): void { // GIVEN @@ -287,7 +276,6 @@ describe('coupon', function (): void { createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, ))->toThrow(\DomainException::class, 'Value of CouponLegalInformationArray must be CouponLegalInformation instances.'); }); @@ -305,7 +293,6 @@ describe('coupon', function (): void { $createdBy, $products, $movements, - $quantity, $availabilityCalendar, ): void { // GIVEN @@ -327,7 +314,6 @@ describe('coupon', function (): void { createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, updatedAt: $updatedAt, ); @@ -350,7 +336,6 @@ describe('coupon', function (): void { $createdBy, $products, $movements, - $quantity, $availabilityCalendar, ): void { // GIVEN / WHEN @@ -367,7 +352,6 @@ describe('coupon', function (): void { createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, ); @@ -385,7 +369,6 @@ describe('coupon', function (): void { ->and($coupon->createdBy)->toBe($createdBy) ->and($coupon->products)->toBe($products) ->and($coupon->movements)->toBe($movements) - ->and($coupon->quantity)->toBe($quantity) ->and($coupon->availabilityCalendar)->toBe($availabilityCalendar) ->and($coupon->updatedAt)->toBeNull() ->and($coupon->updatedAt())->toBeNull(); @@ -404,7 +387,6 @@ describe('coupon', function (): void { $createdBy, $products, $movements, - $quantity, $availabilityCalendar, ): void { // GIVEN / WHEN @@ -421,7 +403,6 @@ describe('coupon', function (): void { createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, ); $secondCoupon = Coupon::create( @@ -437,7 +418,6 @@ describe('coupon', function (): void { createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, ); @@ -458,7 +438,6 @@ describe('coupon', function (): void { $createdBy, $products, $movements, - $quantity, $availabilityCalendar, ): void { // GIVEN @@ -478,7 +457,6 @@ describe('coupon', function (): void { createdBy: $createdBy, products: $products, movements: $movements, - quantity: $quantity, availabilityCalendar: $availabilityCalendar, ); @@ -488,4 +466,43 @@ describe('coupon', function (): void { // THEN expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v')); }); + + it('computes the quantity from its movements', function () use ( + $code, + $name, + $amount, + $picture, + $diffusion, + $validityInformations, + $legalInformations, + $processingCode, + $brandCode, + $createdBy, + $products, + ): void { + // GIVEN + $coupon = Coupon::create( + code: $code, + name: $name, + amount: $amount, + picture: $picture, + diffusion: $diffusion, + validityInformations: $validityInformations, + legalInformations: $legalInformations, + processingCode: $processingCode, + brandCode: $brandCode, + createdBy: $createdBy, + products: $products, + movements: new CouponMovementArray([ + new CouponMovement(1000), + new CouponMovement(-200), + ]), + ); + + // WHEN + $quantity = $coupon->quantity(); + + // THEN + expect($quantity)->toBe(800); + }); }); diff --git a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php b/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php deleted file mode 100644 index b4c7295..0000000 --- a/tests/Unit/Domain/Models/ValueObjects/Coupon/CouponQuantityTest.php +++ /dev/null @@ -1,40 +0,0 @@ -toBeInstanceOf(CouponQuantity::class) - ->and($couponQuantity->value)->toBe($quantity); - }); - - it('rejects a quantity of zero', function () use ($zeroQuantity): void { - // WHEN - $action = fn (): CouponQuantity => new CouponQuantity($zeroQuantity); - - // THEN - expect($action)->toThrow(\DomainException::class, 'CouponQuantity quantity must be positive.'); - }); - - it('rejects a negative quantity', function () use ($negativeQuantity): void { - // WHEN - $action = fn (): CouponQuantity => new CouponQuantity($negativeQuantity); - - // THEN - expect($action)->toThrow(\DomainException::class, 'CouponQuantity quantity must be positive.'); - }); -}); From 933cfa48adbe6116c2087aa304db3f0efc9e0e03 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Tue, 13 Oct 2026 14:51:02 +0200 Subject: [PATCH 17/21] feat: :construction_worker: add ListPrintableCoupon Use Case and tests --- .../UseCases/PrintableCoupon/List/Input.php | 22 ++ .../List/ListPrintableCoupons.php | 76 +++++ .../UseCases/PrintableCoupon/List/Output.php | 17 ++ .../List/PrintableCouponListItem.php | 24 ++ .../Models/Entities/PrintableCoupon.php | 84 ++++++ .../Models/ValueObjects/CrmHashcode.php | 28 ++ .../PrintableCoupon/MarketingDirectCode.php | 47 +++ .../PrintableCoupon/PrintableCouponCode.php | 16 + .../PrintableCouponSortCriteria.php | 14 + .../PrintableCouponSortField.php | 11 + .../PrintableCoupon/PrintedCoupon.php | 28 ++ .../PrintableCoupon/PrintedCouponId.php | 16 + .../PrintableCoupon/UnprintedCoupon.php | 21 ++ .../PrintableCoupon/UnprintedReason.php | 16 + .../PrintableCouponRepository.php | 31 ++ .../List/ListPrintableCouponTest.php | 279 ++++++++++++++++++ .../Models/Entities/PrintableCouponTest.php | 122 ++++++++ .../Models/ValueObjects/CrmHashcodeTest.php | 49 +++ .../MarketingDirectCodeTest.php | 32 ++ .../PrintableCouponCodeTest.php | 28 ++ .../PrintableCoupon/PrintedCouponIdTest.php | 28 ++ .../PrintableCoupon/PrintedCouponTest.php | 62 ++++ .../PrintableCoupon/UnprintedCouponTest.php | 69 +++++ .../PrintableCoupon/UnprintedReasonTest.php | 33 +++ 24 files changed, 1153 insertions(+) create mode 100644 src/Application/UseCases/PrintableCoupon/List/Input.php create mode 100644 src/Application/UseCases/PrintableCoupon/List/ListPrintableCoupons.php create mode 100644 src/Application/UseCases/PrintableCoupon/List/Output.php create mode 100644 src/Application/UseCases/PrintableCoupon/List/PrintableCouponListItem.php create mode 100644 src/Domain/Models/Entities/PrintableCoupon.php create mode 100644 src/Domain/Models/ValueObjects/CrmHashcode.php create mode 100644 src/Domain/Models/ValueObjects/PrintableCoupon/MarketingDirectCode.php create mode 100644 src/Domain/Models/ValueObjects/PrintableCoupon/PrintableCouponCode.php create mode 100644 src/Domain/Models/ValueObjects/PrintableCoupon/PrintableCouponSortCriteria.php create mode 100644 src/Domain/Models/ValueObjects/PrintableCoupon/PrintableCouponSortField.php create mode 100644 src/Domain/Models/ValueObjects/PrintableCoupon/PrintedCoupon.php create mode 100644 src/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponId.php create mode 100644 src/Domain/Models/ValueObjects/PrintableCoupon/UnprintedCoupon.php create mode 100644 src/Domain/Models/ValueObjects/PrintableCoupon/UnprintedReason.php create mode 100644 src/Domain/Ports/Repositories/PrintableCouponRepository.php create mode 100644 tests/Unit/Application/UseCases/PrintableCoupon/List/ListPrintableCouponTest.php create mode 100644 tests/Unit/Domain/Models/Entities/PrintableCouponTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/CrmHashcodeTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/MarketingDirectCodeTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintableCouponCodeTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponIdTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/UnprintedCouponTest.php create mode 100644 tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/UnprintedReasonTest.php diff --git a/src/Application/UseCases/PrintableCoupon/List/Input.php b/src/Application/UseCases/PrintableCoupon/List/Input.php new file mode 100644 index 0000000..7681a1d --- /dev/null +++ b/src/Application/UseCases/PrintableCoupon/List/Input.php @@ -0,0 +1,22 @@ + */ +final readonly class ListPrintableCoupons implements UseCase +{ + public function __construct( + private PrintableCouponRepository $repository, + private CouponRepository $couponRepository, + ) { + } + + #[\Override] + public function execute(BaseInput $input): Output + { + Assert::isInstanceOf($input, Input::class); + + $result = $this->repository->findAllAvailable( + pagination: $input->pagination, + campaignCode: $input->campaignCode, + crmCustomerHashcode: $input->crmCustomerHashcode, + sortCriteria: $input->sortCriteria, + ); + + $printableCoupons = $result->getItems(); + + /** @var array $picturesByCouponCode */ + $picturesByCouponCode = []; + + foreach ($printableCoupons as $printableCoupon) { + $couponCode = $printableCoupon->couponCode; + + if (isset($picturesByCouponCode[$couponCode->value])) { + continue; + } + + $coupon = $this->couponRepository->findByCode($couponCode) + ?? throw new \LogicException(sprintf('Coupon "%s" referenced by a printable coupon does not exist.', $couponCode->value)); + + $picturesByCouponCode[$couponCode->value] = $coupon->picture; + } + + $items = array_map( + static fn (PrintableCoupon $printableCoupon): PrintableCouponListItem => new PrintableCouponListItem( + code: $printableCoupon->code, + couponCode: $printableCoupon->couponCode, + couponName: $printableCoupon->couponName, + couponPicture: $picturesByCouponCode[$printableCoupon->couponCode->value], + distributableUntil: $printableCoupon->distributableUntil, + createdAt: $printableCoupon->createdAt(), + updatedAt: $printableCoupon->updatedAt(), + ), + $printableCoupons, + ); + + return new Output( + printableCoupons: new Paginated( + pagination: $result->getPagination(), + items: $items, + total: $result->getTotal(), + ), + ); + } +} diff --git a/src/Application/UseCases/PrintableCoupon/List/Output.php b/src/Application/UseCases/PrintableCoupon/List/Output.php new file mode 100644 index 0000000..c6ac9ed --- /dev/null +++ b/src/Application/UseCases/PrintableCoupon/List/Output.php @@ -0,0 +1,17 @@ + $printableCoupons */ + public function __construct( + public Paginated $printableCoupons, + ) { + } +} diff --git a/src/Application/UseCases/PrintableCoupon/List/PrintableCouponListItem.php b/src/Application/UseCases/PrintableCoupon/List/PrintableCouponListItem.php new file mode 100644 index 0000000..f7f50c6 --- /dev/null +++ b/src/Application/UseCases/PrintableCoupon/List/PrintableCouponListItem.php @@ -0,0 +1,24 @@ + + */ +final readonly class PrintableCoupon implements AggregateRoot, HasTimestampIdentifier +{ + private function __construct( + public PrintableCouponCode $code, + public CampaignCode $campaignCode, + public CouponCode $couponCode, + public CouponName $couponName, + public CrmHashcode $crmCustomerHashcode, + public \DateTimeInterface $distributableUntil, + public \DateTimeInterface $updatedAt, + ) { + } + + public function id(): PrintableCouponCode + { + return $this->code; + } + + public function createdAt(): \DateTimeInterface + { + return $this->code->toDateTime(); + } + + public function updatedAt(): \DateTimeInterface + { + return $this->updatedAt; + } + + public static function create( + CampaignCode $campaignCode, + CouponCode $couponCode, + CouponName $couponName, + CrmHashcode $crmCustomerHashcode, + \DateTimeInterface $distributableUntil, + ): self { + $code = PrintableCouponCode::generate(); + return new self( + code: $code, + campaignCode: $campaignCode, + couponCode: $couponCode, + couponName: $couponName, + crmCustomerHashcode: $crmCustomerHashcode, + distributableUntil: $distributableUntil, + updatedAt: $code->toDateTime(), + ); + } + + public static function hydrate( + PrintableCouponCode $code, + CampaignCode $campaignCode, + CouponCode $couponCode, + CouponName $couponName, + CrmHashcode $crmCustomerHashcode, + \DateTimeInterface $distributableUntil, + ): self { + return new self( + code: $code, + campaignCode: $campaignCode, + couponCode: $couponCode, + couponName: $couponName, + crmCustomerHashcode: $crmCustomerHashcode, + distributableUntil: $distributableUntil, + updatedAt: DatesHelper::now(), + ); + } +} diff --git a/src/Domain/Models/ValueObjects/CrmHashcode.php b/src/Domain/Models/ValueObjects/CrmHashcode.php new file mode 100644 index 0000000..195d915 --- /dev/null +++ b/src/Domain/Models/ValueObjects/CrmHashcode.php @@ -0,0 +1,28 @@ +value === $other->value; + } +} diff --git a/src/Domain/Models/ValueObjects/PrintableCoupon/MarketingDirectCode.php b/src/Domain/Models/ValueObjects/PrintableCoupon/MarketingDirectCode.php new file mode 100644 index 0000000..d21e226 --- /dev/null +++ b/src/Domain/Models/ValueObjects/PrintableCoupon/MarketingDirectCode.php @@ -0,0 +1,47 @@ +value) !== 1) { + throw new \DomainException( + 'Marketing Direct Code must be exactly 13 numeric characters, e.g. 5425003944130.', + ); + } + } + + public static function fromInt(int $sequence): self + { + if ($sequence < 0) { + throw new \DomainException('Marketing Direct sequence must be positive.'); + } + + return new self(str_pad((string) $sequence, 13, '0', STR_PAD_LEFT)); + } + + public function toInt(): int + { + return (int) $this->value; + } + + #[\Override] + public function equals(mixed $other): bool + { + return $other instanceof self && $this->value === $other->value; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Domain/Models/ValueObjects/PrintableCoupon/PrintableCouponCode.php b/src/Domain/Models/ValueObjects/PrintableCoupon/PrintableCouponCode.php new file mode 100644 index 0000000..6e0291a --- /dev/null +++ b/src/Domain/Models/ValueObjects/PrintableCoupon/PrintableCouponCode.php @@ -0,0 +1,16 @@ +id->toDateTime(); + } +} diff --git a/src/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponId.php b/src/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponId.php new file mode 100644 index 0000000..b04d121 --- /dev/null +++ b/src/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponId.php @@ -0,0 +1,16 @@ +value) === '') { + throw new \DomainException('Unprinted reason cannot be empty.'); + } + } +} diff --git a/src/Domain/Ports/Repositories/PrintableCouponRepository.php b/src/Domain/Ports/Repositories/PrintableCouponRepository.php new file mode 100644 index 0000000..4ead8e6 --- /dev/null +++ b/src/Domain/Ports/Repositories/PrintableCouponRepository.php @@ -0,0 +1,31 @@ + + */ +interface PrintableCouponRepository extends Repository +{ + /** + * @return Paginated + */ + public function findAllAvailable( + Pagination $pagination, + CampaignCode $campaignCode, + CrmHashcode $crmCustomerHashcode, + ?PrintableCouponSortCriteria $sortCriteria = null, + ): Paginated; + + public function save(PrintableCoupon $printableCoupon): void; +} diff --git a/tests/Unit/Application/UseCases/PrintableCoupon/List/ListPrintableCouponTest.php b/tests/Unit/Application/UseCases/PrintableCoupon/List/ListPrintableCouponTest.php new file mode 100644 index 0000000..f91bb83 --- /dev/null +++ b/tests/Unit/Application/UseCases/PrintableCoupon/List/ListPrintableCouponTest.php @@ -0,0 +1,279 @@ + new CouponPicture( + new PicturePath("files/{$name}.jpg"), + new PicturePath("files/{$name}-thumbnail.jpg"), + new PicturePath("files/{$name}-mobile.jpg"), + ); + + $createInput = fn (PrintableCouponSortCriteria | null $sortCriteria = null): Input => new Input( + pagination: Pagination::firstPage(), + campaignCode: new CampaignCode('AAA01'), + crmCustomerHashcode: new CrmHashcode(Uuid::uuid4()), + sortCriteria: $sortCriteria ?? new PrintableCouponSortCriteria( + field: PrintableCouponSortField::CreatedAt, + direction: \SortDirection::Descending, + ), + ); + + $createPrintableCoupon = fn (CouponCode $couponCode): PrintableCoupon => PrintableCoupon::hydrate( + code: PrintableCouponCode::generate(), + campaignCode: new CampaignCode('AAA01'), + couponCode: $couponCode, + couponName: new CouponName('Coupon name'), + crmCustomerHashcode: new CrmHashcode(Uuid::uuid4()), + distributableUntil: new DateTime('2027-01-01'), + ); + + $createCoupon = fn (CouponCode $code, CouponPicture $picture): Coupon => Coupon::create( + code: $code, + name: new CouponName('Coupon name'), + amount: new MoneyAmount(new Money(Currency::EUR, 100)), + picture: $picture, + diffusion: new CouponDiffusion( + new Period( + new DateTime('2026-01-01'), + new DateTime('2026-02-01'), + ), + ), + validityInformations: new CouponValidityInformationArray([ + new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE + ), + ]), + legalInformations: new CouponLegalInformationArray([ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE + ), + ]), + processingCode: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), + ]), + brandCode: new BrandCode('AAA01'), + createdBy: ActorId::generate(), + products: new CouponProductArray([ + new IncoCode(35784), + new IncoCode(15522), + ]), + movements: new CouponMovementArray([ + new CouponMovement(1000) + ]), + quantity: new CouponQuantity(1000), + ); + + it('returns the printable coupons with the picture of their coupon', function () use ( + $createInput, + $createPicture, + $createCoupon, + $createPrintableCoupon, + ): void { + // GIVEN + $input = $createInput(); + $picture = $createPicture('picture'); + $couponCode = new CouponCode('AAA01'); + $printable = $createPrintableCoupon($couponCode); + $coupon = $createCoupon($couponCode, $picture); + + $paginated = new Paginated(pagination: $input->pagination, items: [$printable], total: 1); + + $repository = \Mockery::mock(PrintableCouponRepository::class); + $repository->shouldReceive('findAllAvailable') + ->once() + ->with($input->pagination, $input->campaignCode, $input->crmCustomerHashcode, $input->sortCriteria) + ->andReturn($paginated); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode') + ->once() + ->with($printable->couponCode) + ->andReturn($coupon); + + $useCase = new ListPrintableCoupons($repository, $couponRepository); + + // WHEN + $output = $useCase->execute($input); + + // THEN + $items = $output->printableCoupons->getItems(); + + expect($items)->toHaveCount(1) + ->and($output->printableCoupons->getTotal())->toBe(1) + ->and($items[0]->code)->toBe($printable->code) + ->and($items[0]->couponCode)->toBe($printable->couponCode) + ->and($items[0]->couponName)->toBe($printable->couponName) + ->and($items[0]->couponPicture)->toBe($picture) + ->and($items[0]->distributableUntil)->toBe($printable->distributableUntil); + }); + + it('forwards the sort criteria and keeps the order returned by the repository', function () use ( + $createInput, + $createPicture, + $createCoupon, + $createPrintableCoupon, + ): void { + // GIVEN + $sortCriteria = new PrintableCouponSortCriteria( + field: PrintableCouponSortField::Name, + direction: \SortDirection::Descending, + ); + $input = $createInput($sortCriteria); + + $code1 = new CouponCode('AAA01'); + $code2 = new CouponCode('BBB02'); + $picture1 = $createPicture('picture-1'); + $picture2 = $createPicture('picture-2'); + + $printableCoupon1 = $createPrintableCoupon($code1); + $printableCoupon2 = $createPrintableCoupon($code2); + + $repository = \Mockery::mock(PrintableCouponRepository::class); + $repository->shouldReceive('findAllAvailable') + ->once() + ->with($input->pagination, $input->campaignCode, $input->crmCustomerHashcode, $sortCriteria) + ->andReturn(new Paginated(pagination: $input->pagination, items: [$printableCoupon2, $printableCoupon1], total: 2)); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->with($code2)->once() + ->andReturn($createCoupon($code2, $picture2)); + $couponRepository->shouldReceive('findByCode')->with($code1)->once() + ->andReturn($createCoupon($code1, $picture1)); + + $useCase = new ListPrintableCoupons($repository, $couponRepository); + + // WHEN + $items = $useCase->execute($input)->printableCoupons->getItems(); + + // THEN + expect($items)->toHaveCount(2) + ->and($items[0]->code)->toBe($printableCoupon2->code) + ->and($items[0]->couponName)->toBe($printableCoupon2->couponName) + ->and($items[0]->couponPicture)->toBe($picture2) + ->and($items[1]->code)->toBe($printableCoupon1->code) + ->and($items[1]->couponName)->toBe($printableCoupon1->couponName) + ->and($items[1]->couponPicture)->toBe($picture1); + }); + + it('returns an empty list without error when there is no printable coupon', function () use ($createInput): void { + // GIVEN + $input = $createInput(); + $paginated = new Paginated(pagination: $input->pagination, items: [], total: 0); + + $repository = \Mockery::mock(PrintableCouponRepository::class); + $repository->shouldReceive('findAllAvailable')->once()->andReturn($paginated); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldNotReceive('findByCode'); + + $useCase = new ListPrintableCoupons($repository, $couponRepository); + + // WHEN + $output = $useCase->execute($input); + + // THEN + expect($output->printableCoupons->getItems())->toBe([]) + ->and($output->printableCoupons->getTotal())->toBe(0); + }); + + it('loads a shared coupon only once', function () use ( + $createInput, + $createPicture, + $createPrintableCoupon, + $createCoupon, + ): void { + // GIVEN + $input = $createInput(); + $picture = $createPicture('picture'); + $couponCode = new CouponCode('AAA01'); + $items = [$createPrintableCoupon($couponCode), $createPrintableCoupon($couponCode)]; + + $repository = \Mockery::mock(PrintableCouponRepository::class); + $repository->shouldReceive('findAllAvailable') + ->once() + ->andReturn(new Paginated(pagination: $input->pagination, items: $items, total: 2)); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode') + ->once() + ->andReturn($createCoupon($couponCode, $picture)); + + $useCase = new ListPrintableCoupons($repository, $couponRepository); + + // WHEN + $output = $useCase->execute($input); + + // THEN + expect($output->printableCoupons->getItems())->toHaveCount(2); + }); + + it('throws when the coupon of a printable coupon does not exist', function () use ($createInput, $createPrintableCoupon): void { + // GIVEN + $input = $createInput(); + $printable = $createPrintableCoupon(new CouponCode('BBB02')); + + $repository = \Mockery::mock(PrintableCouponRepository::class); + $repository->shouldReceive('findAllAvailable') + ->once() + ->andReturn(new Paginated(pagination: $input->pagination, items: [$printable], total: 1)); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->andReturn(null); + + $useCase = new ListPrintableCoupons($repository, $couponRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\LogicException::class); + }); +}); diff --git a/tests/Unit/Domain/Models/Entities/PrintableCouponTest.php b/tests/Unit/Domain/Models/Entities/PrintableCouponTest.php new file mode 100644 index 0000000..8fb94e5 --- /dev/null +++ b/tests/Unit/Domain/Models/Entities/PrintableCouponTest.php @@ -0,0 +1,122 @@ +campaignCode)->toBe($campaignCode) + ->and($printable->couponCode)->toBe($couponCode) + ->and($printable->couponName)->toBe($couponName) + ->and($printable->crmCustomerHashcode)->toBe($crmCustomerHashcode) + ->and($printable->distributableUntil)->toBe($distributableUntil); + }); + + it('generates a new code on each creation', function () use ( + $campaignCode, + $couponCode, + $couponName, + $crmCustomerHashcode, + $distributableUntil, + ): void { + // GIVEN / WHEN + $first = PrintableCoupon::create($campaignCode, $couponCode, $couponName, $crmCustomerHashcode, $distributableUntil); + $second = PrintableCoupon::create($campaignCode, $couponCode, $couponName, $crmCustomerHashcode, $distributableUntil); + + // THEN + expect($first->code)->toBeInstanceOf(PrintableCouponCode::class) + ->and($first->code->equals($second->code))->toBeFalse(); + }); + + it('exposes its code as identifier', function () use ( + $campaignCode, + $couponCode, + $couponName, + $crmCustomerHashcode, + $distributableUntil, + ): void { + // GIVEN + $printable = PrintableCoupon::create($campaignCode, $couponCode, $couponName, $crmCustomerHashcode, $distributableUntil); + + // WHEN / THEN + expect($printable->id())->toBe($printable->code); + }); + + it('has an update date equal to its creation date', function () use ( + $campaignCode, + $couponCode, + $couponName, + $crmCustomerHashcode, + $distributableUntil, + ): void { + // GIVEN / WHEN + $printable = PrintableCoupon::create($campaignCode, $couponCode, $couponName, $crmCustomerHashcode, $distributableUntil); + + // THEN + expect($printable->updatedAt)->toEqual($printable->createdAt()) + ->and($printable->updatedAt())->toBe($printable->updatedAt); + }); + + it('hydrates a printable coupon with the given values', function () use ( + $campaignCode, + $couponCode, + $couponName, + $crmCustomerHashcode, + $distributableUntil, + ): void { + // GIVEN + $code = PrintableCouponCode::generate(); + + // WHEN + $printable = PrintableCoupon::hydrate( + $code, + $campaignCode, + $couponCode, + $couponName, + $crmCustomerHashcode, + $distributableUntil, + ); + + // THEN + expect($printable->code)->toBe($code) + ->and($printable->campaignCode)->toBe($campaignCode) + ->and($printable->couponCode)->toBe($couponCode) + ->and($printable->couponName)->toBe($couponName) + ->and($printable->crmCustomerHashcode)->toBe($crmCustomerHashcode) + ->and($printable->distributableUntil)->toBe($distributableUntil) + ->and($printable->createdAt())->toEqual($code->toDateTime()); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/CrmHashcodeTest.php b/tests/Unit/Domain/Models/ValueObjects/CrmHashcodeTest.php new file mode 100644 index 0000000..7f515fc --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/CrmHashcodeTest.php @@ -0,0 +1,49 @@ +value)->toBe($value); + }); + + it('is equal to a hashcode with the same value', function () use ($value): void { + // GIVEN + $hashcode = new CrmHashcode($value); + $other = new CrmHashcode($value); + + // WHEN / THEN + expect($hashcode->equals($other))->toBeTrue(); + }); + + it('is not equal to a hashcode with a different value', function () use ($value): void { + // GIVEN + $hashcode = new CrmHashcode($value); + $other = new CrmHashcode(Uuid::uuid4()); + + // WHEN / THEN + expect($hashcode->equals($other))->toBeFalse(); + }); + + it('is not equal to a value that is not a hashcode', function () use ($value): void { + // GIVEN + $hashcode = new CrmHashcode($value); + + // WHEN / THEN + expect($hashcode->equals($value))->toBeFalse() + ->and($hashcode->equals(null))->toBeFalse(); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/MarketingDirectCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/MarketingDirectCodeTest.php new file mode 100644 index 0000000..0bc9d6d --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/MarketingDirectCodeTest.php @@ -0,0 +1,32 @@ +value)->toBe($expected); + })->with([ + 'one' => [1, '0000000000001'], + 'zero' => [0, '0000000000000'], + 'full size' => [5425003944130, '5425003944130'], + ]); + + it('rejects a negative or too large sequence', function (int $sequence): void { + $action = fn (): MarketingDirectCode => MarketingDirectCode::fromInt($sequence); + + expect($action)->toThrow(\DomainException::class); + })->with([ + 'negative' => [-1], + 'too large' => [10_000_000_000_000], + ]); + + it('converts back to its integer', function (): void { + expect(MarketingDirectCode::fromInt(42)->toInt())->toBe(42); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintableCouponCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintableCouponCodeTest.php new file mode 100644 index 0000000..ab786fb --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintableCouponCodeTest.php @@ -0,0 +1,28 @@ +toBeInstanceOf(PrintableCouponCode::class); + }); + + it('generates different ids on each call', function (): void { + // WHEN + $first = PrintableCouponCode::generate(); + $second = PrintableCouponCode::generate(); + + // THEN + expect((string) $first)->not->toBe((string) $second); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponIdTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponIdTest.php new file mode 100644 index 0000000..76c4f07 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponIdTest.php @@ -0,0 +1,28 @@ +toBeInstanceOf(PrintedCouponId::class); + }); + + it('generates different ids on each call', function (): void { + // WHEN + $first = PrintedCouponId::generate(); + $second = PrintedCouponId::generate(); + + // THEN + expect((string) $first)->not->toBe((string) $second); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponTest.php new file mode 100644 index 0000000..bae0a00 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/PrintedCouponTest.php @@ -0,0 +1,62 @@ +id)->toBe($id) + ->and($printed->printableCode)->toBe($printableCode) + ->and($printed->couponCode)->toBe($couponCode) + ->and($printed->campaignCode)->toBe($campaignCode) + ->and($printed->customer)->toBe($customer) + ->and($printed->printedAt)->toBe($printedAt); + }); + + it('derives the printing date from its id', function () use ($marketingCode, $printableCode, $couponCode, $campaignCode, $customer): void { + // GIVEN + $id = PrintedCouponId::generate(); + $printed = new PrintedCoupon( + $id, + $marketingCode, + $printableCode, + $couponCode, + $campaignCode, + $customer, + new \DateTimeImmutable('2020-01-01'), + ); + + // WHEN + $date = $printed->printedAt(); + + // THEN + expect($date)->toEqual($id->toDateTime()) + ->and($date)->not->toEqual($printed->printedAt); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/UnprintedCouponTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/UnprintedCouponTest.php new file mode 100644 index 0000000..03d5789 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/UnprintedCouponTest.php @@ -0,0 +1,69 @@ +printableCode)->toBe($printableCode) + ->and($unprinted->couponCode)->toBe($couponCode) + ->and($unprinted->campaignCode)->toBe($campaignCode) + ->and($unprinted->customer)->toBe($customer) + ->and($unprinted->unprintedReason)->toBe($reason); + }); + + it('keeps the reason as given without trimming it', function () use ($printableCode, $couponCode, $campaignCode, $customer): void { + // GIVEN + $reason = new UnprintedReason(' Printer failure '); + + // WHEN + $unprinted = new UnprintedCoupon($printableCode, $couponCode, $campaignCode, $customer, $reason); + + // THEN + expect($unprinted->unprintedReason)->toBe($reason); + }); + + it('rejects an empty reason', function (string $invalidReason) use ($printableCode, $couponCode, $campaignCode, $customer): void { + // GIVEN / WHEN + $action = fn (): UnprintedCoupon => new UnprintedCoupon( + $printableCode, + $couponCode, + $campaignCode, + $customer, + new UnprintedReason($invalidReason), + ); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Unprinted reason cannot be empty.'); + })->with([ + 'empty' => '', + 'spaces' => ' ', + 'tab' => "\t", + 'new lines' => "\n\r\n", + 'mixed spaces' => " \t\n ", + 'unicode spaces' => "\u{00A0}\u{2003}", + ]); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/UnprintedReasonTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/UnprintedReasonTest.php new file mode 100644 index 0000000..c4cf3f9 --- /dev/null +++ b/tests/Unit/Domain/Models/ValueObjects/PrintableCoupon/UnprintedReasonTest.php @@ -0,0 +1,33 @@ +toBeInstanceOf(UnprintedReason::class) + ->and($reason->value)->toBe($value); + }); + + it('rejects an empty or whitespace-only reason', function (string $invalidValue): void { + // WHEN + $action = fn (): UnprintedReason => new UnprintedReason($invalidValue); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Unprinted reason cannot be empty.'); + })->with([ + 'empty' => '', + 'spaces' => ' ', + ]); +}); From ec7fc6d719cfe1c281e6a347899a4230ae0d5240 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Wed, 14 Oct 2026 12:41:49 +0200 Subject: [PATCH 18/21] fix: :truck: move PeriodTest in global VO tests folder --- .../Models/ValueObjects/{RequestedCoupon => }/PeriodTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/Unit/Domain/Models/ValueObjects/{RequestedCoupon => }/PeriodTest.php (94%) diff --git a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php b/tests/Unit/Domain/Models/ValueObjects/PeriodTest.php similarity index 94% rename from tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php rename to tests/Unit/Domain/Models/ValueObjects/PeriodTest.php index 22e3449..444c536 100644 --- a/tests/Unit/Domain/Models/ValueObjects/RequestedCoupon/PeriodTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/PeriodTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\Unit\Domain\Models\ValueObjects\RequestedCoupon; +namespace Tests\Unit\Domain\Models\ValueObjects; use Domain\Models\ValueObjects\Period; use DateTime; From 3b50f284e8d26bc422fbb696349432aea92330de Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Wed, 14 Oct 2026 12:43:20 +0200 Subject: [PATCH 19/21] refactor: :recycle: clean createdAt & updatedAt methods and set them only on Find UseCases --- .../UseCases/Brand/Create/CreateBrand.php | 1 - .../UseCases/Brand/Create/Output.php | 1 - .../UseCases/Brand/Find/FindBrand.php | 1 + .../UseCases/Brand/Find/Output/BrandFound.php | 1 + .../Brand/Update/Output/BrandUpdated.php | 2 - .../UseCases/Brand/Update/UpdateBrand.php | 2 - .../Create/CreateCampaignRequest.php | 1 - .../CampaignRequest/Create/Output.php | 1 - .../Reject/Output/CampaignRequestRejected.php | 1 - .../Reject/RejectCampaignRequest.php | 1 - .../UseCases/Coupon/Create/CreateCoupon.php | 1 - .../UseCases/Coupon/Create/Output.php | 1 - .../Coupon/Update/Output/CouponUpdated.php | 2 - .../UseCases/Coupon/Update/UpdateCoupon.php | 2 - .../List/ListPrintableCoupons.php | 2 - .../List/PrintableCouponListItem.php | 2 - src/Domain/Models/Entities/Actor.php | 4 +- src/Domain/Models/Entities/Brand.php | 6 +- src/Domain/Models/Entities/Campaign.php | 7 +-- .../Models/Entities/CampaignRequest.php | 6 +- src/Domain/Models/Entities/Coupon.php | 6 +- .../Models/Entities/PrintableCoupon.php | 9 ++- src/Domain/Models/Entities/Stock.php | 4 +- .../UseCases/Brand/Update/UpdateBrandTest.php | 34 +++++----- .../Reject/RejectCampaignRequestTest.php | 6 +- .../Coupon/Create/CreateCouponTest.php | 3 +- .../Coupon/Update/UpdateCouponTest.php | 3 +- .../List/ListPrintableCouponTest.php | 2 - .../Unit/Domain/Models/Entities/ActorTest.php | 21 +------ .../Unit/Domain/Models/Entities/BrandTest.php | 43 ++++--------- .../Models/Entities/CampaignRequestTest.php | 39 ++---------- .../Domain/Models/Entities/CampaignTest.php | 38 ++--------- .../Domain/Models/Entities/CouponTest.php | 50 ++------------- .../Models/Entities/PrintableCouponTest.php | 63 ++++++++++--------- .../Unit/Domain/Models/Entities/StockTest.php | 32 +++------- 35 files changed, 109 insertions(+), 289 deletions(-) diff --git a/src/Application/UseCases/Brand/Create/CreateBrand.php b/src/Application/UseCases/Brand/Create/CreateBrand.php index eb98750..c653255 100644 --- a/src/Application/UseCases/Brand/Create/CreateBrand.php +++ b/src/Application/UseCases/Brand/Create/CreateBrand.php @@ -48,7 +48,6 @@ final readonly class CreateBrand implements UseCase, Transactional logo: $brand->logo, rcs: $brand->rcs, contact: $brand->contact, - createdAt: $brand->createdAt(), ); } } diff --git a/src/Application/UseCases/Brand/Create/Output.php b/src/Application/UseCases/Brand/Create/Output.php index fbf416f..e931182 100644 --- a/src/Application/UseCases/Brand/Create/Output.php +++ b/src/Application/UseCases/Brand/Create/Output.php @@ -19,7 +19,6 @@ final readonly class Output implements UseCaseOutput public PicturePath $logo, public Rcs $rcs, public Email $contact, - public \DateTimeInterface $createdAt, ) { } } diff --git a/src/Application/UseCases/Brand/Find/FindBrand.php b/src/Application/UseCases/Brand/Find/FindBrand.php index 90d0ccc..caf39b1 100644 --- a/src/Application/UseCases/Brand/Find/FindBrand.php +++ b/src/Application/UseCases/Brand/Find/FindBrand.php @@ -39,6 +39,7 @@ final readonly class FindBrand implements UseCase rcs: $brand->rcs, contact: $brand->contact, createdAt: $brand->createdAt(), + updatedAt: $brand->updatedAt(), ); } } diff --git a/src/Application/UseCases/Brand/Find/Output/BrandFound.php b/src/Application/UseCases/Brand/Find/Output/BrandFound.php index 2540207..60a04f5 100644 --- a/src/Application/UseCases/Brand/Find/Output/BrandFound.php +++ b/src/Application/UseCases/Brand/Find/Output/BrandFound.php @@ -19,6 +19,7 @@ final readonly class BrandFound implements Output public Rcs $rcs, public Email $contact, public \DateTimeInterface $createdAt, + public ?\DateTimeInterface $updatedAt, ) { } } diff --git a/src/Application/UseCases/Brand/Update/Output/BrandUpdated.php b/src/Application/UseCases/Brand/Update/Output/BrandUpdated.php index bd094ca..d5ce362 100644 --- a/src/Application/UseCases/Brand/Update/Output/BrandUpdated.php +++ b/src/Application/UseCases/Brand/Update/Output/BrandUpdated.php @@ -18,8 +18,6 @@ final readonly class BrandUpdated implements Output public PicturePath $logo, public Rcs $rcs, public Email $contact, - public \DateTimeInterface $createdAt, - public ?\DateTimeInterface $updatedAt = null, ) { } } diff --git a/src/Application/UseCases/Brand/Update/UpdateBrand.php b/src/Application/UseCases/Brand/Update/UpdateBrand.php index f894176..4bbda6a 100644 --- a/src/Application/UseCases/Brand/Update/UpdateBrand.php +++ b/src/Application/UseCases/Brand/Update/UpdateBrand.php @@ -56,8 +56,6 @@ final readonly class UpdateBrand implements UseCase, Transactional logo: $brand->logo, rcs: $brand->rcs, contact: $brand->contact, - createdAt: $brand->createdAt(), - updatedAt: $brand->updatedAt(), ); } } diff --git a/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php index b2d1812..1050bce 100644 --- a/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php +++ b/src/Application/UseCases/CampaignRequest/Create/CreateCampaignRequest.php @@ -48,7 +48,6 @@ final readonly class CreateCampaignRequest implements UseCase, Transactional campaignType: $campaignRequest->campaignType, requestedCoupon: $campaignRequest->requestedCoupon, status: $campaignRequest->status, - createdAt: $campaignRequest->createdAt(), ); } } diff --git a/src/Application/UseCases/CampaignRequest/Create/Output.php b/src/Application/UseCases/CampaignRequest/Create/Output.php index 29ffce6..9571784 100644 --- a/src/Application/UseCases/CampaignRequest/Create/Output.php +++ b/src/Application/UseCases/CampaignRequest/Create/Output.php @@ -23,7 +23,6 @@ final readonly class Output implements UseCaseOutput public CampaignRequestType $campaignType, public RequestedCoupon $requestedCoupon, public CampaignRequestStatus $status, - public \DateTimeInterface $createdAt, ) { } } diff --git a/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php b/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php index dcf3a78..2b2f595 100644 --- a/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php +++ b/src/Application/UseCases/CampaignRequest/Reject/Output/CampaignRequestRejected.php @@ -14,7 +14,6 @@ final readonly class CampaignRequestRejected implements Output public CampaignRequestId $id, public CampaignRequestStatus $status, public RejectionReason $rejectionReason, - public ?\DateTimeInterface $updatedAt = null ) { } } diff --git a/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php b/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php index ac717f3..7f79818 100644 --- a/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php +++ b/src/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequest.php @@ -41,7 +41,6 @@ final readonly class RejectCampaignRequest implements UseCase, Transactional id: $campaignRequest->id(), status: $campaignRequest->status, rejectionReason:$campaignRequest->rejectionReason(), - updatedAt: $campaignRequest->updatedAt(), ); } } diff --git a/src/Application/UseCases/Coupon/Create/CreateCoupon.php b/src/Application/UseCases/Coupon/Create/CreateCoupon.php index ffa6f15..0881f68 100644 --- a/src/Application/UseCases/Coupon/Create/CreateCoupon.php +++ b/src/Application/UseCases/Coupon/Create/CreateCoupon.php @@ -66,7 +66,6 @@ final readonly class CreateCoupon implements UseCase, Transactional createdBy: $coupon->createdBy, products: $coupon->products, movements: $coupon->movements, - createdAt: $coupon->createdAt(), availabilityCalendar: $coupon->availabilityCalendar, ); } diff --git a/src/Application/UseCases/Coupon/Create/Output.php b/src/Application/UseCases/Coupon/Create/Output.php index e359990..0a27b2e 100644 --- a/src/Application/UseCases/Coupon/Create/Output.php +++ b/src/Application/UseCases/Coupon/Create/Output.php @@ -36,7 +36,6 @@ final readonly class Output implements UseCaseOutput public ActorId $createdBy, public CouponProductArray $products, public CouponMovementArray $movements, - public \DateTimeInterface $createdAt, public ?CouponAvailabilityCalendar $availabilityCalendar = null, ) { } diff --git a/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php b/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php index 775ba23..968a15d 100644 --- a/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php +++ b/src/Application/UseCases/Coupon/Update/Output/CouponUpdated.php @@ -34,8 +34,6 @@ final readonly class CouponUpdated implements Output public ActorId $createdBy, public CouponProductArray $products, public CouponMovementArray $movements, - public \DateTimeInterface $createdAt, - public ?\DateTimeInterface $updatedAt, ) { } } diff --git a/src/Application/UseCases/Coupon/Update/UpdateCoupon.php b/src/Application/UseCases/Coupon/Update/UpdateCoupon.php index 425eb0e..5bed784 100644 --- a/src/Application/UseCases/Coupon/Update/UpdateCoupon.php +++ b/src/Application/UseCases/Coupon/Update/UpdateCoupon.php @@ -66,8 +66,6 @@ final readonly class UpdateCoupon implements UseCase, Transactional createdBy: $coupon->createdBy, products: $coupon->products, movements: $coupon->movements, - createdAt: $coupon->createdAt(), - updatedAt: $coupon->updatedAt(), ); } } diff --git a/src/Application/UseCases/PrintableCoupon/List/ListPrintableCoupons.php b/src/Application/UseCases/PrintableCoupon/List/ListPrintableCoupons.php index e6f8b1c..0769cf9 100644 --- a/src/Application/UseCases/PrintableCoupon/List/ListPrintableCoupons.php +++ b/src/Application/UseCases/PrintableCoupon/List/ListPrintableCoupons.php @@ -59,8 +59,6 @@ final readonly class ListPrintableCoupons implements UseCase couponName: $printableCoupon->couponName, couponPicture: $picturesByCouponCode[$printableCoupon->couponCode->value], distributableUntil: $printableCoupon->distributableUntil, - createdAt: $printableCoupon->createdAt(), - updatedAt: $printableCoupon->updatedAt(), ), $printableCoupons, ); diff --git a/src/Application/UseCases/PrintableCoupon/List/PrintableCouponListItem.php b/src/Application/UseCases/PrintableCoupon/List/PrintableCouponListItem.php index f7f50c6..31e3a64 100644 --- a/src/Application/UseCases/PrintableCoupon/List/PrintableCouponListItem.php +++ b/src/Application/UseCases/PrintableCoupon/List/PrintableCouponListItem.php @@ -17,8 +17,6 @@ final readonly class PrintableCouponListItem public CouponName $couponName, public CouponPicture $couponPicture, public \DateTimeInterface $distributableUntil, - public \DateTimeInterface $createdAt, - public \DateTimeInterface $updatedAt, ) { } } diff --git a/src/Domain/Models/Entities/Actor.php b/src/Domain/Models/Entities/Actor.php index fecbafa..3e3c7de 100644 --- a/src/Domain/Models/Entities/Actor.php +++ b/src/Domain/Models/Entities/Actor.php @@ -8,6 +8,7 @@ use Domain\Models\ValueObjects\Actor\ActorDisplayName; use Domain\Models\ValueObjects\Actor\ActorId; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; +use Shared\Helpers\DatesHelper; /** * @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Actor\ActorId> @@ -39,12 +40,11 @@ final readonly class Actor implements AggregateRoot, HasTimestampIdentifier public static function hydrate( ActorId $id, ActorDisplayName $displayName, - ?\DateTimeInterface $updatedAt = null, ): self { return new self( id: $id, displayName: $displayName, - updatedAt: $updatedAt, + updatedAt: DatesHelper::now(), ); } } diff --git a/src/Domain/Models/Entities/Brand.php b/src/Domain/Models/Entities/Brand.php index 00cd6bc..945c139 100644 --- a/src/Domain/Models/Entities/Brand.php +++ b/src/Domain/Models/Entities/Brand.php @@ -51,7 +51,6 @@ final readonly class Brand implements AggregateRoot, HasTimestampIdentifier PicturePath $logo, Rcs $rcs, Email $contact, - ?\DateTimeInterface $updatedAt = null, ): Brand { return new self( id: BrandId::generate(), @@ -60,7 +59,7 @@ final readonly class Brand implements AggregateRoot, HasTimestampIdentifier logo: $logo, rcs: $rcs, contact: $contact, - updatedAt: $updatedAt, + updatedAt: null, ); } @@ -71,7 +70,6 @@ final readonly class Brand implements AggregateRoot, HasTimestampIdentifier PicturePath $logo, Rcs $rcs, Email $contact, - ?\DateTimeInterface $updatedAt = null, ): self { return new self( id: $id, @@ -80,7 +78,7 @@ final readonly class Brand implements AggregateRoot, HasTimestampIdentifier logo: $logo, rcs: $rcs, contact: $contact, - updatedAt: $updatedAt, + updatedAt: DatesHelper::now(), ); } diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php index 9c099d9..f61db2a 100644 --- a/src/Domain/Models/Entities/Campaign.php +++ b/src/Domain/Models/Entities/Campaign.php @@ -12,6 +12,7 @@ use Domain\Models\ValueObjects\Campaign\CampaignId; use Domain\Models\ValueObjects\Campaign\CampaignName; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; +use Shared\Helpers\DatesHelper; /** * @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Campaign\CampaignId> @@ -54,7 +55,6 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier CampaignCouponArray $coupons, BrandCode $brandCode, ActorId $actorId, - ?\DateTimeInterface $updatedAt = null, ): Campaign { return new self( id: CampaignId::generate(), @@ -65,7 +65,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier coupons: $coupons, brandCode: $brandCode, actorId: $actorId, - updatedAt: $updatedAt, + updatedAt: null, ); } @@ -78,7 +78,6 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier CampaignCouponArray $coupons, BrandCode $brandCode, ActorId $actorId, - ?\DateTimeInterface $updatedAt = null, ): self { return new self( id: $id, @@ -89,7 +88,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier coupons: $coupons, brandCode: $brandCode, actorId: $actorId, - updatedAt: $updatedAt, + updatedAt: DatesHelper::now(), ); } } diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index 56acaf6..c873da6 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -62,7 +62,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent Applicant $applicant, CampaignRequestType $campaignType, RequestedCoupon $requestedCoupon, - ?\DateTimeInterface $updatedAt = null, ): self { return new self( id: CampaignRequestId::generate(), @@ -72,7 +71,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent campaignType: $campaignType, requestedCoupon: $requestedCoupon, status: CampaignRequestStatus::CREATED, - updatedAt: $updatedAt, + updatedAt: null, ); } @@ -85,7 +84,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent RequestedCoupon $requestedCoupon, CampaignRequestStatus $status, ?RejectionReason $rejectionReason = null, - ?\DateTimeInterface $updatedAt = null, ): self { return new self( id: $id, @@ -96,7 +94,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent requestedCoupon: $requestedCoupon, status: $status, rejectionReason: $rejectionReason, - updatedAt: $updatedAt, + updatedAt: DatesHelper::now(), ); } diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index 213eab0..5fe8923 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -86,7 +86,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier CouponProductArray $products, CouponMovementArray $movements, ?CouponAvailabilityCalendar $availabilityCalendar = null, - ?\DateTimeInterface $updatedAt = null, ): Coupon { return new self( id: CouponId::generate(), @@ -103,7 +102,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier products: $products, movements: $movements, availabilityCalendar: $availabilityCalendar, - updatedAt: $updatedAt, + updatedAt: null, ); } @@ -122,7 +121,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier CouponProductArray $products, CouponMovementArray $movements, ?CouponAvailabilityCalendar $availabilityCalendar = null, - ?\DateTimeInterface $updatedAt = null, ): self { return new self( id: $id, @@ -139,7 +137,7 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier products: $products, movements: $movements, availabilityCalendar: $availabilityCalendar, - updatedAt: $updatedAt, + updatedAt: DatesHelper::now(), ); } diff --git a/src/Domain/Models/Entities/PrintableCoupon.php b/src/Domain/Models/Entities/PrintableCoupon.php index 23e8a1a..0337ebd 100644 --- a/src/Domain/Models/Entities/PrintableCoupon.php +++ b/src/Domain/Models/Entities/PrintableCoupon.php @@ -25,7 +25,7 @@ final readonly class PrintableCoupon implements AggregateRoot, HasTimestampIdent public CouponName $couponName, public CrmHashcode $crmCustomerHashcode, public \DateTimeInterface $distributableUntil, - public \DateTimeInterface $updatedAt, + public ?\DateTimeInterface $updatedAt = null, ) { } @@ -39,7 +39,7 @@ final readonly class PrintableCoupon implements AggregateRoot, HasTimestampIdent return $this->code->toDateTime(); } - public function updatedAt(): \DateTimeInterface + public function updatedAt(): ?\DateTimeInterface { return $this->updatedAt; } @@ -51,15 +51,14 @@ final readonly class PrintableCoupon implements AggregateRoot, HasTimestampIdent CrmHashcode $crmCustomerHashcode, \DateTimeInterface $distributableUntil, ): self { - $code = PrintableCouponCode::generate(); return new self( - code: $code, + code: PrintableCouponCode::generate(), campaignCode: $campaignCode, couponCode: $couponCode, couponName: $couponName, crmCustomerHashcode: $crmCustomerHashcode, distributableUntil: $distributableUntil, - updatedAt: $code->toDateTime(), + updatedAt: null, ); } diff --git a/src/Domain/Models/Entities/Stock.php b/src/Domain/Models/Entities/Stock.php index dbd4307..6343baf 100644 --- a/src/Domain/Models/Entities/Stock.php +++ b/src/Domain/Models/Entities/Stock.php @@ -11,6 +11,7 @@ use Domain\Models\ValueObjects\Stock\StockAmount; use Domain\Models\ValueObjects\Stock\StockId; use Shared\DDD\Models\AggregateRoot; use Shared\DDD\Models\HasTimestampIdentifier; +use Shared\Helpers\DatesHelper; /** * @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Stock\StockId> @@ -57,7 +58,6 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier CampaignCode $campaignCode, CouponCode $couponCode, array $stockAdjustment = [], - ?\DateTimeInterface $updatedAt = null, ): self { return new self( id: $id, @@ -65,7 +65,7 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier campaignCode: $campaignCode, couponCode: $couponCode, stockAdjustment: $stockAdjustment, - updatedAt: $updatedAt, + updatedAt: DatesHelper::now(), ); } } diff --git a/tests/Unit/Application/UseCases/Brand/Update/UpdateBrandTest.php b/tests/Unit/Application/UseCases/Brand/Update/UpdateBrandTest.php index c2bbeb4..3558819 100644 --- a/tests/Unit/Application/UseCases/Brand/Update/UpdateBrandTest.php +++ b/tests/Unit/Application/UseCases/Brand/Update/UpdateBrandTest.php @@ -21,16 +21,16 @@ use Shared\ValueObjects\Rcs; covers(UpdateBrand::class, Input::class, BrandUpdated::class, BrandNotFound::class); -$existingBrand = static fn (BrandCode $code): Brand => Brand::hydrate( - BrandId::generate(), - $code, - new BrandName('Name'), - new PicturePath('files/picture-old.jpg'), - new Rcs('RCS PARIS 111 222 333'), - new Email('contact-old@example.com'), -); +describe('update brand use case', function (): void { + $existingBrand = static fn (BrandCode $code): Brand => Brand::hydrate( + BrandId::generate(), + $code, + new BrandName('Name'), + new PicturePath('files/picture-old.jpg'), + new Rcs('RCS PARIS 111 222 333'), + new Email('contact-old@example.com'), + ); -describe('update brand use case', function () use ($existingBrand): void { it('updates every provided field', function () use ($existingBrand): void { // GIVEN $code = new BrandCode('AAA01'); @@ -47,8 +47,7 @@ describe('update brand use case', function () use ($existingBrand): void { && $saved->rcs === $newRcs && $saved->contact === $newContact && $saved->code === $brand->code - && $saved->name === $brand->name - && $saved->updatedAt instanceof \DateTimeInterface, + && $saved->name === $brand->name, )); $useCase = new UpdateBrand($repository); @@ -58,8 +57,10 @@ describe('update brand use case', function () use ($existingBrand): void { // THEN expect($result)->toBeInstanceOf(BrandUpdated::class) - ->and($result->code)->toBe($brand->code) - ->and($result->updatedAt)->not->toBeNull(); + ->and($result->logo)->toBe($newLogo) + ->and($result->rcs)->toBe($newRcs) + ->and($result->contact)->toBe($newContact) + ->and($result->code)->toBe($brand->code); }); it('updates only the contact and leaves logo/rcs untouched', function () use ($existingBrand): void { @@ -76,8 +77,7 @@ describe('update brand use case', function () use ($existingBrand): void { && $saved->logo === $brand->logo && $saved->rcs === $brand->rcs && $saved->code === $brand->code - && $saved->name === $brand->name - && $saved->updatedAt instanceof \DateTimeInterface, + && $saved->name === $brand->name, )); $useCase = new UpdateBrand($repository); @@ -87,8 +87,8 @@ describe('update brand use case', function () use ($existingBrand): void { // THEN expect($result)->toBeInstanceOf(BrandUpdated::class) - ->and($result->code)->toBe($brand->code) - ->and($result->updatedAt)->not->toBeNull(); + ->and($result->contact)->toBe($newContact) + ->and($result->code)->toBe($brand->code); }); }); diff --git a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php index b4de50e..60af83d 100644 --- a/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php +++ b/tests/Unit/Application/UseCases/CampaignRequest/Reject/RejectCampaignRequestTest.php @@ -93,8 +93,7 @@ describe('reject campaign request use case', function (): void { $repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest); $repository->shouldReceive('save')->once()->with(\Mockery::on( fn (CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED - && $saved->rejectionReason === $reason - && $saved->updatedAt instanceof \DateTimeInterface, + && $saved->rejectionReason === $reason, )); $useCase = new RejectCampaignRequest($repository); @@ -106,8 +105,7 @@ describe('reject campaign request use case', function (): void { expect($result)->toBeInstanceOf(CampaignRequestRejected::class) ->and($result->id)->toBe($id) ->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'); + ->and($result->rejectionReason)->toBe($reason); }); it('returns CampaignRequestNotFound when the id is not found', function (): void { diff --git a/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php index e550347..5e71918 100644 --- a/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Create/CreateCouponTest.php @@ -100,8 +100,7 @@ describe('create coupon use case', function (): void { ->and($result->createdBy)->toEqual($input->createdBy) ->and($result->products)->toEqual($input->products) ->and($result->movements)->toEqual($input->movements) - ->and($result->availabilityCalendar)->toBeNull() - ->and($result->createdAt)->toBeInstanceOf(\DateTimeInterface::class); + ->and($result->availabilityCalendar)->toBeNull(); }); it('throws when the brand does not exist', function () use ($makeInput): void { diff --git a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php index 21aa859..fd45a6c 100644 --- a/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Update/UpdateCouponTest.php @@ -110,8 +110,7 @@ describe('update coupon use case', function (): void { // THEN expect($result)->toBeInstanceOf(CouponUpdated::class) ->and($result->code)->toBe($coupon->code) - ->and($result->picture)->toBe($newPicture) - ->and($result->updatedAt)->not->toBeNull(); + ->and($result->picture)->toBe($newPicture); }); it('returns CouponNotFound when the coupon does not exist', function (): void { diff --git a/tests/Unit/Application/UseCases/PrintableCoupon/List/ListPrintableCouponTest.php b/tests/Unit/Application/UseCases/PrintableCoupon/List/ListPrintableCouponTest.php index f91bb83..59e6113 100644 --- a/tests/Unit/Application/UseCases/PrintableCoupon/List/ListPrintableCouponTest.php +++ b/tests/Unit/Application/UseCases/PrintableCoupon/List/ListPrintableCouponTest.php @@ -23,7 +23,6 @@ use Domain\Models\ValueObjects\Coupon\CouponMovementArray; use Domain\Models\ValueObjects\Coupon\CouponName; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Models\ValueObjects\Coupon\CouponProductArray; -use Domain\Models\ValueObjects\Coupon\CouponQuantity; use Domain\Models\ValueObjects\Coupon\CouponValidityInformation; use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray; use Domain\Models\ValueObjects\CrmHashcode; @@ -108,7 +107,6 @@ describe('list printable coupons use case', function (): void { movements: new CouponMovementArray([ new CouponMovement(1000) ]), - quantity: new CouponQuantity(1000), ); it('returns the printable coupons with the picture of their coupon', function () use ( diff --git a/tests/Unit/Domain/Models/Entities/ActorTest.php b/tests/Unit/Domain/Models/Entities/ActorTest.php index 65942dc..f49881e 100644 --- a/tests/Unit/Domain/Models/Entities/ActorTest.php +++ b/tests/Unit/Domain/Models/Entities/ActorTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; +use DateTimeInterface; use Domain\Models\Entities\Actor; use Domain\Models\ValueObjects\Actor\ActorDisplayName; use Domain\Models\ValueObjects\Actor\ActorId; @@ -27,24 +28,8 @@ describe('actor', function (): void { // THEN expect($actor->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 () use ($displayName): void { - // GIVEN - $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); - - // WHEN - $actor = Actor::hydrate( - ActorId::generate(), - $displayName, - $updatedAt, - ); - - // THEN - expect($actor->updatedAt)->toBe($updatedAt) - ->and($actor->updatedAt())->toBe($updatedAt); + ->and($actor->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($actor->updatedAt())->toBeInstanceOf(DateTimeInterface::class); }); it('returns the creation date extracted from the id', function () use ($displayName): void { diff --git a/tests/Unit/Domain/Models/Entities/BrandTest.php b/tests/Unit/Domain/Models/Entities/BrandTest.php index cb80b26..57d03fb 100644 --- a/tests/Unit/Domain/Models/Entities/BrandTest.php +++ b/tests/Unit/Domain/Models/Entities/BrandTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; +use DateTimeInterface; use Domain\Models\Entities\Brand; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Brand\BrandId; @@ -48,29 +49,8 @@ describe('brand', function (): void { ->and($brand->logo)->toBe($logo) ->and($brand->rcs)->toBe($rcs) ->and($brand->contact)->toBe($contact) - ->and($brand->updatedAt)->toBeNull() - ->and($brand->updatedAt())->toBeNull(); - }); - - it('creates a brand with an explicit updatedAt date', function () use ($code, $name, $logo, $rcs, $contact): void { - // GIVEN - $id = BrandId::generate(); - $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); - - // WHEN - $brand = Brand::hydrate( - $id, - $code, - $name, - $logo, - $rcs, - $contact, - $updatedAt, - ); - - // THEN - expect($brand->updatedAt)->toBe($updatedAt) - ->and($brand->updatedAt())->toBe($updatedAt); + ->and($brand->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($brand->updatedAt())->toBeInstanceOf(DateTimeInterface::class); }); it('creates a new brand with a freshly generated id', function () use ($code, $name, $logo, $rcs, $contact): void { @@ -90,7 +70,7 @@ describe('brand', function (): void { ->and($brand->logo)->toBe($logo) ->and($brand->rcs)->toBe($rcs) ->and($brand->contact)->toBe($contact) - ->and($brand->updatedAt)->toBeNull() + ->and($brand->createdAt())->toBeInstanceOf(DateTimeInterface::class) ->and($brand->updatedAt())->toBeNull(); }); @@ -140,8 +120,9 @@ describe('brand', function (): void { ->and($brand->logo)->toBe($logoNew) ->and($brand->rcs)->toBe($rcs) ->and($brand->contact)->toBe($contact) - ->and($brand->updatedAt)->not->toBeNull() - ->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00'); + ->and($brand->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($brand->updatedAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($brand->updatedAt()->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00'); }); it('updates a brand rcs and sets updatedAt timestamp', function () use ($code, $name, $logo, $contact): void { @@ -169,8 +150,9 @@ describe('brand', function (): void { ->and($brand->logo)->toBe($logo) ->and($brand->rcs)->toBe($rcsNew) ->and($brand->contact)->toBe($contact) - ->and($brand->updatedAt)->not->toBeNull() - ->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 14:30:00'); + ->and($brand->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($brand->updatedAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($brand->updatedAt()->format('Y-m-d H:i:s'))->toBe('2026-06-15 14:30:00'); }); it('updates a brand contact and sets updatedAt timestamp', function () use ($code, $name, $logo, $rcs): void { @@ -198,8 +180,9 @@ describe('brand', function (): void { ->and($brand->logo)->toBe($logo) ->and($brand->rcs)->toBe($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'); + ->and($brand->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($brand->updatedAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($brand->updatedAt()->format('Y-m-d H:i:s'))->toBe('2026-06-15 16:45:00'); }); it('returns the creation date extracted from the id', function () use ($code, $name, $logo, $rcs, $contact): void { diff --git a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php index a47b3f2..9637393 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignRequestTest.php @@ -6,6 +6,7 @@ namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; use DateTime; +use DateTimeInterface; use Domain\Models\Entities\CampaignRequest; use Domain\Models\ValueObjects\Brand\BrandCode; use Domain\Models\ValueObjects\Campaign\CampaignName; @@ -100,37 +101,8 @@ describe('campaign request', function (): void { ->and($campaignRequest->campaignType)->toBe($campaignType) ->and($campaignRequest->requestedCoupon)->toBe($requestedCoupon) ->and($campaignRequest->status)->toBe($status) - ->and($campaignRequest->updatedAt)->toBeNull() - ->and($campaignRequest->updatedAt())->toBeNull(); - }); - - it('hydrates a campaign request with an explicit updatedAt date', function () use ( - $code, - $name, - $applicant, - $campaignType, - $requestedCoupon, - $status, - ): void { - // GIVEN - $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); - - // WHEN - $campaignRequest = CampaignRequest::hydrate( - CampaignRequestId::generate(), - $code, - $name, - $applicant, - $campaignType, - $requestedCoupon, - $status, - null, - $updatedAt, - ); - - // THEN - expect($campaignRequest->updatedAt)->toBe($updatedAt) - ->and($campaignRequest->updatedAt())->toBe($updatedAt); + ->and($campaignRequest->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($campaignRequest->updatedAt())->toBeInstanceOf(DateTimeInterface::class); }); it('hydrates a rejected campaign request with a rejection reason', function () use ( @@ -246,8 +218,9 @@ describe('campaign request', function (): void { ->and($rejected->applicant)->toBe($campaignRequest->applicant) ->and($rejected->campaignType)->toBe($campaignRequest->campaignType) ->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'); + ->and($rejected->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($rejected->updatedAt())->toBeInstanceOf(DateTimeInterface::class) + ->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 () use ( diff --git a/tests/Unit/Domain/Models/Entities/CampaignTest.php b/tests/Unit/Domain/Models/Entities/CampaignTest.php index e48d8a5..ad796d5 100644 --- a/tests/Unit/Domain/Models/Entities/CampaignTest.php +++ b/tests/Unit/Domain/Models/Entities/CampaignTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; +use DateTimeInterface; use Domain\Models\Entities\Campaign; use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; @@ -69,39 +70,8 @@ describe('campaign', function (): void { ->and($campaign->printAllAtOnce)->toBe($printAllAtOnce) ->and($campaign->coupons)->toBe($coupons) ->and($campaign->brandCode)->toBe($brandCode) - ->and($campaign->updatedAt)->toBeNull() - ->and($campaign->updatedAt())->toBeNull(); - }); - - it('creates a coupon with an explicit updatedAt date', function () use ( - $code, - $name, - $publication, - $printAllAtOnce, - $coupons, - $brandCode, - $actorId, - ): void { - // GIVEN - $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, - $actorId, - $updatedAt - ); - - // THEN - expect($campaign->updatedAt)->toBe($updatedAt) - ->and($campaign->updatedAt())->toBe($updatedAt); + ->and($campaign->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($campaign->updatedAt())->toBeInstanceOf(DateTimeInterface::class); }); it('creates a new campaign with a freshly generated id', function () use ( @@ -132,7 +102,7 @@ describe('campaign', function (): void { ->and($campaign->printAllAtOnce)->toBe($printAllAtOnce) ->and($campaign->coupons)->toBe($coupons) ->and($campaign->brandCode)->toBe($brandCode) - ->and($campaign->updatedAt)->toBeNull() + ->and($campaign->createdAt())->toBeInstanceOf(DateTimeInterface::class) ->and($campaign->updatedAt())->toBeNull(); }); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index b1940a0..91d1aa7 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -6,6 +6,7 @@ namespace Tests\Unit\Domain\Models\Entities; use Carbon\CarbonImmutable; use DateTime; +use DateTimeInterface; use Domain\Models\Entities\Coupon; use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Brand\BrandCode; @@ -125,8 +126,8 @@ describe('coupon', function (): void { ->and($coupon->products)->toBe($products) ->and($coupon->availabilityCalendar)->toBe($availabilityCalendar) ->and($coupon->movements)->toBe($movements) - ->and($coupon->updatedAt)->toBeNull() - ->and($coupon->updatedAt())->toBeNull(); + ->and($coupon->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($coupon->updatedAt())->toBeInstanceOf(DateTimeInterface::class); }); it('hydrates a coupon without availability calendar', function () use ( @@ -280,49 +281,6 @@ describe('coupon', function (): void { ))->toThrow(\DomainException::class, 'Value of CouponLegalInformationArray must be CouponLegalInformation instances.'); }); - it('hydrates a coupon with an explicit updatedAt date', function () use ( - $code, - $name, - $amount, - $picture, - $diffusion, - $validityInformations, - $legalInformations, - $processingCode, - $brandCode, - $createdBy, - $products, - $movements, - $availabilityCalendar, - ): void { - // GIVEN - $id = CouponId::generate(); - $updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC')); - - // WHEN - $coupon = Coupon::hydrate( - $id, - code: $code, - name: $name, - amount: $amount, - picture: $picture, - diffusion: $diffusion, - validityInformations: $validityInformations, - legalInformations: $legalInformations, - processingCode: $processingCode, - brandCode: $brandCode, - createdBy: $createdBy, - products: $products, - movements: $movements, - availabilityCalendar: $availabilityCalendar, - updatedAt: $updatedAt, - ); - - // THEN - expect($coupon->updatedAt)->toBe($updatedAt) - ->and($coupon->updatedAt())->toBe($updatedAt); - }); - it('creates a new coupon with a freshly generated id', function () use ( $code, $name, @@ -370,7 +328,7 @@ describe('coupon', function (): void { ->and($coupon->products)->toBe($products) ->and($coupon->movements)->toBe($movements) ->and($coupon->availabilityCalendar)->toBe($availabilityCalendar) - ->and($coupon->updatedAt)->toBeNull() + ->and($coupon->createdAt())->toBeInstanceOf(DateTimeInterface::class) ->and($coupon->updatedAt())->toBeNull(); }); diff --git a/tests/Unit/Domain/Models/Entities/PrintableCouponTest.php b/tests/Unit/Domain/Models/Entities/PrintableCouponTest.php index 8fb94e5..85dbf7f 100644 --- a/tests/Unit/Domain/Models/Entities/PrintableCouponTest.php +++ b/tests/Unit/Domain/Models/Entities/PrintableCouponTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; +use DateTimeInterface; use Domain\Models\Entities\PrintableCoupon; use Domain\Models\ValueObjects\Campaign\CampaignCode; use Domain\Models\ValueObjects\Coupon\CouponCode; @@ -42,7 +43,9 @@ describe('printable coupon', function (): void { ->and($printable->couponCode)->toBe($couponCode) ->and($printable->couponName)->toBe($couponName) ->and($printable->crmCustomerHashcode)->toBe($crmCustomerHashcode) - ->and($printable->distributableUntil)->toBe($distributableUntil); + ->and($printable->distributableUntil)->toBe($distributableUntil) + ->and($printable->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($printable->updatedAt())->toBeNull(); }); it('generates a new code on each creation', function () use ( @@ -61,35 +64,6 @@ describe('printable coupon', function (): void { ->and($first->code->equals($second->code))->toBeFalse(); }); - it('exposes its code as identifier', function () use ( - $campaignCode, - $couponCode, - $couponName, - $crmCustomerHashcode, - $distributableUntil, - ): void { - // GIVEN - $printable = PrintableCoupon::create($campaignCode, $couponCode, $couponName, $crmCustomerHashcode, $distributableUntil); - - // WHEN / THEN - expect($printable->id())->toBe($printable->code); - }); - - it('has an update date equal to its creation date', function () use ( - $campaignCode, - $couponCode, - $couponName, - $crmCustomerHashcode, - $distributableUntil, - ): void { - // GIVEN / WHEN - $printable = PrintableCoupon::create($campaignCode, $couponCode, $couponName, $crmCustomerHashcode, $distributableUntil); - - // THEN - expect($printable->updatedAt)->toEqual($printable->createdAt()) - ->and($printable->updatedAt())->toBe($printable->updatedAt); - }); - it('hydrates a printable coupon with the given values', function () use ( $campaignCode, $couponCode, @@ -117,6 +91,33 @@ describe('printable coupon', function (): void { ->and($printable->couponName)->toBe($couponName) ->and($printable->crmCustomerHashcode)->toBe($crmCustomerHashcode) ->and($printable->distributableUntil)->toBe($distributableUntil) - ->and($printable->createdAt())->toEqual($code->toDateTime()); + ->and($printable->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($printable->updatedAt())->toBeInstanceOf(DateTimeInterface::class); + }); + + it('returns the creation date extracted from the id', function () use ( + $campaignCode, + $couponCode, + $couponName, + $crmCustomerHashcode, + $distributableUntil, + ): void { + // GIVEN + $expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC')); + $code = new PrintableCouponCode(Uuid::uuid7($expected)); + $printableCoupon = PrintableCoupon::hydrate( + $code, + $campaignCode, + $couponCode, + $couponName, + $crmCustomerHashcode, + $distributableUntil, + ); + + // WHEN + $result = $printableCoupon->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/StockTest.php b/tests/Unit/Domain/Models/Entities/StockTest.php index 6d4d469..11659f6 100644 --- a/tests/Unit/Domain/Models/Entities/StockTest.php +++ b/tests/Unit/Domain/Models/Entities/StockTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Tests\Unit\Domain\Models\Entities; +use DateTimeInterface; use Domain\Models\Entities\Stock; use Domain\Models\ValueObjects\Actor\ActorId; use Domain\Models\ValueObjects\Campaign\CampaignCode; @@ -40,8 +41,8 @@ describe('stock', function (): void { ->and($stock->campaignCode)->toBe($campaignCode) ->and($stock->couponCode)->toBe($couponCode) ->and($stock->stockAdjustment)->toBe($stockAdjustment) - ->and($stock->updatedAt)->toBeNull() - ->and($stock->updatedAt())->toBeNull(); + ->and($stock->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($stock->updatedAt())->toBeInstanceOf(DateTimeInterface::class); }); it('hydrates a stock with several stock adjustments', function () use ($stockAmount, $campaignCode, $couponCode): void { @@ -67,8 +68,8 @@ describe('stock', function (): void { ->and($stock->campaignCode)->toBe($campaignCode) ->and($stock->couponCode)->toBe($couponCode) ->and($stock->stockAdjustment)->toBe($severalAdjustments) - ->and($stock->updatedAt)->toBeNull() - ->and($stock->updatedAt())->toBeNull(); + ->and($stock->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($stock->updatedAt())->toBeInstanceOf(DateTimeInterface::class); }); it('hydrates a stock without stock adjustement', function () use ($stockAmount, $campaignCode, $couponCode): void { @@ -89,8 +90,8 @@ describe('stock', function (): void { ->and($stock->campaignCode)->toBe($campaignCode) ->and($stock->couponCode)->toBe($couponCode) ->and($stock->stockAdjustment)->toBe([]) - ->and($stock->updatedAt)->toBeNull() - ->and($stock->updatedAt())->toBeNull(); + ->and($stock->createdAt())->toBeInstanceOf(DateTimeInterface::class) + ->and($stock->updatedAt())->toBeInstanceOf(DateTimeInterface::class); }); it('rejects a stock with a non-stock-adjustment element', function () use ($stockAmount, $campaignCode, $couponCode): void { @@ -108,25 +109,6 @@ describe('stock', function (): void { ))->toThrow(\DomainException::class, 'Stock adjustment must be StockAdjustment instances.'); }); - it('hydrates a stock with an explicit updatedAt date', function () use ($stockAmount, $campaignCode, $couponCode, $stockAdjustment): void { - // GIVEN - $updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC')); - - // WHEN - $stock = Stock::hydrate( - StockId::generate(), - $stockAmount, - $campaignCode, - $couponCode, - $stockAdjustment, - $updatedAt, - ); - - // THEN - expect($stock->updatedAt)->toBe($updatedAt) - ->and($stock->updatedAt())->toBe($updatedAt); - }); - it('returns the creation date extracted from the id', function () use ($stockAmount, $campaignCode, $couponCode, $stockAdjustment): void { // GIVEN $expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC')); From 89854cba4db34dde2908907386c6aa0c4bc55b07 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Wed, 14 Oct 2026 17:21:14 +0200 Subject: [PATCH 20/21] feat: :sparkles: add use case create printable coupon + tests --- .../Create/CreatePrintableCoupon.php | 100 ++++ .../UseCases/PrintableCoupon/Create/Input.php | 20 + .../PrintableCoupon/Create/Output.php | 23 + src/Domain/Models/Entities/Coupon.php | 34 ++ .../ValueObjects/Campaign/CampaignCode.php | 4 +- .../Coupon/CouponAvailabilityCalendar.php | 11 +- src/Domain/Models/ValueObjects/Period.php | 21 +- src/Domain/Models/ValueObjects/PrintRules.php | 26 +- .../Ports/Repositories/CampaignRepository.php | 6 + .../PrintableCouponRepository.php | 14 + .../Create/CreatePrintableCouponTest.php | 429 ++++++++++++++++++ .../Models/ValueObjects/PrintRulesTest.php | 53 ++- 12 files changed, 710 insertions(+), 31 deletions(-) create mode 100644 src/Application/UseCases/PrintableCoupon/Create/CreatePrintableCoupon.php create mode 100644 src/Application/UseCases/PrintableCoupon/Create/Input.php create mode 100644 src/Application/UseCases/PrintableCoupon/Create/Output.php create mode 100644 tests/Unit/Application/UseCases/PrintableCoupon/Create/CreatePrintableCouponTest.php diff --git a/src/Application/UseCases/PrintableCoupon/Create/CreatePrintableCoupon.php b/src/Application/UseCases/PrintableCoupon/Create/CreatePrintableCoupon.php new file mode 100644 index 0000000..76e7d1a --- /dev/null +++ b/src/Application/UseCases/PrintableCoupon/Create/CreatePrintableCoupon.php @@ -0,0 +1,100 @@ + */ +final readonly class CreatePrintableCoupon implements UseCase, Transactional +{ + public function __construct( + private PrintableCouponRepository $printableCouponRepository, + private CouponRepository $couponRepository, + private CampaignRepository $campaignRepository, + ) { + } + + #[\Override] + public function execute(BaseInput $input): Output + { + Assert::isInstanceOf($input, Input::class); + + $campaign = $this->campaignRepository->findByCode($input->campaignCode); + if (! $campaign instanceof Campaign) { + throw new \DomainException(sprintf('Campaign from code "%s" not found.', $input->campaignCode->value)); + } + if (! $campaign->publication) { + throw new \DomainException('Campaign must be published.'); + } + + $coupon = $this->couponRepository->findByCode($input->couponCode); + if (! $coupon instanceof Coupon) { + throw new \DomainException(sprintf('Coupon code "%s" not found.', $input->couponCode->value)); + } + + $campaignCoupon = $this->campaignRepository->findCampaignCoupon($campaign->code, $coupon->code); + if (! $campaignCoupon instanceof CampaignCoupon) { + throw new \DomainException('Coupon must be associated to the associated campaign coupon.'); + } + + $period = $coupon->getActivePeriod(); + + $this->checkPrintRulesFromCampaignCoupon($campaign, $campaignCoupon, $coupon, $input->crmCustomerHashcode); + + $printableCoupon = PrintableCoupon::create( + campaignCode: $campaign->code, + couponCode: $coupon->code, + couponName: $coupon->name, + crmCustomerHashcode: $input->crmCustomerHashcode, + distributableUntil: $period->end, + ); + + $this->printableCouponRepository->save($printableCoupon); + + return new Output( + campaignCode: $printableCoupon->campaignCode, + couponCode: $printableCoupon->couponCode, + couponName: $printableCoupon->couponName, + crmCustomerHashcode: $printableCoupon->crmCustomerHashcode, + distributableUntil: $printableCoupon->distributableUntil, + ); + } + + public function checkPrintRulesFromCampaignCoupon( + Campaign $campaign, + CampaignCoupon $campaignCoupon, + Coupon $coupon, + CrmHashcode $crmCustomerHashcode, + ): void { + if ($campaignCoupon->printRules instanceof PrintRules) { + $countPrintedCoupon = $this->printableCouponRepository->countPrintedCouponForCustomer( + $campaign->code, + $coupon->code, + $crmCustomerHashcode, + ); + $campaignCoupon->printRules->hasPrintLimitNotReached($countPrintedCoupon); + + $lastPrintedCoupon = $this->printableCouponRepository->findLastPrintedCouponForCustomer( + $campaign->code, + $coupon->code, + $crmCustomerHashcode, + ); + $diff = $lastPrintedCoupon->printedAt()->diff(new \DateTime()); + $campaignCoupon->printRules->hasReprintPeriodExpired($diff->days); + } + } +} diff --git a/src/Application/UseCases/PrintableCoupon/Create/Input.php b/src/Application/UseCases/PrintableCoupon/Create/Input.php new file mode 100644 index 0000000..209b011 --- /dev/null +++ b/src/Application/UseCases/PrintableCoupon/Create/Input.php @@ -0,0 +1,20 @@ +validityInformations, $this->legalInformations, ); + $this->validateCalendarPeriodsWithinDiffusionPeriod(); } public function id(): CouponId @@ -182,4 +184,36 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier updatedAt: DatesHelper::now(), ); } + + public function getActivePeriod(): Period + { + $currentDate = new \DateTime(); + $diffusionPeriod = $this->diffusion->period; + $period = $this->availabilityCalendar?->isActiveAt($currentDate) + ? $this->availabilityCalendar->getCurrentPeriod($currentDate) + ?? $diffusionPeriod + : $diffusionPeriod; + + if (! $period->hasStarted()) { + throw new \DomainException('The printing period has not yet begun.'); + } + + if (! $period->hasEnded()) { + throw new \DomainException('The printing period has expired.'); + } + + return $period; + } + + public function validateCalendarPeriodsWithinDiffusionPeriod(): void + { + if ($this->availabilityCalendar instanceof CouponAvailabilityCalendar) { + $diffusionPeriod = $this->diffusion->period; + foreach ($this->availabilityCalendar->periods as $calendarPeriod) { + if (! $diffusionPeriod->inRange($calendarPeriod->start) || ! $diffusionPeriod->inRange($calendarPeriod->end)) { + throw new \DomainException('Calendar period must be in diffusion period.'); + } + } + } + } } diff --git a/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php b/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php index afe38a9..33a855b 100644 --- a/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php +++ b/src/Domain/Models/ValueObjects/Campaign/CampaignCode.php @@ -7,9 +7,9 @@ namespace Domain\Models\ValueObjects\Campaign; final readonly class CampaignCode { public function __construct( - public string $code, + public string $value, ) { - if (mb_trim($this->code) === '') { + if (mb_trim($this->value) === '') { throw new \DomainException('Campaign code cannot be empty.'); } } diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php b/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php index 90de3ff..a1f5ad4 100644 --- a/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php +++ b/src/Domain/Models/ValueObjects/Coupon/CouponAvailabilityCalendar.php @@ -10,8 +10,10 @@ use DateTime; final readonly class CouponAvailabilityCalendar implements Equatable { + /** + * @param array $periods + */ public function __construct( - /** @var Period[] */ public array $periods, ) { if ($this->periods === []) { @@ -27,7 +29,7 @@ final readonly class CouponAvailabilityCalendar implements Equatable public function isActiveAt(DateTime $in): bool { - return array_any($this->periods, fn ($period): bool => $in >= $period->start && $in <= $period->end); + return array_any($this->periods, fn ($period): bool => $period->inRange($in)); } public function beginsAt(): DateTime @@ -40,6 +42,11 @@ final readonly class CouponAvailabilityCalendar implements Equatable return max(array_map(fn (Period $period): DateTime => $period->end, $this->periods)); } + public function getCurrentPeriod(DateTime $in): ?Period + { + return array_first(array_filter($this->periods, fn ($period): bool => $period->inRange($in))); + } + #[\Override] public function equals(mixed $other): bool { diff --git a/src/Domain/Models/ValueObjects/Period.php b/src/Domain/Models/ValueObjects/Period.php index c189079..4a68005 100644 --- a/src/Domain/Models/ValueObjects/Period.php +++ b/src/Domain/Models/ValueObjects/Period.php @@ -4,16 +4,31 @@ declare(strict_types=1); namespace Domain\Models\ValueObjects; -use DateTime; +use Shared\Helpers\DatesHelper; final readonly class Period { public function __construct( - public DateTime $start, - public DateTime $end, + public \DateTime $start, + public \DateTime $end, ) { if ($this->end < $this->start) { throw new \DomainException('Period end date cannot be before its start date.'); } } + + public function hasStarted(): bool + { + return $this->start <= DatesHelper::now(); + } + + public function hasEnded(): bool + { + return $this->end >= DatesHelper::now(); + } + + public function inRange(\DateTime $date): bool + { + return $date >= $this->start && $date <= $this->end; + } } diff --git a/src/Domain/Models/ValueObjects/PrintRules.php b/src/Domain/Models/ValueObjects/PrintRules.php index f915889..ec81030 100644 --- a/src/Domain/Models/ValueObjects/PrintRules.php +++ b/src/Domain/Models/ValueObjects/PrintRules.php @@ -11,15 +11,33 @@ final readonly class PrintRules 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.'); + throw new \DomainException('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.'); + throw new \DomainException('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.'); + if (null !== $this->cooldownDays && $this->cooldownDays < 30) { + throw new \DomainException('Cooldown days must be greater than 30.'); + } + + if ($this->maxPerCustomer && $this->cooldownDays) { + throw new \DomainException('No more than one print rule.'); + } + } + + public function hasPrintLimitNotReached(int $countPrintedCoupon): void + { + if ($this->maxPerCustomer && $this->maxPerCustomer < $countPrintedCoupon) { + throw new \DomainException('Maximum number of prints reached.'); + } + } + + public function hasReprintPeriodExpired(int $diffDays): void + { + if ($this->cooldownDays && $diffDays < $this->cooldownDays) { + throw new \DomainException('Reprint period has not expired.'); } } } diff --git a/src/Domain/Ports/Repositories/CampaignRepository.php b/src/Domain/Ports/Repositories/CampaignRepository.php index 4618258..98445e7 100644 --- a/src/Domain/Ports/Repositories/CampaignRepository.php +++ b/src/Domain/Ports/Repositories/CampaignRepository.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace Domain\Ports\Repositories; use Domain\Models\Entities\Campaign; +use Domain\Models\ValueObjects\Campaign\CampaignCode; +use Domain\Models\ValueObjects\Campaign\CampaignCoupon; use Domain\Models\ValueObjects\Coupon\CouponCode; use Shared\DDD\Ports\Repository; @@ -19,4 +21,8 @@ interface CampaignRepository extends Repository * @return array */ public function findByCouponCode(CouponCode $couponCode): array; + + public function findByCode(CampaignCode $code): ?Campaign; + + public function findCampaignCoupon(CampaignCode $campaignCode, CouponCode $couponCode): ?CampaignCoupon; } diff --git a/src/Domain/Ports/Repositories/PrintableCouponRepository.php b/src/Domain/Ports/Repositories/PrintableCouponRepository.php index 4ead8e6..b53d27b 100644 --- a/src/Domain/Ports/Repositories/PrintableCouponRepository.php +++ b/src/Domain/Ports/Repositories/PrintableCouponRepository.php @@ -6,8 +6,10 @@ namespace Domain\Ports\Repositories; use Domain\Models\Entities\PrintableCoupon; use Domain\Models\ValueObjects\Campaign\CampaignCode; +use Domain\Models\ValueObjects\Coupon\CouponCode; use Domain\Models\ValueObjects\CrmHashcode; use Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponSortCriteria; +use Domain\Models\ValueObjects\PrintableCoupon\PrintedCoupon; use Shared\DDD\Ports\Repository; use Shared\ValueObjects\Pagination\Paginated; use Shared\ValueObjects\Pagination\Pagination; @@ -27,5 +29,17 @@ interface PrintableCouponRepository extends Repository ?PrintableCouponSortCriteria $sortCriteria = null, ): Paginated; + public function countPrintedCouponForCustomer( + CampaignCode $campaignCode, + CouponCode $couponCode, + CrmHashcode $crmCustomerHashcode, + ): int; + + public function findLastPrintedCouponForCustomer( + CampaignCode $campaignCode, + CouponCode $couponCode, + CrmHashcode $crmCustomerHashcode, + ): PrintedCoupon; + public function save(PrintableCoupon $printableCoupon): void; } diff --git a/tests/Unit/Application/UseCases/PrintableCoupon/Create/CreatePrintableCouponTest.php b/tests/Unit/Application/UseCases/PrintableCoupon/Create/CreatePrintableCouponTest.php new file mode 100644 index 0000000..3cf50bb --- /dev/null +++ b/tests/Unit/Application/UseCases/PrintableCoupon/Create/CreatePrintableCouponTest.php @@ -0,0 +1,429 @@ +modify('-1 month'); + $couponDiffusionEnd = new DateTime()->modify('+1 month'); + $makeCoupon = fn (array $overrides = []): Coupon => Coupon::create( + code: $couponCode, + name: $couponName, + amount: new MoneyAmount(new Money(Currency::EUR, 100)), + picture: new CouponPicture( + new PicturePath('files/product.jpg'), + new PicturePath('files/thumbnail.jpg'), + new PicturePath('files/mobile_thumbnail.jpg'), + ), + diffusion: new CouponDiffusion( + new Period( + $overrides['diffusionStart'] ?? $couponDiffusionStart, + $overrides['diffusionEnd'] ?? $couponDiffusionEnd, + ), + ), + validityInformations: new CouponValidityInformationArray([ + new CouponValidityInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE, + ), + ]), + legalInformations: new CouponLegalInformationArray([ + new CouponLegalInformation( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', + Country::FRANCE, + ), + ]), + processingCode: new ProcessingCodeArray([ + new ProcessingCode('5425003944130', Country::FRANCE), + ]), + brandCode: $brandCode, + createdBy: ActorId::generate(), + products: new CouponProductArray([ + new IncoCode(35784) , + new IncoCode(15522), + ]), + movements: new CouponMovementArray([ + new CouponMovement(1000) + ]), + availabilityCalendar: $overrides['availabilityCalendar'] ?? null, + ); + $printRules = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null): ?PrintRules => ($maxPerCustomer || $cooldownDays) ? new PrintRules( + maxPerCustomer: $maxPerCustomer, + cooldownDays: $cooldownDays, + ) : null; + $campaignCoupon = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null): CampaignCoupon => new CampaignCoupon( + $couponCode, + $printRules($maxPerCustomer, $cooldownDays), + ); + $makeCampaign = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null, bool $published = true): Campaign => Campaign::create( + code: $campaignCode, + name: new CampaignName('Campaign name'), + publication: $published, + printAllAtOnce: true, + coupons: new CampaignCouponArray([ + $campaignCoupon($maxPerCustomer, $cooldownDays), + ]), + brandCode: $brandCode, + actorId: ActorId::generate(), + ); + $printedCoupon = new PrintedCoupon( + id: PrintedCouponId::generate(), + marketingCode: new MarketingDirectCode('0000000000001'), + printableCode: PrintableCouponCode::generate(), + couponCode: $couponCode, + campaignCode: $campaignCode, + customer: $crmCustomerHashcode, + printedAt: DatesHelper::now(), + ); + $makeInput = fn (): Input => new Input( + campaignCode: $campaignCode, + couponCode: $couponCode, + crmCustomerHashcode: $crmCustomerHashcode, + ); + + it('creates a printable coupon', function () use ($makeInput, $crmCustomerHashcode, $makeCampaign, $makeCoupon, $campaignCoupon): void { + // GIVEN + $input = $makeInput(); + $campaign = $makeCampaign(); + $coupon = $makeCoupon(); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign); + $campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code) + ->andReturn($campaignCoupon()); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + + $printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class); + $printableCouponRepository->shouldReceive('save')->once()->with(\Mockery::type(PrintableCoupon::class)); + + $useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(Output::class) + ->and($result->campaignCode)->toEqual($campaign->code) + ->and($result->couponCode)->toEqual($coupon->code) + ->and($result->crmCustomerHashcode)->toEqual($crmCustomerHashcode) + ->and($result->distributableUntil)->toEqual($coupon->diffusion->period->end); + }); + + it('rejects a printable coupon when campaign not found', function () use ($makeInput, $makeCampaign): void { + // GIVEN + $input = $makeInput(); + $campaign = $makeCampaign(); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn(null); + + $couponRepository = \Mockery::mock(CouponRepository::class); + + $printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class); + + $useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, sprintf('Campaign from code "%s" not found.', $input->campaignCode->value)); + }); + + it('rejects a printable coupon when campaign is not published', function () use ($makeInput, $makeCampaign): void { + // GIVEN + $input = $makeInput(); + $campaign = $makeCampaign(published: false); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign); + + $couponRepository = \Mockery::mock(CouponRepository::class); + + $printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class); + + $useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Campaign must be published.'); + }); + + it('rejects a printable coupon when coupon is not found', function () use ($makeInput, $makeCampaign, $makeCoupon): void { + // GIVEN + $input = $makeInput(); + $campaign = $makeCampaign(); + $coupon = $makeCoupon(); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn(null); + + $printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class); + + $useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, sprintf('Coupon code "%s" not found.', $input->couponCode->value)); + }); + + it('rejects a printable coupon when campaign coupon is not found', function () use ($makeInput, $makeCampaign, $makeCoupon): void { + // GIVEN + $input = $makeInput(); + $campaign = $makeCampaign(); + $coupon = $makeCoupon(); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign); + $campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code) + ->andReturn(null); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + + $printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class); + + $useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Coupon must be associated to the associated campaign coupon.'); + }); + + it('rejects a printable coupon when outside of diffusion start', function () use ($makeInput, $makeCampaign, $makeCoupon, $campaignCoupon): void { + // GIVEN + $input = $makeInput(); + $campaign = $makeCampaign(); + $coupon = $makeCoupon(['diffusionStart' => new DateTime()->modify('+1 day')]); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign); + $campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code) + ->andReturn($campaignCoupon()); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + + $printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class); + + $useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, 'The printing period has not yet begun.'); + }); + + it('rejects a printable coupon when outside of diffusion end', function () use ($makeInput, $makeCampaign, $makeCoupon, $campaignCoupon): void { + // GIVEN + $input = $makeInput(); + $campaign = $makeCampaign(); + $coupon = $makeCoupon(['diffusionEnd' => new DateTime()->modify('-1 day')]); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign); + $campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code) + ->andReturn($campaignCoupon()); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + + $printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class); + + $useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, 'The printing period has expired.'); + }); + + it('creates a printable coupon when calendar period is inside diffusion period', function () use ( + $makeInput, + $crmCustomerHashcode, + $makeCampaign, + $makeCoupon, + $campaignCoupon, + ): void { + // GIVEN + $calendarStart = new DateTime()->modify('-1 day'); + $calendarEnd = new DateTime()->modify('+1 day'); + $input = $makeInput(); + $campaign = $makeCampaign(); + $coupon = $makeCoupon([ + 'availabilityCalendar' => new CouponAvailabilityCalendar([ + new Period( + start: $calendarStart, + end: $calendarEnd, + ), + ]), + ]); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign); + $campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code) + ->andReturn($campaignCoupon()); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + + $printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class); + $printableCouponRepository->shouldReceive('save')->once()->with(\Mockery::type(PrintableCoupon::class)); + + $useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository); + + // WHEN + $result = $useCase->execute($input); + + // THEN + expect($result)->toBeInstanceOf(Output::class) + ->and($result->campaignCode)->toEqual($campaign->code) + ->and($result->crmCustomerHashcode)->toEqual($crmCustomerHashcode) + ->and($result->distributableUntil)->toEqual($calendarEnd); + }); + + it('rejects a printable coupon when maximum number of prints reached', function () use ( + $makeInput, + $crmCustomerHashcode, + $makeCampaign, + $makeCoupon, + $campaignCoupon, + ): void { + // GIVEN + $input = $makeInput(); + $campaign = $makeCampaign(maxPerCustomer: 1); + $coupon = $makeCoupon(); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign); + $campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code) + ->andReturn($campaignCoupon(maxPerCustomer: 1)); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + + $printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class); + $printableCouponRepository->shouldReceive('countPrintedCouponForCustomer')->once()->with( + $campaign->code, + $coupon->code, + $crmCustomerHashcode, + )->andReturn(5); + + $useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Maximum number of prints reached.'); + }); + + it('rejects a printable coupon when reprint period has expired', function () use ( + $makeInput, + $crmCustomerHashcode, + $makeCampaign, + $makeCoupon, + $campaignCoupon, + $printedCoupon, + ): void { + // GIVEN + $input = $makeInput(); + $campaign = $makeCampaign(cooldownDays: 30); + $coupon = $makeCoupon(); + + $campaignRepository = \Mockery::mock(CampaignRepository::class); + $campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign); + $campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code) + ->andReturn($campaignCoupon(cooldownDays: 30)); + + $couponRepository = \Mockery::mock(CouponRepository::class); + $couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon); + + $printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class); + $printableCouponRepository->shouldReceive('countPrintedCouponForCustomer')->once()->with( + $campaign->code, + $coupon->code, + $crmCustomerHashcode, + )->andReturn(5); + $printableCouponRepository->shouldReceive('findLastPrintedCouponForCustomer')->once()->with( + $campaign->code, + $coupon->code, + $crmCustomerHashcode, + )->andReturn($printedCoupon); + + $useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository); + + // WHEN + $action = fn (): Output => $useCase->execute($input); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Reprint period has not expired.'); + }); +}); diff --git a/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php b/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php index e84fd8e..7a7ca4b 100644 --- a/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/PrintRulesTest.php @@ -16,17 +16,16 @@ describe('print rules', function (): void { $minCooldownDays = 1; $zeroCooldownDays = 0; - it('creates print rules with both fields set', function () use ($maxPerCustomer, $cooldownDays): void { + it('rejects print rules with both fields set', function () use ($maxPerCustomer, $cooldownDays): void { // GIVEN / WHEN - $printRules = new PrintRules( + $printRules = fn (): PrintRules => new PrintRules( maxPerCustomer: $maxPerCustomer, cooldownDays: $cooldownDays ); // THEN - expect($printRules)->toBeInstanceOf(PrintRules::class) - ->and($printRules->maxPerCustomer)->toBe($maxPerCustomer) - ->and($printRules->cooldownDays)->toBe($cooldownDays); + expect($printRules) + ->toThrow(\DomainException::class, 'No more than one print rule.'); }); it('creates print rules with only maxPerCustomer set', function () use ($maxPerCustomer): void { @@ -54,32 +53,46 @@ describe('print rules', 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 () use ($minMaxPerCustomer, $minCooldownDays): void { - // GIVEN / WHEN - $printRules = new PrintRules( - maxPerCustomer: $minMaxPerCustomer, - cooldownDays: $minCooldownDays - ); - - // THEN - expect($printRules->maxPerCustomer)->toBe($minMaxPerCustomer) - ->and($printRules->cooldownDays)->toBe($minCooldownDays); + ->toThrow(\DomainException::class, 'PrintRules require at least a max per customer or a cooldown days.'); }); it('rejects a maxPerCustomer equal to zero', function () use ($zeroMaxPerCustomer): void { // GIVEN / WHEN / THEN expect(fn (): PrintRules => new PrintRules( maxPerCustomer: $zeroMaxPerCustomer - ))->toThrow(\InvalidArgumentException::class, 'Max per customer must be greater than 0.'); + ))->toThrow(\DomainException::class, 'Max per customer must be greater than 0.'); }); it('rejects a cooldownDays equal to zero', function () use ($zeroCooldownDays): void { // GIVEN / WHEN / THEN expect(fn (): PrintRules => new PrintRules( cooldownDays: $zeroCooldownDays - ))->toThrow(\InvalidArgumentException::class, 'Cooldown days must be greater than 0.'); + ))->toThrow(\DomainException::class, 'Cooldown days must be greater than 30.'); + }); + + it('rejects print rule when max per customer limit has reached', function () use ($maxPerCustomer, $minMaxPerCustomer): void { + // GIVEN + $printRules = new PrintRules( + maxPerCustomer: $minMaxPerCustomer + ); + + // WHEN + $action = fn () => $printRules->hasPrintLimitNotReached($maxPerCustomer); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Maximum number of prints reached.'); + }); + + it('rejects print rule when cooldown days period has expired', function () use ($cooldownDays, $minCooldownDays): void { + // GIVEN + $printRules = new PrintRules( + cooldownDays: $cooldownDays + ); + + // WHEN + $action = fn () => $printRules->hasReprintPeriodExpired($minCooldownDays); + + // THEN + expect($action)->toThrow(\DomainException::class, 'Reprint period has not expired.'); }); }); From 937b516242391fbd670038d5ea7ef49d1e74c8c3 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Thu, 15 Oct 2026 15:40:13 +0200 Subject: [PATCH 21/21] =?UTF-8?q?fix:=20=F0=9F=9A=A8=20resolve=20linters?= =?UTF-8?q?=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UseCases/PrintableCoupon/List/ListPrintableCoupons.php | 7 +++++-- src/Domain/Models/Entities/Brand.php | 1 - src/Domain/Models/Entities/Campaign.php | 1 - src/Domain/Models/Entities/CampaignRequest.php | 1 - src/Domain/Models/Entities/Coupon.php | 1 - src/Domain/Models/Entities/PrintableCoupon.php | 1 - .../Application/UseCases/Coupon/Find/FindCouponTest.php | 3 ++- tests/Unit/Domain/Models/Entities/CouponTest.php | 2 +- .../Models/ValueObjects/Campaign/CampaignCodeTest.php | 2 +- 9 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Application/UseCases/PrintableCoupon/List/ListPrintableCoupons.php b/src/Application/UseCases/PrintableCoupon/List/ListPrintableCoupons.php index 0769cf9..694dce6 100644 --- a/src/Application/UseCases/PrintableCoupon/List/ListPrintableCoupons.php +++ b/src/Application/UseCases/PrintableCoupon/List/ListPrintableCoupons.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Application\UseCases\PrintableCoupon\List; +use Domain\Models\Entities\Coupon; use Domain\Models\Entities\PrintableCoupon; use Domain\Models\ValueObjects\Coupon\CouponPicture; use Domain\Ports\Repositories\CouponRepository; @@ -46,8 +47,10 @@ final readonly class ListPrintableCoupons implements UseCase continue; } - $coupon = $this->couponRepository->findByCode($couponCode) - ?? throw new \LogicException(sprintf('Coupon "%s" referenced by a printable coupon does not exist.', $couponCode->value)); + $coupon = $this->couponRepository->findByCode($couponCode); + if (!$coupon instanceof Coupon) { + throw new \LogicException(sprintf('Coupon "%s" referenced by a printable coupon does not exist.', $couponCode->value)); + } $picturesByCouponCode[$couponCode->value] = $coupon->picture; } diff --git a/src/Domain/Models/Entities/Brand.php b/src/Domain/Models/Entities/Brand.php index 945c139..52a9f65 100644 --- a/src/Domain/Models/Entities/Brand.php +++ b/src/Domain/Models/Entities/Brand.php @@ -59,7 +59,6 @@ final readonly class Brand implements AggregateRoot, HasTimestampIdentifier logo: $logo, rcs: $rcs, contact: $contact, - updatedAt: null, ); } diff --git a/src/Domain/Models/Entities/Campaign.php b/src/Domain/Models/Entities/Campaign.php index f61db2a..9e23da9 100644 --- a/src/Domain/Models/Entities/Campaign.php +++ b/src/Domain/Models/Entities/Campaign.php @@ -65,7 +65,6 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier coupons: $coupons, brandCode: $brandCode, actorId: $actorId, - updatedAt: null, ); } diff --git a/src/Domain/Models/Entities/CampaignRequest.php b/src/Domain/Models/Entities/CampaignRequest.php index c873da6..3fad464 100644 --- a/src/Domain/Models/Entities/CampaignRequest.php +++ b/src/Domain/Models/Entities/CampaignRequest.php @@ -71,7 +71,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent campaignType: $campaignType, requestedCoupon: $requestedCoupon, status: CampaignRequestStatus::CREATED, - updatedAt: null, ); } diff --git a/src/Domain/Models/Entities/Coupon.php b/src/Domain/Models/Entities/Coupon.php index f190dde..ca23ff2 100644 --- a/src/Domain/Models/Entities/Coupon.php +++ b/src/Domain/Models/Entities/Coupon.php @@ -104,7 +104,6 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier products: $products, movements: $movements, availabilityCalendar: $availabilityCalendar, - updatedAt: null, ); } diff --git a/src/Domain/Models/Entities/PrintableCoupon.php b/src/Domain/Models/Entities/PrintableCoupon.php index 0337ebd..8ad7c4e 100644 --- a/src/Domain/Models/Entities/PrintableCoupon.php +++ b/src/Domain/Models/Entities/PrintableCoupon.php @@ -58,7 +58,6 @@ final readonly class PrintableCoupon implements AggregateRoot, HasTimestampIdent couponName: $couponName, crmCustomerHashcode: $crmCustomerHashcode, distributableUntil: $distributableUntil, - updatedAt: null, ); } diff --git a/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php b/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php index eb64946..4ef0ce1 100644 --- a/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php +++ b/tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php @@ -8,6 +8,7 @@ use Application\UseCases\Coupon\Find\FindCoupon; use Application\UseCases\Coupon\Find\Input; use Application\UseCases\Coupon\Find\Output\CouponFound; use Application\UseCases\Coupon\Find\Output\CouponNotFound; +use Application\UseCases\Coupon\Find\Output\Output; use DateTime; use Domain\Models\Entities\Brand; use Domain\Models\Entities\Campaign; @@ -180,7 +181,7 @@ describe('find coupon use case', function (): void { $useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository); // WHEN / THEN - expect(fn () => $useCase->execute(new Input(code: $coupon->code))) + expect(fn (): Output => $useCase->execute(new Input(code: $coupon->code))) ->toThrow(\DomainException::class); }); diff --git a/tests/Unit/Domain/Models/Entities/CouponTest.php b/tests/Unit/Domain/Models/Entities/CouponTest.php index 91d1aa7..19cdfc3 100644 --- a/tests/Unit/Domain/Models/Entities/CouponTest.php +++ b/tests/Unit/Domain/Models/Entities/CouponTest.php @@ -45,7 +45,7 @@ describe('coupon', function (): void { new PicturePath('files/thumbnail.jpg'), new PicturePath('files/mobileThumbnail.jpg'), ); - $diffusion = new CouponDiffusion(new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'))); + $diffusion = new CouponDiffusion(new Period(new DateTime('2026-01-01'), new DateTime('2026-12-31'))); $validityInformations = new CouponValidityInformationArray([ new CouponValidityInformation( 'Lorem ipsum dolor sit amet consectetur adipiscing elit.', diff --git a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php index 59bc1b3..39f6ca9 100644 --- a/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php +++ b/tests/Unit/Domain/Models/ValueObjects/Campaign/CampaignCodeTest.php @@ -17,7 +17,7 @@ describe('campaign code', function (): void { $name = new CampaignCode($code); // THEN - expect($name->code)->toBe($code); + expect($name->value)->toBe($code); }); it('rejects invalid campaign code', function () use ($emptyCode): void {