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,