Merge branch 'common-sprint-2' into 'refonte'
Common sprint 2 See merge request ldc-web/projets/mesbr!39
This commit is contained in:
commit
a2d211814b
121 changed files with 5786 additions and 91 deletions
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
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;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\DDD\UseCases\Transactional;
|
||||
use Shared\DDD\UseCases\UseCase;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements UseCase<\Application\UseCases\CampaignRequest\Create\Input, \Application\UseCases\CampaignRequest\Create\Output> */
|
||||
final readonly class CreateCampaignRequest implements UseCase, Transactional
|
||||
{
|
||||
public function __construct(
|
||||
private CampaignRequestRepository $campaignRequestRepository,
|
||||
private BrandRepository $brandRepository
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
if (!$this->brandRepository->existsByCode($input->brandCode)) {
|
||||
throw new BrandNotFoundException($input->brandCode);
|
||||
}
|
||||
|
||||
$campaignRequest = CampaignRequest::create(
|
||||
brandCode: $input->brandCode,
|
||||
name: $input->name,
|
||||
applicant: $input->applicant,
|
||||
campaignType: $input->campaignType,
|
||||
requestedCoupon: $input->requestedCoupon,
|
||||
);
|
||||
|
||||
$this->campaignRequestRepository->save($campaignRequest);
|
||||
|
||||
return new Output(
|
||||
id: $campaignRequest->id(),
|
||||
brandCode: $campaignRequest->brandCode,
|
||||
name: $campaignRequest->name,
|
||||
applicant: $campaignRequest->applicant,
|
||||
campaignType: $campaignRequest->campaignType,
|
||||
requestedCoupon: $campaignRequest->requestedCoupon,
|
||||
status: $campaignRequest->status,
|
||||
createdAt: $campaignRequest->createdAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
24
src/Application/UseCases/CampaignRequest/Create/Input.php
Normal file
24
src/Application/UseCases/CampaignRequest/Create/Input.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Create;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Shared\DDD\UseCases\Input as UseCaseInput;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
|
||||
final readonly class Input implements UseCaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public BrandCode $brandCode,
|
||||
public CampaignName $name,
|
||||
public Applicant $applicant,
|
||||
public CampaignRequestType $campaignType,
|
||||
public RequestedCoupon $requestedCoupon,
|
||||
) {
|
||||
}
|
||||
}
|
||||
29
src/Application/UseCases/CampaignRequest/Create/Output.php
Normal file
29
src/Application/UseCases/CampaignRequest/Create/Output.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Create;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
|
||||
final readonly class Output implements UseCaseOutput
|
||||
{
|
||||
public function __construct(
|
||||
public CampaignRequestId $id,
|
||||
public BrandCode $brandCode,
|
||||
public CampaignName $name,
|
||||
public Applicant $applicant,
|
||||
public CampaignRequestType $campaignType,
|
||||
public RequestedCoupon $requestedCoupon,
|
||||
public CampaignRequestStatus $status,
|
||||
public \DateTimeInterface $createdAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use Domain\Ports\Repositories\CampaignRequestRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\DDD\UseCases\UseCase;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements UseCase<\Application\UseCases\CampaignRequest\Find\Input, \Application\UseCases\CampaignRequest\Find\Output\Output> */
|
||||
final readonly class FindCampaignRequest implements UseCase
|
||||
{
|
||||
public function __construct(
|
||||
private CampaignRequestRepository $repository,
|
||||
private BrandRepository $brandRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$campaignRequest = $this->repository->findById($input->id);
|
||||
|
||||
if (! $campaignRequest instanceof CampaignRequest) {
|
||||
return new CampaignRequestNotFound();
|
||||
}
|
||||
|
||||
$brand = $this->brandRepository->findByCode($campaignRequest->brandCode);
|
||||
|
||||
if (! $brand instanceof Brand) {
|
||||
throw new BrandNotFoundException($campaignRequest->brandCode);
|
||||
}
|
||||
|
||||
return new CampaignRequestFound(
|
||||
brand: $brand,
|
||||
name: $campaignRequest->name,
|
||||
applicant: $campaignRequest->applicant,
|
||||
campaignType: $campaignRequest->campaignType,
|
||||
requestedCoupon: $campaignRequest->requestedCoupon,
|
||||
status: $campaignRequest->status,
|
||||
rejectionReason: $campaignRequest->rejectionReason,
|
||||
createdAt: $campaignRequest->createdAt(),
|
||||
updatedAt: $campaignRequest->updatedAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/Application/UseCases/CampaignRequest/Find/Input.php
Normal file
16
src/Application/UseCases/CampaignRequest/Find/Input.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Find;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
use Shared\DDD\UseCases\Input as UseCaseInput;
|
||||
|
||||
final readonly class Input implements UseCaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public CampaignRequestId $id,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Find\Output;
|
||||
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
|
||||
final readonly class CampaignRequestFound implements Output
|
||||
{
|
||||
public function __construct(
|
||||
public Brand $brand,
|
||||
public CampaignName $name,
|
||||
public Applicant $applicant,
|
||||
public CampaignRequestType $campaignType,
|
||||
public RequestedCoupon $requestedCoupon,
|
||||
public CampaignRequestStatus $status,
|
||||
public ?RejectionReason $rejectionReason,
|
||||
public \DateTimeInterface $createdAt,
|
||||
public ?\DateTimeInterface $updatedAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Find\Output;
|
||||
|
||||
final readonly class CampaignRequestNotFound implements Output
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Find\Output;
|
||||
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
|
||||
interface Output extends UseCaseOutput
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\List;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
|
||||
final readonly class CampaignRequestListItem
|
||||
{
|
||||
public function __construct(
|
||||
public BrandCode $brandCode,
|
||||
public CampaignName $name,
|
||||
public Applicant $applicant,
|
||||
public CampaignRequestType $campaignType,
|
||||
public CampaignRequestStatus $status
|
||||
) {
|
||||
}
|
||||
}
|
||||
20
src/Application/UseCases/CampaignRequest/List/Input.php
Normal file
20
src/Application/UseCases/CampaignRequest/List/Input.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\List;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestFilters;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria;
|
||||
use Shared\DDD\UseCases\Input as UseCaseInput;
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
|
||||
final readonly class Input implements UseCaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public Pagination $pagination,
|
||||
public ?CampaignRequestSortCriteria $sort = null,
|
||||
public ?CampaignRequestFilters $filters = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\List;
|
||||
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Ports\Repositories\CampaignRequestRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\DDD\UseCases\UseCase;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements UseCase<\Application\UseCases\CampaignRequest\List\Input, \Application\UseCases\CampaignRequest\List\Output> */
|
||||
final readonly class ListCampaignsRequest implements UseCase
|
||||
{
|
||||
public function __construct(
|
||||
private CampaignRequestRepository $repository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$result = $this->repository->findAll(
|
||||
pagination: $input->pagination,
|
||||
sortCriteria: $input->sort,
|
||||
filters: $input->filters
|
||||
);
|
||||
|
||||
$items = array_map(
|
||||
static fn (CampaignRequest $campaignRequest): CampaignRequestListItem => new CampaignRequestListItem(
|
||||
brandCode: $campaignRequest->brandCode,
|
||||
name: $campaignRequest->name,
|
||||
applicant: $campaignRequest->applicant,
|
||||
campaignType: $campaignRequest->campaignType,
|
||||
status: $campaignRequest->status
|
||||
),
|
||||
$result->getItems(),
|
||||
);
|
||||
|
||||
return new Output(
|
||||
campaignsRequest: new Paginated(
|
||||
pagination: $result->getPagination(),
|
||||
items: $items,
|
||||
total: $result->getTotal(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/Application/UseCases/CampaignRequest/List/Output.php
Normal file
17
src/Application/UseCases/CampaignRequest/List/Output.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\List;
|
||||
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
|
||||
final readonly class Output implements UseCaseOutput
|
||||
{
|
||||
/** @param Paginated<CampaignRequestListItem> $campaignsRequest */
|
||||
public function __construct(
|
||||
public Paginated $campaignsRequest,
|
||||
) {
|
||||
}
|
||||
}
|
||||
18
src/Application/UseCases/CampaignRequest/Reject/Input.php
Normal file
18
src/Application/UseCases/CampaignRequest/Reject/Input.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Reject;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
|
||||
use Shared\DDD\UseCases\Input as UseCaseInput;
|
||||
|
||||
final readonly class Input implements UseCaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public CampaignRequestId $id,
|
||||
public RejectionReason $reason,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Reject\Output;
|
||||
|
||||
final readonly class CampaignRequestNotFound implements Output
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Reject\Output;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
|
||||
|
||||
final readonly class CampaignRequestRejected implements Output
|
||||
{
|
||||
public function __construct(
|
||||
public CampaignRequestId $id,
|
||||
public CampaignRequestStatus $status,
|
||||
public RejectionReason $rejectionReason,
|
||||
public ?\DateTimeInterface $updatedAt = null
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Reject\Output;
|
||||
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
|
||||
interface Output extends UseCaseOutput
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\CampaignRequest\Reject;
|
||||
|
||||
use Application\UseCases\CampaignRequest\Reject\Output\CampaignRequestNotFound;
|
||||
use Application\UseCases\CampaignRequest\Reject\Output\CampaignRequestRejected;
|
||||
use Application\UseCases\CampaignRequest\Reject\Output\Output;
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Ports\Repositories\CampaignRequestRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\DDD\UseCases\Transactional;
|
||||
use Shared\DDD\UseCases\UseCase;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements UseCase<\Application\UseCases\CampaignRequest\Reject\Input, \Application\UseCases\CampaignRequest\Reject\Output\Output> */
|
||||
final readonly class RejectCampaignRequest implements UseCase, Transactional
|
||||
{
|
||||
public function __construct(
|
||||
private CampaignRequestRepository $repository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$campaignRequest = $this->repository->findById($input->id);
|
||||
|
||||
if (! $campaignRequest instanceof CampaignRequest) {
|
||||
return new CampaignRequestNotFound();
|
||||
}
|
||||
|
||||
$campaignRequest = $campaignRequest->reject($input->reason);
|
||||
|
||||
$this->repository->save($campaignRequest);
|
||||
|
||||
return new CampaignRequestRejected(
|
||||
id: $campaignRequest->id(),
|
||||
status: $campaignRequest->status,
|
||||
rejectionReason:$campaignRequest->rejectionReason(),
|
||||
updatedAt: $campaignRequest->updatedAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
12
src/Domain/Enums/BarcodeFormat.php
Normal file
12
src/Domain/Enums/BarcodeFormat.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Enums;
|
||||
|
||||
enum BarcodeFormat: string
|
||||
{
|
||||
case EAN13 = 'ean';
|
||||
case CODE128 = 'code128';
|
||||
case GS1_128 = 'gs1';
|
||||
}
|
||||
19
src/Domain/Exceptions/BrandNotFoundException.php
Normal file
19
src/Domain/Exceptions/BrandNotFoundException.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Exceptions;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Shared\DDD\Exceptions\NotFoundException;
|
||||
|
||||
/**
|
||||
* The brand referenced by a code could not be found.
|
||||
*/
|
||||
final class BrandNotFoundException extends NotFoundException
|
||||
{
|
||||
public function __construct(BrandCode $brandCode)
|
||||
{
|
||||
parent::__construct(sprintf('Brand with code "%s" was not found.', $brandCode->value));
|
||||
}
|
||||
}
|
||||
50
src/Domain/Models/Entities/Actor.php
Normal file
50
src/Domain/Models/Entities/Actor.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\Entities;
|
||||
|
||||
use Domain\Models\ValueObjects\Actor\ActorDisplayName;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Shared\DDD\Models\AggregateRoot;
|
||||
use Shared\DDD\Models\HasTimestampIdentifier;
|
||||
|
||||
/**
|
||||
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Actor\ActorId>
|
||||
*/
|
||||
final readonly class Actor implements AggregateRoot, HasTimestampIdentifier
|
||||
{
|
||||
private function __construct(
|
||||
public ActorId $id,
|
||||
public ActorDisplayName $displayName,
|
||||
public ?\DateTimeInterface $updatedAt = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): ActorId
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function createdAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->id->toDateTime();
|
||||
}
|
||||
|
||||
public function updatedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public static function hydrate(
|
||||
ActorId $id,
|
||||
ActorDisplayName $displayName,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
displayName: $displayName,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
109
src/Domain/Models/Entities/Campaign.php
Normal file
109
src/Domain/Models/Entities/Campaign.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\Entities;
|
||||
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignId;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Shared\DDD\Models\AggregateRoot;
|
||||
use Shared\DDD\Models\HasTimestampIdentifier;
|
||||
|
||||
/**
|
||||
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Campaign\CampaignId>
|
||||
*/
|
||||
final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier
|
||||
{
|
||||
/**
|
||||
* @param array<CampaignCoupon> $coupons
|
||||
*/
|
||||
private function __construct(
|
||||
public CampaignId $id,
|
||||
public CampaignCode $code,
|
||||
public CampaignName $name,
|
||||
public bool $publication,
|
||||
public bool $printAllAtOnce,
|
||||
public array $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
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function createdAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->id->toDateTime();
|
||||
}
|
||||
|
||||
public function updatedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<CampaignCoupon> $coupons
|
||||
*/
|
||||
public static function create(
|
||||
CampaignCode $code,
|
||||
CampaignName $name,
|
||||
bool $publication,
|
||||
bool $printAllAtOnce,
|
||||
array $coupons,
|
||||
BrandCode $brandCode,
|
||||
ActorId $actorId,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): Campaign {
|
||||
return new self(
|
||||
id: CampaignId::generate(),
|
||||
code: $code,
|
||||
name: $name,
|
||||
publication: $publication,
|
||||
printAllAtOnce: $printAllAtOnce,
|
||||
coupons: $coupons,
|
||||
brandCode: $brandCode,
|
||||
actorId: $actorId,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<CampaignCoupon> $coupons
|
||||
*/
|
||||
public static function hydrate(
|
||||
CampaignId $id,
|
||||
CampaignCode $code,
|
||||
CampaignName $name,
|
||||
bool $publication,
|
||||
bool $printAllAtOnce,
|
||||
array $coupons,
|
||||
BrandCode $brandCode,
|
||||
ActorId $actorId,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
code: $code,
|
||||
name: $name,
|
||||
publication: $publication,
|
||||
printAllAtOnce: $printAllAtOnce,
|
||||
coupons: $coupons,
|
||||
brandCode: $brandCode,
|
||||
actorId: $actorId,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
131
src/Domain/Models/Entities/CampaignRequest.php
Normal file
131
src/Domain/Models/Entities/CampaignRequest.php
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\Entities;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Shared\DDD\Models\AggregateRoot;
|
||||
use Shared\DDD\Models\HasTimestampIdentifier;
|
||||
use Shared\Helpers\DatesHelper;
|
||||
|
||||
/**
|
||||
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId>
|
||||
*/
|
||||
final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdentifier
|
||||
{
|
||||
private function __construct(
|
||||
public CampaignRequestId $id,
|
||||
public BrandCode $brandCode,
|
||||
public CampaignName $name,
|
||||
public Applicant $applicant,
|
||||
public CampaignRequestType $campaignType,
|
||||
public RequestedCoupon $requestedCoupon,
|
||||
public CampaignRequestStatus $status,
|
||||
public ?RejectionReason $rejectionReason = null,
|
||||
public ?\DateTimeInterface $updatedAt = null,
|
||||
) {
|
||||
if ($this->status === CampaignRequestStatus::REJECTED && ! $this->rejectionReason instanceof RejectionReason) {
|
||||
throw new \DomainException('A rejection reason is required when the campaign request is rejected.');
|
||||
}
|
||||
|
||||
if ($this->status !== CampaignRequestStatus::REJECTED && $this->rejectionReason instanceof RejectionReason) {
|
||||
throw new \DomainException('A rejection reason can only be set when the campaign request is rejected.');
|
||||
}
|
||||
}
|
||||
|
||||
public function id(): CampaignRequestId
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function createdAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->id->toDateTime();
|
||||
}
|
||||
|
||||
public function updatedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public static function create(
|
||||
BrandCode $brandCode,
|
||||
CampaignName $name,
|
||||
Applicant $applicant,
|
||||
CampaignRequestType $campaignType,
|
||||
RequestedCoupon $requestedCoupon,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: CampaignRequestId::generate(),
|
||||
brandCode: $brandCode,
|
||||
name: $name,
|
||||
applicant: $applicant,
|
||||
campaignType: $campaignType,
|
||||
requestedCoupon: $requestedCoupon,
|
||||
status: CampaignRequestStatus::CREATED,
|
||||
rejectionReason: null,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
public static function hydrate(
|
||||
CampaignRequestId $id,
|
||||
BrandCode $brandCode,
|
||||
CampaignName $name,
|
||||
Applicant $applicant,
|
||||
CampaignRequestType $campaignType,
|
||||
RequestedCoupon $requestedCoupon,
|
||||
CampaignRequestStatus $status,
|
||||
?RejectionReason $rejectionReason = null,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
brandCode: $brandCode,
|
||||
name: $name,
|
||||
applicant: $applicant,
|
||||
campaignType: $campaignType,
|
||||
requestedCoupon: $requestedCoupon,
|
||||
status: $status,
|
||||
rejectionReason: $rejectionReason,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
public function reject(RejectionReason $reason): self
|
||||
{
|
||||
if ($this->status !== CampaignRequestStatus::CREATED) {
|
||||
throw new \DomainException('Only a created campaign request can be rejected.');
|
||||
}
|
||||
|
||||
return new self(
|
||||
id: $this->id,
|
||||
brandCode: $this->brandCode,
|
||||
name: $this->name,
|
||||
applicant: $this->applicant,
|
||||
campaignType: $this->campaignType,
|
||||
requestedCoupon: $this->requestedCoupon,
|
||||
status: CampaignRequestStatus::REJECTED,
|
||||
rejectionReason: $reason,
|
||||
updatedAt: DatesHelper::now(),
|
||||
);
|
||||
}
|
||||
|
||||
public function rejectionReason(): RejectionReason
|
||||
{
|
||||
if (! $this->rejectionReason instanceof RejectionReason) {
|
||||
throw new \DomainException('Only a rejected campaign request has a rejection reason.');
|
||||
}
|
||||
|
||||
return $this->rejectionReason;
|
||||
}
|
||||
}
|
||||
165
src/Domain/Models/Entities/Coupon.php
Normal file
165
src/Domain/Models/Entities/Coupon.php
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\Entities;
|
||||
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Coupon\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\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Shared\DDD\Models\AggregateRoot;
|
||||
use Shared\DDD\Models\HasTimestampIdentifier;
|
||||
|
||||
/**
|
||||
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Coupon\CouponId>
|
||||
*/
|
||||
final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier
|
||||
{
|
||||
/**
|
||||
* @param array<CouponValidityInformation> $couponValidityInformation
|
||||
* @param array<CouponLegalInformation> $couponLegalInformation
|
||||
* @param array<IncoCode> $products
|
||||
*/
|
||||
private function __construct(
|
||||
public CouponId $id,
|
||||
public CouponCode $code,
|
||||
public CouponName $name,
|
||||
public MoneyAmount $amount,
|
||||
public CouponPicture $picture,
|
||||
public CouponDiffusion $diffusion,
|
||||
public CouponValidity $validity,
|
||||
public array $couponValidityInformation,
|
||||
public array $couponLegalInformation,
|
||||
public ProcessingCode $processingCode,
|
||||
public BrandCode $brandCode,
|
||||
public ActorId $createdBy,
|
||||
public array $products = [],
|
||||
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
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function createdAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->id->toDateTime();
|
||||
}
|
||||
|
||||
public function updatedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<CouponValidityInformation> $couponValidityInformation
|
||||
* @param array<CouponLegalInformation> $couponLegalInformation
|
||||
* @param array<IncoCode> $products
|
||||
*/
|
||||
public static function create(
|
||||
CouponCode $code,
|
||||
CouponName $name,
|
||||
MoneyAmount $amount,
|
||||
CouponPicture $picture,
|
||||
CouponDiffusion $diffusion,
|
||||
CouponValidity $validity,
|
||||
array $couponValidityInformation,
|
||||
array $couponLegalInformation,
|
||||
ProcessingCode $processingCode,
|
||||
BrandCode $brandCode,
|
||||
ActorId $createdBy,
|
||||
array $products = [],
|
||||
?CouponAvailabilityCalendar $availabilityCalendar = null,
|
||||
?GS1 $gs1 = null,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): Coupon {
|
||||
return new self(
|
||||
id: CouponId::generate(),
|
||||
code: $code,
|
||||
name: $name,
|
||||
amount: $amount,
|
||||
picture: $picture,
|
||||
diffusion: $diffusion,
|
||||
validity: $validity,
|
||||
couponValidityInformation: $couponValidityInformation,
|
||||
couponLegalInformation: $couponLegalInformation,
|
||||
processingCode: $processingCode,
|
||||
brandCode: $brandCode,
|
||||
createdBy: $createdBy,
|
||||
products: $products,
|
||||
availabilityCalendar: $availabilityCalendar,
|
||||
gs1: $gs1,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<CouponValidityInformation> $couponValidityInformation
|
||||
* @param array<CouponLegalInformation> $couponLegalInformation
|
||||
* @param array<IncoCode> $products
|
||||
*/
|
||||
public static function hydrate(
|
||||
CouponId $id,
|
||||
CouponCode $code,
|
||||
CouponName $name,
|
||||
MoneyAmount $amount,
|
||||
CouponPicture $picture,
|
||||
CouponDiffusion $diffusion,
|
||||
CouponValidity $validity,
|
||||
array $couponValidityInformation,
|
||||
array $couponLegalInformation,
|
||||
ProcessingCode $processingCode,
|
||||
BrandCode $brandCode,
|
||||
ActorId $createdBy,
|
||||
array $products = [],
|
||||
?CouponAvailabilityCalendar $availabilityCalendar = null,
|
||||
?GS1 $gs1 = null,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
code: $code,
|
||||
name: $name,
|
||||
amount: $amount,
|
||||
picture: $picture,
|
||||
diffusion: $diffusion,
|
||||
validity: $validity,
|
||||
couponValidityInformation: $couponValidityInformation,
|
||||
couponLegalInformation: $couponLegalInformation,
|
||||
processingCode: $processingCode,
|
||||
brandCode: $brandCode,
|
||||
createdBy: $createdBy,
|
||||
products: $products,
|
||||
availabilityCalendar: $availabilityCalendar,
|
||||
gs1: $gs1,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
71
src/Domain/Models/Entities/Stock.php
Normal file
71
src/Domain/Models/Entities/Stock.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\Entities;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\Stock\StockAdjustment;
|
||||
use Domain\Models\ValueObjects\Stock\StockAmount;
|
||||
use Domain\Models\ValueObjects\Stock\StockId;
|
||||
use Shared\DDD\Models\AggregateRoot;
|
||||
use Shared\DDD\Models\HasTimestampIdentifier;
|
||||
|
||||
/**
|
||||
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Stock\StockId>
|
||||
*/
|
||||
final readonly class Stock implements AggregateRoot, HasTimestampIdentifier
|
||||
{
|
||||
private function __construct(
|
||||
public StockId $id,
|
||||
public StockAmount $stockAmount,
|
||||
public CampaignCode $campaignCode,
|
||||
public ProcessingCode $processingCode,
|
||||
/** @var array<int, StockAdjustment> $stockAdjustment */
|
||||
public array $stockAdjustment,
|
||||
public ?\DateTimeInterface $updatedAt = null,
|
||||
) {
|
||||
foreach ($this->stockAdjustment as $adjustment) {
|
||||
if (!$adjustment instanceof StockAdjustment) {
|
||||
throw new \DomainException('Stock adjustment must be StockAdjustment instances.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function id(): StockId
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function createdAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->id->toDateTime();
|
||||
}
|
||||
|
||||
public function updatedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, StockAdjustment> $stockAdjustment
|
||||
*/
|
||||
public static function hydrate(
|
||||
StockId $id,
|
||||
StockAmount $stockAmount,
|
||||
CampaignCode $campaignCode,
|
||||
ProcessingCode $processingCode,
|
||||
array $stockAdjustment = [],
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
stockAmount: $stockAmount,
|
||||
campaignCode: $campaignCode,
|
||||
processingCode: $processingCode,
|
||||
stockAdjustment: $stockAdjustment,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php
Normal file
16
src/Domain/Models/ValueObjects/Actor/ActorDisplayName.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Actor;
|
||||
|
||||
final readonly class ActorDisplayName
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
if (mb_trim($this->value) === '') {
|
||||
throw new \DomainException('Display name cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Domain/Models/ValueObjects/Actor/ActorId.php
Normal file
16
src/Domain/Models/ValueObjects/Actor/ActorId.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Actor;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\DDD\Models\Uuidv7EntityId;
|
||||
|
||||
final readonly class ActorId extends Uuidv7EntityId
|
||||
{
|
||||
public static function generate(): self
|
||||
{
|
||||
return new self(Uuid::uuid7());
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ final readonly class BrandName
|
|||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
$trimmed = trim($value);
|
||||
$trimmed = mb_trim($value);
|
||||
|
||||
if ($trimmed === '') {
|
||||
throw new \DomainException('Brand name cannot be empty.');
|
||||
|
|
|
|||
16
src/Domain/Models/ValueObjects/Campaign/CampaignCode.php
Normal file
16
src/Domain/Models/ValueObjects/Campaign/CampaignCode.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Campaign;
|
||||
|
||||
final readonly class CampaignCode
|
||||
{
|
||||
public function __construct(
|
||||
public string $code,
|
||||
) {
|
||||
if (mb_trim($this->code) === '') {
|
||||
throw new \DomainException('Campaign code cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php
Normal file
21
src/Domain/Models/ValueObjects/Campaign/CampaignCoupon.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Campaign;
|
||||
|
||||
use Domain\Models\ValueObjects\Coupon\CouponQuantity;
|
||||
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 ?PrintRules $printRules = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
16
src/Domain/Models/ValueObjects/Campaign/CampaignId.php
Normal file
16
src/Domain/Models/ValueObjects/Campaign/CampaignId.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Campaign;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\DDD\Models\Uuidv7EntityId;
|
||||
|
||||
final readonly class CampaignId extends Uuidv7EntityId
|
||||
{
|
||||
public static function generate(): self
|
||||
{
|
||||
return new self(Uuid::uuid7());
|
||||
}
|
||||
}
|
||||
16
src/Domain/Models/ValueObjects/Campaign/CampaignName.php
Normal file
16
src/Domain/Models/ValueObjects/Campaign/CampaignName.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Campaign;
|
||||
|
||||
final readonly class CampaignName
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
if (mb_trim($this->value) === '') {
|
||||
throw new \DomainException('Campaign name cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Domain/Models/ValueObjects/CampaignRequest/Applicant.php
Normal file
36
src/Domain/Models/ValueObjects/CampaignRequest/Applicant.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
use Shared\ValueObjects\Email;
|
||||
|
||||
final readonly class Applicant
|
||||
{
|
||||
public function __construct(
|
||||
public string $firstname,
|
||||
public string $lastname,
|
||||
public Email $email,
|
||||
) {
|
||||
$trimmedFirstname = mb_trim($firstname);
|
||||
|
||||
if ($trimmedFirstname === '') {
|
||||
throw new \DomainException('Applicant firstname cannot be empty.');
|
||||
}
|
||||
|
||||
if (mb_strlen($trimmedFirstname) > 200) {
|
||||
throw new \DomainException('Applicant firstname exceeds the maximum allowed length.');
|
||||
}
|
||||
|
||||
$trimmedLastname = mb_trim($lastname);
|
||||
|
||||
if ($trimmedLastname === '') {
|
||||
throw new \DomainException('Applicant lastname cannot be empty.');
|
||||
}
|
||||
|
||||
if (mb_strlen($trimmedLastname) > 200) {
|
||||
throw new \DomainException('Applicant lastname exceeds the maximum allowed length.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
|
||||
final readonly class CampaignRequestFilters
|
||||
{
|
||||
public function __construct(
|
||||
public ?BrandCode $brandCode = null,
|
||||
public ?CampaignRequestStatus $status = null,
|
||||
) {
|
||||
if (null === $this->brandCode && null === $this->status) {
|
||||
throw new \InvalidArgumentException('CampaignRequestFilters require at least a brand code or a status.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\DDD\Models\Uuidv7EntityId;
|
||||
|
||||
final readonly class CampaignRequestId extends Uuidv7EntityId
|
||||
{
|
||||
public static function generate(): static
|
||||
{
|
||||
return new self(Uuid::uuid7());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
final readonly class CampaignRequestSortClause
|
||||
{
|
||||
public function __construct(
|
||||
public CampaignRequestSortField $field,
|
||||
public \SortDirection $direction,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
final readonly class CampaignRequestSortCriteria
|
||||
{
|
||||
public function __construct(
|
||||
/** @var CampaignRequestSortClause[] */
|
||||
public array $clauses
|
||||
) {
|
||||
if ($this->clauses === []) {
|
||||
throw new \DomainException('Campaign Request Sort Criteria requires at least one clause.');
|
||||
}
|
||||
|
||||
foreach ($this->clauses as $clause) {
|
||||
if (!$clause instanceof CampaignRequestSortClause) {
|
||||
throw new \DomainException('CampaignRequestSortCriteria clauses must be CampaignRequestSortClause instances.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
enum CampaignRequestSortField: string
|
||||
{
|
||||
case BrandCode = 'brand_code';
|
||||
case Status = 'status';
|
||||
case CreatedAt = 'created_at';
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
enum CampaignRequestStatus: string
|
||||
{
|
||||
case CREATED = 'created';
|
||||
case TRANSFORMED = 'transformed';
|
||||
case REJECTED = 'rejected';
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
enum CampaignRequestType: string
|
||||
{
|
||||
case CLUB = 'club';
|
||||
case GAME = 'game';
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
final readonly class RejectionReason
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
if (mb_trim($this->value) === '') {
|
||||
throw new \DomainException('Rejection reason cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Shared\DDD\Models\Equatable;
|
||||
use DateTime;
|
||||
|
||||
final readonly class CouponAvailabilityCalendar implements Equatable
|
||||
{
|
||||
public function __construct(
|
||||
/** @var Period[] */
|
||||
public array $periods,
|
||||
) {
|
||||
if ($this->periods === []) {
|
||||
throw new \DomainException('Diffusion requires at least one period.');
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
if (!$period instanceof Period) {
|
||||
throw new \DomainException('CouponAvailabilityCalendar periods must be Period instances.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function isActiveAt(DateTime $in): bool
|
||||
{
|
||||
foreach ($this->periods as $period) {
|
||||
if ($in >= $period->start && $in <= $period->end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function beginsAt(): DateTime
|
||||
{
|
||||
return min(array_map(fn (Period $period) => $period->start, $this->periods));
|
||||
}
|
||||
|
||||
public function endsAt(): DateTime
|
||||
{
|
||||
return max(array_map(fn (Period $period) => $period->end, $this->periods));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof self && $this->periods == $other->periods;
|
||||
}
|
||||
}
|
||||
26
src/Domain/Models/ValueObjects/Coupon/CouponCode.php
Normal file
26
src/Domain/Models/ValueObjects/Coupon/CouponCode.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
final readonly class CouponCode
|
||||
{
|
||||
private const string REGEX = '/^[A-Z]{3}\d{2}$/';
|
||||
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
if (! preg_match(self::REGEX, $value)) {
|
||||
throw new \DomainException(
|
||||
"Invalid code '{$value}' does not match pattern " . self::REGEX,
|
||||
);
|
||||
}
|
||||
|
||||
if ($value[3] === $value[4]) {
|
||||
throw new \DomainException(
|
||||
"Invalid code '{$value}' has the same digit repeated",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php
Normal file
15
src/Domain/Models/ValueObjects/Coupon/CouponDiffusion.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
|
||||
final readonly class CouponDiffusion
|
||||
{
|
||||
public function __construct(
|
||||
public Period $period,
|
||||
) {
|
||||
}
|
||||
}
|
||||
16
src/Domain/Models/ValueObjects/Coupon/CouponId.php
Normal file
16
src/Domain/Models/ValueObjects/Coupon/CouponId.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\DDD\Models\Uuidv7EntityId;
|
||||
|
||||
final readonly class CouponId extends Uuidv7EntityId
|
||||
{
|
||||
public static function generate(): self
|
||||
{
|
||||
return new self(Uuid::uuid7());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Shared\Enums\Country;
|
||||
|
||||
final readonly class CouponLegalInformation
|
||||
{
|
||||
public function __construct(
|
||||
public string $legalText,
|
||||
public Country $forLocale
|
||||
) {
|
||||
if (mb_trim($this->legalText) === '') {
|
||||
throw new \DomainException('Legal text cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php
Normal file
16
src/Domain/Models/ValueObjects/Coupon/CouponQuantity.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
final readonly class CouponQuantity
|
||||
{
|
||||
public function __construct(
|
||||
public int $quantity,
|
||||
) {
|
||||
if ($this->quantity <= 0) {
|
||||
throw new \DomainException('CouponQuantity quantity must be positive.');
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/Domain/Models/ValueObjects/Coupon/CouponValidity.php
Normal file
26
src/Domain/Models/ValueObjects/Coupon/CouponValidity.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
use DateTime;
|
||||
|
||||
final readonly class CouponValidity implements Equatable
|
||||
{
|
||||
public function __construct(
|
||||
public int $days,
|
||||
public DateTime $until
|
||||
) {
|
||||
if ($this->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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Shared\Enums\Country;
|
||||
|
||||
final readonly class CouponValidityInformation
|
||||
{
|
||||
public function __construct(
|
||||
public string $validityText,
|
||||
public Country $forLocale
|
||||
) {
|
||||
if (mb_trim($this->validityText) === '') {
|
||||
throw new \DomainException('Validity text cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Domain/Models/ValueObjects/CouponName.php
Normal file
16
src/Domain/Models/ValueObjects/CouponName.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
final readonly class CouponName
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
if (mb_trim($this->value) === '') {
|
||||
throw new \DomainException('Coupon name cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Domain/Models/ValueObjects/CouponPicture.php
Normal file
15
src/Domain/Models/ValueObjects/CouponPicture.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
final readonly class CouponPicture
|
||||
{
|
||||
public function __construct(
|
||||
public PicturePath $decoration,
|
||||
public PicturePath $thumbnail,
|
||||
public PicturePath $inactiveThumbnail
|
||||
) {
|
||||
}
|
||||
}
|
||||
31
src/Domain/Models/ValueObjects/GS1.php
Normal file
31
src/Domain/Models/ValueObjects/GS1.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Domain\Enums\BarcodeFormat;
|
||||
use Shared\DDD\Models\Equatable;
|
||||
|
||||
final readonly class GS1 implements \Stringable, Equatable
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
public BarcodeFormat $format
|
||||
) {
|
||||
if (mb_trim($this->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;
|
||||
}
|
||||
}
|
||||
27
src/Domain/Models/ValueObjects/IncoCode.php
Normal file
27
src/Domain/Models/ValueObjects/IncoCode.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
|
||||
final readonly class IncoCode implements Equatable
|
||||
{
|
||||
/**
|
||||
* @throws \DomainException
|
||||
*/
|
||||
public function __construct(
|
||||
public int $value,
|
||||
) {
|
||||
if ($this->value <= 0) {
|
||||
throw new \DomainException('Inco Code must be a positive integer.');
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof self && $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
30
src/Domain/Models/ValueObjects/MoneyAmount.php
Normal file
30
src/Domain/Models/ValueObjects/MoneyAmount.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
use Shared\ValueObjects\Money;
|
||||
|
||||
/**
|
||||
* Wraps a Money value as a requested/expected monetary amount.
|
||||
*/
|
||||
final readonly class MoneyAmount implements \Stringable, Equatable
|
||||
{
|
||||
public function __construct(
|
||||
public Money $value,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof self && $this->value->equals($other->value);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string) $this->value;
|
||||
}
|
||||
}
|
||||
19
src/Domain/Models/ValueObjects/Period.php
Normal file
19
src/Domain/Models/ValueObjects/Period.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use DateTime;
|
||||
|
||||
final readonly class Period
|
||||
{
|
||||
public function __construct(
|
||||
public DateTime $start,
|
||||
public DateTime $end,
|
||||
) {
|
||||
if ($this->end < $this->start) {
|
||||
throw new \DomainException('Period end date cannot be before its start date.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ final readonly class PicturePath implements \Stringable, Equatable
|
|||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
$trimmed = trim($this->value);
|
||||
$trimmed = mb_trim($this->value);
|
||||
|
||||
if ($trimmed === '') {
|
||||
throw new \DomainException('Picture Path cannot be empty.');
|
||||
|
|
|
|||
25
src/Domain/Models/ValueObjects/PrintRules.php
Normal file
25
src/Domain/Models/ValueObjects/PrintRules.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
final readonly class PrintRules
|
||||
{
|
||||
public function __construct(
|
||||
public ?int $maxPerCustomer = null,
|
||||
public ?int $cooldownDays = null,
|
||||
) {
|
||||
if (null === $this->maxPerCustomer && null === $this->cooldownDays) {
|
||||
throw new \InvalidArgumentException('PrintRules require at least a max per customer or a cooldown days.');
|
||||
}
|
||||
|
||||
if (null !== $this->maxPerCustomer && $this->maxPerCustomer <= 0) {
|
||||
throw new \InvalidArgumentException('Max per customer must be greater than 0.');
|
||||
}
|
||||
|
||||
if (null !== $this->cooldownDays && $this->cooldownDays <= 0) {
|
||||
throw new \InvalidArgumentException('Cooldown days must be greater than 0.');
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Domain/Models/ValueObjects/ProcessingCode.php
Normal file
29
src/Domain/Models/ValueObjects/ProcessingCode.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
|
||||
final readonly class ProcessingCode implements \Stringable, Equatable
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
if (mb_trim($this->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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\RequestedCoupon;
|
||||
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\GS1;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Shared\Enums\Country;
|
||||
|
||||
final readonly class RequestedCoupon
|
||||
{
|
||||
private const int MIN_FACE_VALUE_IN_CENTS = 1;
|
||||
private const int MAX_FACE_VALUE_IN_CENTS = 300;
|
||||
|
||||
/**
|
||||
* @param array<CouponValidityInformation> $couponValidityInformation
|
||||
* @param array<IncoCode> $products
|
||||
*/
|
||||
public function __construct(
|
||||
public CouponName $name,
|
||||
public RequestedCouponQuantity $quantity,
|
||||
public Period $diffusion,
|
||||
public MoneyAmount $moneyAmount,
|
||||
public ProcessingCode $processingCode,
|
||||
public CouponValidity $couponValidity,
|
||||
public array $couponValidityInformation,
|
||||
public array $products = [],
|
||||
public ?PrintRules $printRules = null,
|
||||
public ?GS1 $gs1 = null,
|
||||
) {
|
||||
$money = $this->moneyAmount->value;
|
||||
|
||||
foreach ($this->couponValidityInformation as $validityInformation) {
|
||||
if (!$validityInformation instanceof CouponValidityInformation) {
|
||||
throw new \DomainException('Coupon validity information must be CouponValidityInformation instances.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($money->value < self::MIN_FACE_VALUE_IN_CENTS || $money->value > self::MAX_FACE_VALUE_IN_CENTS) {
|
||||
throw new \DomainException(sprintf(
|
||||
'Coupon face value must be between 0.01€ and 3.00€, %s given.',
|
||||
$money,
|
||||
));
|
||||
}
|
||||
|
||||
if (!$this->hasValidityInformationFor(Country::FRANCE)) {
|
||||
throw new \DomainException('A coupon requires validity information for France.');
|
||||
}
|
||||
|
||||
if ($this->gs1 instanceof GS1 && !$this->hasValidityInformationFor(Country::BENELUX)) {
|
||||
throw new \DomainException('A GS1 coupon requires validity information for Benelux.');
|
||||
}
|
||||
}
|
||||
|
||||
private function hasValidityInformationFor(Country $country): bool
|
||||
{
|
||||
foreach ($this->couponValidityInformation as $validityInformation) {
|
||||
if ($validityInformation->forLocale === $country) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\RequestedCoupon;
|
||||
|
||||
final readonly class RequestedCouponQuantity
|
||||
{
|
||||
public function __construct(
|
||||
public int $value,
|
||||
) {
|
||||
if ($this->value <= 0) {
|
||||
throw new \DomainException('Quantity should be more than 0.');
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Domain/Models/ValueObjects/Stock/StockAdjustment.php
Normal file
25
src/Domain/Models/ValueObjects/Stock/StockAdjustment.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Stock;
|
||||
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
|
||||
final readonly class StockAdjustment
|
||||
{
|
||||
public function __construct(
|
||||
public int $quantity,
|
||||
public string $reason,
|
||||
public ActorId $actorId,
|
||||
public ?\DateTimeInterface $createdAt = null,
|
||||
) {
|
||||
if ($this->quantity <= 0) {
|
||||
throw new \DomainException('Stock Amount should be more than 0.');
|
||||
}
|
||||
|
||||
if (mb_trim($this->reason) === '') {
|
||||
throw new \DomainException('Reason cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Domain/Models/ValueObjects/Stock/StockAmount.php
Normal file
16
src/Domain/Models/ValueObjects/Stock/StockAmount.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Stock;
|
||||
|
||||
final readonly class StockAmount
|
||||
{
|
||||
public function __construct(
|
||||
public int $value,
|
||||
) {
|
||||
if ($this->value < 0) {
|
||||
throw new \DomainException('Stock Amount should be positive.');
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Domain/Models/ValueObjects/Stock/StockId.php
Normal file
16
src/Domain/Models/ValueObjects/Stock/StockId.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Stock;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\DDD\Models\Uuidv7EntityId;
|
||||
|
||||
final readonly class StockId extends Uuidv7EntityId
|
||||
{
|
||||
public static function generate(): self
|
||||
{
|
||||
return new self(Uuid::uuid7());
|
||||
}
|
||||
}
|
||||
14
src/Domain/Ports/Repositories/CampaignRepository.php
Normal file
14
src/Domain/Ports/Repositories/CampaignRepository.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Ports\Repositories;
|
||||
|
||||
use Shared\DDD\Ports\Repository;
|
||||
|
||||
/**
|
||||
* @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\Campaign\CampaignId, \Domain\Models\Entities\Campaign>
|
||||
*/
|
||||
interface CampaignRepository extends Repository
|
||||
{
|
||||
}
|
||||
31
src/Domain/Ports/Repositories/CampaignRequestRepository.php
Normal file
31
src/Domain/Ports/Repositories/CampaignRequestRepository.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Ports\Repositories;
|
||||
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestFilters;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria;
|
||||
use Shared\DDD\Ports\Repository;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
|
||||
/**
|
||||
* @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId, \Domain\Models\Entities\CampaignRequest>
|
||||
*/
|
||||
interface CampaignRequestRepository extends Repository
|
||||
{
|
||||
public function findById(CampaignRequestId $id): ?CampaignRequest;
|
||||
|
||||
public function save(CampaignRequest $campaignRequest): void;
|
||||
/**
|
||||
* @return Paginated<CampaignRequest>
|
||||
*/
|
||||
public function findAll(
|
||||
Pagination $pagination,
|
||||
?CampaignRequestSortCriteria $sortCriteria = null,
|
||||
?CampaignRequestFilters $filters = null,
|
||||
): Paginated;
|
||||
}
|
||||
14
src/Domain/Ports/Repositories/CouponRepository.php
Normal file
14
src/Domain/Ports/Repositories/CouponRepository.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Ports\Repositories;
|
||||
|
||||
use Shared\DDD\Ports\Repository;
|
||||
|
||||
/**
|
||||
* @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\Coupon\CouponId, \Domain\Models\Entities\Coupon>
|
||||
*/
|
||||
interface CouponRepository extends Repository
|
||||
{
|
||||
}
|
||||
20
src/Shared/DDD/Exceptions/UnknownCountryIsoCodeException.php
Normal file
20
src/Shared/DDD/Exceptions/UnknownCountryIsoCodeException.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\DDD\Exceptions;
|
||||
|
||||
use Shared\Enums\Country;
|
||||
|
||||
final class UnknownCountryIsoCodeException extends \DomainException
|
||||
{
|
||||
public static function fromIsoCode(string $isoCode): self
|
||||
{
|
||||
return new self("No country found for ISO code '{$isoCode}'");
|
||||
}
|
||||
|
||||
public static function fromCountry(Country $country): self
|
||||
{
|
||||
return new self("No ISO code found for country '{$country->name}'");
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ abstract readonly class EntityId implements \Stringable, Equatable
|
|||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
if (trim($value) === '') {
|
||||
if (mb_trim($this->value) === '') {
|
||||
throw new \DomainException('EntityId value cannot be empty');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
src/Shared/Enums/Country.php
Normal file
11
src/Shared/Enums/Country.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\Enums;
|
||||
|
||||
enum Country: string
|
||||
{
|
||||
case FRANCE = 'france';
|
||||
case BENELUX = 'benelux';
|
||||
}
|
||||
10
src/Shared/Enums/Currency.php
Normal file
10
src/Shared/Enums/Currency.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\Enums;
|
||||
|
||||
enum Currency: string
|
||||
{
|
||||
case EUR = 'euro';
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ final class DatesHelper
|
|||
|
||||
public static function parseDateTime(string $datetime): \DateTimeInterface
|
||||
{
|
||||
$datetime = trim($datetime);
|
||||
$datetime = mb_trim($datetime);
|
||||
|
||||
if ($datetime === '') {
|
||||
throw InvalidDateFormatException::forDateString($datetime);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ final readonly class Email implements \Stringable, Equatable
|
|||
public function __construct(
|
||||
string $value,
|
||||
) {
|
||||
$trimmed = trim($value);
|
||||
$trimmed = mb_trim($value);
|
||||
|
||||
if ($trimmed === '') {
|
||||
throw new \DomainException('Email cannot be empty.');
|
||||
|
|
|
|||
42
src/Shared/ValueObjects/Money.php
Normal file
42
src/Shared/ValueObjects/Money.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\ValueObjects;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
use Shared\Enums\Currency;
|
||||
|
||||
/**
|
||||
* Represents a monetary amount, expressed as an integer in the currency's
|
||||
* smallest unit (e.g. cents).
|
||||
*/
|
||||
final readonly class Money implements \Stringable, Equatable
|
||||
{
|
||||
/**
|
||||
* @param Currency $currency Currency of the amount.
|
||||
* @param int $value Amount expressed in the currency's smallest unit (e.g. cents). Must be zero or positive.
|
||||
* @throws \DomainException
|
||||
*/
|
||||
public function __construct(
|
||||
public Currency $currency,
|
||||
public int $value,
|
||||
) {
|
||||
if ($this->value < 0) {
|
||||
throw new \DomainException('Money value cannot be negative.');
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof self
|
||||
&& $this->currency->value === $other->currency->value
|
||||
&& $this->value === $other->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('%d %s', $this->value, $this->currency->name);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ final readonly class Rcs implements \Stringable, Equatable
|
|||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
$trimmed = trim($this->value);
|
||||
$trimmed = mb_trim($this->value);
|
||||
|
||||
if ($trimmed === '') {
|
||||
throw new \DomainException('Rcs text cannot be empty.');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\CampaignRequest\Create;
|
||||
|
||||
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\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Domain\Ports\Repositories\CampaignRequestRepository;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Email;
|
||||
use Shared\ValueObjects\Money;
|
||||
|
||||
covers(CreateCampaignRequest::class, Input::class, Output::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->input = new Input(
|
||||
brandCode: new BrandCode('AAA01'),
|
||||
name: new CampaignName('Campaign name'),
|
||||
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
campaignType: CampaignRequestType::CLUB,
|
||||
requestedCoupon: new RequestedCoupon(
|
||||
name: new CouponName('Coupon A'),
|
||||
quantity: new RequestedCouponQuantity(100),
|
||||
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
processingCode: new ProcessingCode('PC1'),
|
||||
couponValidity: new CouponValidity(30, new DateTime('2026-02-01')),
|
||||
couponValidityInformation: [
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
],
|
||||
products: [new IncoCode(35784), new IncoCode(15522)],
|
||||
printRules: new PrintRules(maxPerCustomer: 3)
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe('create campaign request use case', function (): void {
|
||||
it('creates a campaign request when the brand exists', function (): void {
|
||||
// GIVEN
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($this->input->brandCode)->andReturn(true);
|
||||
|
||||
$campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$campaignRequestRepository->shouldReceive('save')->once()->with(\Mockery::type(CampaignRequest::class));
|
||||
|
||||
$useCase = new CreateCampaignRequest($campaignRequestRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($this->input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class)
|
||||
->and($result->status)->toBe(CampaignRequestStatus::CREATED)
|
||||
->and($result->brandCode)->toBe($this->input->brandCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create campaign request use case validation rules', function (): void {
|
||||
it('throws when the brand does not exist', function (): void {
|
||||
// GIVEN
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($this->input->brandCode)->andReturn(false);
|
||||
|
||||
$campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$campaignRequestRepository->shouldNotReceive('save');
|
||||
|
||||
$useCase = new CreateCampaignRequest($campaignRequestRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($this->input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(BrandNotFoundException::class);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\CampaignRequest\Find;
|
||||
|
||||
use Application\UseCases\CampaignRequest\Find\FindCampaignRequest;
|
||||
use Application\UseCases\CampaignRequest\Find\Input;
|
||||
use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestFound;
|
||||
use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestNotFound;
|
||||
use Application\UseCases\CampaignRequest\Find\Output\Output;
|
||||
use DateTime;
|
||||
use Domain\Enums\BarcodeFormat;
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\GS1;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Domain\Ports\Repositories\CampaignRequestRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Email;
|
||||
use Shared\ValueObjects\Money;
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
it('finds a campaign request by its id, along with its brand', function (): 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,
|
||||
);
|
||||
$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'),
|
||||
);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('findByCode')->once()->with($brandCode)->andReturn($brand);
|
||||
|
||||
$useCase = new FindCampaignRequest($repository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(id: $id));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CampaignRequestFound::class)
|
||||
->and($result->brand)->toBe($brand)
|
||||
->and($result->name)->toBe($campaignRequest->name)
|
||||
->and($result->applicant)->toBe($campaignRequest->applicant)
|
||||
->and($result->campaignType)->toBe($campaignRequest->campaignType)
|
||||
->and($result->requestedCoupon)->toBe($campaignRequest->requestedCoupon)
|
||||
->and($result->status)->toBe($campaignRequest->status)
|
||||
->and($result->rejectionReason)->toBeNull();
|
||||
});
|
||||
|
||||
it('finds a rejected campaign request with its rejection reason', function (): void {
|
||||
// GIVEN
|
||||
$id = CampaignRequestId::generate();
|
||||
$code = new BrandCode('AAA01');
|
||||
$rejectionReason = new RejectionReason('Missing legal documents.');
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
$id,
|
||||
$code,
|
||||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->requestedCoupon,
|
||||
CampaignRequestStatus::REJECTED,
|
||||
$rejectionReason,
|
||||
);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(
|
||||
Brand::hydrate(
|
||||
BrandId::generate(),
|
||||
$code,
|
||||
new BrandName('Brand A'),
|
||||
new PicturePath('brands/logo.jpg'),
|
||||
new Rcs('RCS PARIS 111 222 333'),
|
||||
new Email('contact@example.com'),
|
||||
)
|
||||
);
|
||||
|
||||
$useCase = new FindCampaignRequest($repository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(id: $id));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CampaignRequestFound::class)
|
||||
->and($result->status)->toBe(CampaignRequestStatus::REJECTED)
|
||||
->and($result->rejectionReason)->toBe($rejectionReason);
|
||||
});
|
||||
|
||||
it('returns CampaignRequestNotFound when the id is not found', function (): void {
|
||||
// GIVEN
|
||||
$id = CampaignRequestId::generate();
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findById')->once()->with($id)->andReturn(null);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldNotReceive('findByCode');
|
||||
|
||||
$useCase = new FindCampaignRequest($repository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(id: $id));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CampaignRequestNotFound::class);
|
||||
});
|
||||
|
||||
it('throws when the campaign request references a brand that no longer exists', function (): void {
|
||||
// GIVEN
|
||||
$id = CampaignRequestId::generate();
|
||||
$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,
|
||||
);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('findByCode')->once()->with($brandCode)->andReturn(null);
|
||||
|
||||
$useCase = new FindCampaignRequest($repository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute(new Input(id: $id));
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(BrandNotFoundException::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('find campaign request use case assertions', function (): void {
|
||||
it('rejects an input of the wrong type', function (): void {
|
||||
// GIVEN
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$useCase = new FindCampaignRequest($repository, $brandRepository);
|
||||
$invalidInput = new class () implements BaseInput
|
||||
{
|
||||
};
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($invalidInput);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
<?php
|
||||
|
||||
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;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestFilters;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortClause;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\GS1;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
|
||||
use Domain\Ports\Repositories\CampaignRequestRepository;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Email;
|
||||
use Shared\ValueObjects\Money;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
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)
|
||||
);
|
||||
|
||||
$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)
|
||||
);
|
||||
});
|
||||
|
||||
it('maps repository campaigns request to list items, preserving pagination and sort', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage();
|
||||
$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,
|
||||
);
|
||||
$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,
|
||||
);
|
||||
$paginated = new Paginated($pagination, [$campaignRequest1, $campaignRequest2], 2);
|
||||
|
||||
$sort = new CampaignRequestSortCriteria(
|
||||
clauses: [
|
||||
new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending),
|
||||
new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending)
|
||||
]
|
||||
);
|
||||
$filters = new CampaignRequestFilters($campaignRequest2->brandCode, CampaignRequestStatus::CREATED);
|
||||
$input = new Input(pagination: $pagination, sort: $sort, filters: $filters);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($pagination, CampaignRequestSortCriteria::class, CampaignRequestFilters::class)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCampaignsRequest($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class);
|
||||
|
||||
$items = $result->campaignsRequest->getItems();
|
||||
expect($items)->toHaveCount(2)
|
||||
->and($items[0])->toBeInstanceOf(CampaignRequestListItem::class)
|
||||
->and($items[0]->brandCode)->toBe($campaignRequest1->brandCode)
|
||||
->and($items[0]->name)->toBe($campaignRequest1->name)
|
||||
->and($items[0]->applicant)->toBe($campaignRequest1->applicant)
|
||||
->and($items[0]->campaignType)->toBe($campaignRequest1->campaignType)
|
||||
->and($items[0]->status)->toBe($campaignRequest1->status)
|
||||
->and($result->campaignsRequest->getTotal())->toBe(2)
|
||||
->and($result->campaignsRequest->getPagination())->toBe($pagination);
|
||||
});
|
||||
|
||||
it('lists campaigns request with filters but without sort', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage();
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
new BrandCode('AAA01'),
|
||||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->couponA,
|
||||
CampaignRequestStatus::CREATED,
|
||||
);
|
||||
$paginated = new Paginated($pagination, [$campaignRequest], 1);
|
||||
|
||||
$filters = new CampaignRequestFilters($campaignRequest->brandCode, CampaignRequestStatus::CREATED);
|
||||
$input = new Input(pagination: $pagination, filters: $filters);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($pagination, null, $filters)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCampaignsRequest($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
$items = $result->campaignsRequest->getItems();
|
||||
expect($items)->toHaveCount(1)
|
||||
->and($items[0])->toBeInstanceOf(CampaignRequestListItem::class)
|
||||
->and($items[0]->brandCode)->toBe($campaignRequest->brandCode)
|
||||
->and($items[0]->name)->toBe($campaignRequest->name)
|
||||
->and($items[0]->status)->toBe($campaignRequest->status)
|
||||
->and($result->campaignsRequest->getTotal())->toBe(1)
|
||||
->and($result->campaignsRequest->getPagination())->toBe($pagination);
|
||||
});
|
||||
|
||||
it('lists campaigns request with sort but without filters', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage();
|
||||
$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,
|
||||
);
|
||||
$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,
|
||||
);
|
||||
$paginated = new Paginated($pagination, [$campaignRequest2, $campaignRequest1], 2);
|
||||
|
||||
$sort = new CampaignRequestSortCriteria(
|
||||
clauses: [
|
||||
new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Descending),
|
||||
]
|
||||
);
|
||||
$input = new Input(pagination: $pagination, sort: $sort);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($pagination, $sort, null)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCampaignsRequest($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
$items = $result->campaignsRequest->getItems();
|
||||
expect($items)->toHaveCount(2)
|
||||
->and($items[0]->brandCode)->toBe($campaignRequest2->brandCode)
|
||||
->and($items[1]->brandCode)->toBe($campaignRequest1->brandCode)
|
||||
->and($result->campaignsRequest->getTotal())->toBe(2)
|
||||
->and($result->campaignsRequest->getPagination())->toBe($pagination);
|
||||
});
|
||||
|
||||
it('returns an empty list without error when there are no campaigns request', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage();
|
||||
$paginated = new Paginated($pagination, [], 0);
|
||||
$input = new Input(pagination: $pagination);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($pagination, null, null)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCampaignsRequest($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result->campaignsRequest->getItems())->toBe([])
|
||||
->and($result->campaignsRequest->getTotal())->toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\CampaignRequest\Reject;
|
||||
|
||||
use Application\UseCases\CampaignRequest\Reject\Input;
|
||||
use Application\UseCases\CampaignRequest\Reject\Output\CampaignRequestNotFound;
|
||||
use Application\UseCases\CampaignRequest\Reject\Output\CampaignRequestRejected;
|
||||
use Application\UseCases\CampaignRequest\Reject\Output\Output;
|
||||
use Application\UseCases\CampaignRequest\Reject\RejectCampaignRequest;
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTime;
|
||||
use Domain\Enums\BarcodeFormat;
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\GS1;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
|
||||
use Domain\Ports\Repositories\CampaignRequestRepository;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Email;
|
||||
use Shared\ValueObjects\Money;
|
||||
|
||||
covers(RejectCampaignRequest::class, Input::class, CampaignRequestRejected::class, CampaignRequestNotFound::class);
|
||||
|
||||
describe('reject campaign request use case', function (): void {
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
CarbonImmutable::setTestNow();
|
||||
});
|
||||
|
||||
it('rejects a campaign request and persists it', function (): void {
|
||||
// GIVEN
|
||||
CarbonImmutable::setTestNow('2026-06-15 12:00:00');
|
||||
$id = CampaignRequestId::generate();
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
$id,
|
||||
new BrandCode('AAA01'),
|
||||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->requestedCoupon,
|
||||
CampaignRequestStatus::CREATED,
|
||||
);
|
||||
$reason = new RejectionReason('Missing legal documents.');
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest);
|
||||
$repository->shouldReceive('save')->once()->with(\Mockery::on(
|
||||
fn (CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED
|
||||
&& $saved->rejectionReason === $reason
|
||||
&& $saved->updatedAt !== null,
|
||||
));
|
||||
|
||||
$useCase = new RejectCampaignRequest($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(id: $id, reason: $reason));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CampaignRequestRejected::class)
|
||||
->and($result->id)->toBe($id)
|
||||
->and($result->status)->toBe(CampaignRequestStatus::REJECTED)
|
||||
->and($result->rejectionReason)->toBe($reason)
|
||||
->and($result->updatedAt->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00');
|
||||
});
|
||||
|
||||
it('returns CampaignRequestNotFound when the id is not found', function (): void {
|
||||
// GIVEN
|
||||
$id = CampaignRequestId::generate();
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findById')->once()->with($id)->andReturn(null);
|
||||
$repository->shouldNotReceive('save');
|
||||
|
||||
$useCase = new RejectCampaignRequest($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(id: $id, reason: new RejectionReason('Missing legal documents.')));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CampaignRequestNotFound::class);
|
||||
});
|
||||
|
||||
it('propagates the domain exception when the campaign request cannot be rejected', function (): void {
|
||||
// GIVEN
|
||||
$id = CampaignRequestId::generate();
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
$id,
|
||||
new BrandCode('AAA01'),
|
||||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->requestedCoupon,
|
||||
CampaignRequestStatus::TRANSFORMED,
|
||||
);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest);
|
||||
$repository->shouldNotReceive('save');
|
||||
|
||||
$useCase = new RejectCampaignRequest($repository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute(
|
||||
new Input(id: $id, reason: new RejectionReason('Missing legal documents.'))
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
23
tests/Unit/Domain/Enums/BarcodeFormatTest.php
Normal file
23
tests/Unit/Domain/Enums/BarcodeFormatTest.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Shared\Enums;
|
||||
|
||||
use Domain\Enums\BarcodeFormat;
|
||||
|
||||
covers(BarcodeFormat::class);
|
||||
|
||||
describe('barcode format', function (): void {
|
||||
it('has the expected cases with their backing values', function (): void {
|
||||
// THEN
|
||||
expect(BarcodeFormat::EAN13->value)->toBe('ean')
|
||||
->and(BarcodeFormat::CODE128->value)->toBe('code128')
|
||||
->and(BarcodeFormat::GS1_128->value)->toBe('gs1');
|
||||
});
|
||||
|
||||
it('exposes its case name', function (): void {
|
||||
// THEN
|
||||
expect(BarcodeFormat::EAN13->name)->toBe('EAN13');
|
||||
});
|
||||
});
|
||||
23
tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php
Normal file
23
tests/Unit/Domain/Enums/Campaign/CampaignStatusTest.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Enums\Campaign;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
|
||||
covers(CampaignRequestStatus::class);
|
||||
|
||||
describe('campaign status', function (): void {
|
||||
it('has the expected cases with their backing values', function (): void {
|
||||
// THEN
|
||||
expect(CampaignRequestStatus::CREATED->value)->toBe('created')
|
||||
->and(CampaignRequestStatus::TRANSFORMED->value)->toBe('transformed')
|
||||
->and(CampaignRequestStatus::REJECTED->value)->toBe('rejected');
|
||||
});
|
||||
|
||||
it('exposes its case name', function (): void {
|
||||
// THEN
|
||||
expect(CampaignRequestStatus::CREATED->name)->toBe('CREATED');
|
||||
});
|
||||
});
|
||||
22
tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php
Normal file
22
tests/Unit/Domain/Enums/Campaign/CampaignTypeTest.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Enums\Campaign;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
|
||||
covers(CampaignRequestType::class);
|
||||
|
||||
describe('campaign type', function (): void {
|
||||
it('has the expected cases with their backing values', function (): void {
|
||||
// THEN
|
||||
expect(CampaignRequestType::CLUB->value)->toBe('club')
|
||||
->and(CampaignRequestType::GAME->value)->toBe('game');
|
||||
});
|
||||
|
||||
it('exposes its case name', function (): void {
|
||||
// THEN
|
||||
expect(CampaignRequestType::CLUB->name)->toBe('CLUB');
|
||||
});
|
||||
});
|
||||
22
tests/Unit/Domain/Enums/CountryTest.php
Normal file
22
tests/Unit/Domain/Enums/CountryTest.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Shared\Enums;
|
||||
|
||||
use Shared\Enums\Country;
|
||||
|
||||
covers(Country::class);
|
||||
|
||||
describe('country', function (): void {
|
||||
it('has the expected cases with their backing values', function (): void {
|
||||
// THEN
|
||||
expect(Country::FRANCE->value)->toBe('france')
|
||||
->and(Country::BENELUX->value)->toBe('benelux');
|
||||
});
|
||||
|
||||
it('exposes its case name', function (): void {
|
||||
// THEN
|
||||
expect(Country::FRANCE->name)->toBe('FRANCE');
|
||||
});
|
||||
});
|
||||
67
tests/Unit/Domain/Models/Entities/ActorTest.php
Normal file
67
tests/Unit/Domain/Models/Entities/ActorTest.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\Entities;
|
||||
|
||||
use Domain\Models\Entities\Actor;
|
||||
use Domain\Models\ValueObjects\Actor\ActorDisplayName;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(Actor::class);
|
||||
|
||||
describe('actor', function (): void {
|
||||
beforeEach(function (): void {
|
||||
$this->displayName = new ActorDisplayName('John Doe');
|
||||
});
|
||||
|
||||
it('hydrates an actor', function (): void {
|
||||
// GIVEN
|
||||
$id = ActorId::generate();
|
||||
|
||||
// WHEN
|
||||
$actor = Actor::hydrate(
|
||||
$id,
|
||||
$this->displayName
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($actor->id())->toBe($id)
|
||||
->and($actor->displayName)->toBe($this->displayName)
|
||||
->and($actor->updatedAt)->toBeNull()
|
||||
->and($actor->updatedAt())->toBeNull();
|
||||
});
|
||||
|
||||
it('hydrates an actor with an explicit updatedAt date', function (): void {
|
||||
// GIVEN
|
||||
$updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC'));
|
||||
|
||||
// WHEN
|
||||
$actor = Actor::hydrate(
|
||||
ActorId::generate(),
|
||||
$this->displayName,
|
||||
$updatedAt
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($actor->updatedAt)->toBe($updatedAt)
|
||||
->and($actor->updatedAt())->toBe($updatedAt);
|
||||
});
|
||||
|
||||
it('returns the creation date extracted from the id', function (): void {
|
||||
// GIVEN
|
||||
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
|
||||
$id = new ActorId(Uuid::uuid7($expected));
|
||||
$actor = Actor::hydrate(
|
||||
$id,
|
||||
$this->displayName
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$result = $actor->createdAt();
|
||||
|
||||
// THEN
|
||||
expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v'));
|
||||
});
|
||||
});
|
||||
|
|
@ -17,29 +17,39 @@ use Shared\ValueObjects\Rcs;
|
|||
covers(Brand::class);
|
||||
|
||||
describe('brand', function (): void {
|
||||
beforeEach(function (): void {
|
||||
$this->code = new BrandCode('AAA01');
|
||||
$this->name = new BrandName('Name');
|
||||
$this->logo = new PicturePath('files/picture.jpg');
|
||||
$this->rcs = new Rcs('RCS PARIS 111 222 333');
|
||||
$this->contact = new Email('contact@example.com');
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
CarbonImmutable::setTestNow();
|
||||
});
|
||||
|
||||
it('creates a brand', function (): void {
|
||||
// GIVEN
|
||||
$id = BrandId::generate();
|
||||
$code = new BrandCode('AAA01');
|
||||
$name = new BrandName('Name');
|
||||
$logo = new PicturePath('files/picture.jpg');
|
||||
$rcs = new Rcs('RCS PARIS 111 222 333');
|
||||
$contact = new Email('contact@example.com');
|
||||
$id = BrandId::generate();
|
||||
|
||||
// WHEN
|
||||
$brand = Brand::hydrate($id, $code, $name, $logo, $rcs, $contact);
|
||||
$brand = Brand::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->logo,
|
||||
$this->rcs,
|
||||
$this->contact
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($brand->id())->toBe($id)
|
||||
->and($brand->code)->toBe($code)
|
||||
->and($brand->name)->toBe($name)
|
||||
->and($brand->logo)->toBe($logo)
|
||||
->and($brand->rcs)->toBe($rcs)
|
||||
->and($brand->contact)->toBe($contact)
|
||||
->and($brand->code)->toBe($this->code)
|
||||
->and($brand->name)->toBe($this->name)
|
||||
->and($brand->logo)->toBe($this->logo)
|
||||
->and($brand->rcs)->toBe($this->rcs)
|
||||
->and($brand->contact)->toBe($this->contact)
|
||||
->and($brand->updatedAt)->toBeNull()
|
||||
->and($brand->updatedAt())->toBeNull();
|
||||
});
|
||||
|
|
@ -47,15 +57,18 @@ describe('brand', function (): void {
|
|||
it('creates a brand with an explicit updatedAt date', function (): void {
|
||||
// GIVEN
|
||||
$id = BrandId::generate();
|
||||
$code = new BrandCode('AAA01');
|
||||
$name = new BrandName('Name');
|
||||
$logo = new PicturePath('files/picture.jpg');
|
||||
$rcs = new Rcs('RCS PARIS 111 222 333');
|
||||
$contact = new Email('contact@example.com');
|
||||
$updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC'));
|
||||
|
||||
// WHEN
|
||||
$brand = Brand::hydrate($id, $code, $name, $logo, $rcs, $contact, $updatedAt);
|
||||
$brand = Brand::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->logo,
|
||||
$this->rcs,
|
||||
$this->contact,
|
||||
$updatedAt
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($brand->updatedAt)->toBe($updatedAt)
|
||||
|
|
@ -63,38 +76,42 @@ describe('brand', function (): void {
|
|||
});
|
||||
|
||||
it('creates a new brand with a freshly generated id', function (): void {
|
||||
// GIVEN
|
||||
$code = new BrandCode('AAA01');
|
||||
$name = new BrandName('Name');
|
||||
$logo = new PicturePath('files/picture.jpg');
|
||||
$rcs = new Rcs('RCS PARIS 111 222 333');
|
||||
$contact = new Email('contact@example.com');
|
||||
|
||||
// WHEN
|
||||
$brand = Brand::create($code, $name, $logo, $rcs, $contact);
|
||||
// GIVEN / WHEN
|
||||
$brand = Brand::create(
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->logo,
|
||||
$this->rcs,
|
||||
$this->contact
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($brand->id())->toBeInstanceOf(BrandId::class)
|
||||
->and($brand->code)->toBe($code)
|
||||
->and($brand->name)->toBe($name)
|
||||
->and($brand->logo)->toBe($logo)
|
||||
->and($brand->rcs)->toBe($rcs)
|
||||
->and($brand->contact)->toBe($contact)
|
||||
->and($brand->code)->toBe($this->code)
|
||||
->and($brand->name)->toBe($this->name)
|
||||
->and($brand->logo)->toBe($this->logo)
|
||||
->and($brand->rcs)->toBe($this->rcs)
|
||||
->and($brand->contact)->toBe($this->contact)
|
||||
->and($brand->updatedAt)->toBeNull()
|
||||
->and($brand->updatedAt())->toBeNull();
|
||||
});
|
||||
|
||||
it('generates a different id for each created brand', function (): void {
|
||||
// GIVEN
|
||||
$code = new BrandCode('AAA01');
|
||||
$name = new BrandName('Name');
|
||||
$logo = new PicturePath('files/picture.jpg');
|
||||
$rcs = new Rcs('RCS PARIS 111 222 333');
|
||||
$contact = new Email('contact@example.com');
|
||||
|
||||
// WHEN
|
||||
$first = Brand::create($code, $name, $logo, $rcs, $contact);
|
||||
$second = Brand::create($code, $name, $logo, $rcs, $contact);
|
||||
// GIVEN / WHEN
|
||||
$first = Brand::create(
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->logo,
|
||||
$this->rcs,
|
||||
$this->contact
|
||||
);
|
||||
$second = Brand::create(
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->logo,
|
||||
$this->rcs,
|
||||
$this->contact
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($first->id()->equals($second->id()))->toBeFalse();
|
||||
|
|
@ -104,24 +121,27 @@ describe('brand', function (): void {
|
|||
// GIVEN
|
||||
CarbonImmutable::setTestNow('2026-06-15 12:00:00');
|
||||
$id = BrandId::generate();
|
||||
$code = new BrandCode('AAA01');
|
||||
$name = new BrandName('Name');
|
||||
$logoOld = new PicturePath('files/picture-old.jpg');
|
||||
$logoNew = new PicturePath('files/picture-new.jpg');
|
||||
$rcs = new Rcs('RCS PARIS 111 222 333');
|
||||
$contact = new Email('contact@example.com');
|
||||
$brand = Brand::hydrate($id, $code, $name, $logoOld, $rcs, $contact);
|
||||
$brand = Brand::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$logoOld,
|
||||
$this->rcs,
|
||||
$this->contact
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$brand = $brand->changeLogo($logoNew);
|
||||
|
||||
// THEN
|
||||
expect($brand->id())->toBe($id)
|
||||
->and($brand->code)->toBe($code)
|
||||
->and($brand->name)->toBe($name)
|
||||
->and($brand->code)->toBe($this->code)
|
||||
->and($brand->name)->toBe($this->name)
|
||||
->and($brand->logo)->toBe($logoNew)
|
||||
->and($brand->rcs)->toBe($rcs)
|
||||
->and($brand->contact)->toBe($contact)
|
||||
->and($brand->rcs)->toBe($this->rcs)
|
||||
->and($brand->contact)->toBe($this->contact)
|
||||
->and($brand->updatedAt)->not->toBeNull()
|
||||
->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00');
|
||||
});
|
||||
|
|
@ -130,24 +150,27 @@ describe('brand', function (): void {
|
|||
// GIVEN
|
||||
CarbonImmutable::setTestNow('2026-06-15 14:30:00');
|
||||
$id = BrandId::generate();
|
||||
$code = new BrandCode('AAA01');
|
||||
$name = new BrandName('Name');
|
||||
$logo = new PicturePath('files/picture.jpg');
|
||||
$rcsOld = new Rcs('RCS PARIS 111 222 333');
|
||||
$rcsNew = new Rcs('RCS NANTES 444 555 666');
|
||||
$contact = new Email('contact@example.com');
|
||||
$brand = Brand::hydrate($id, $code, $name, $logo, $rcsOld, $contact);
|
||||
$brand = Brand::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->logo,
|
||||
$rcsOld,
|
||||
$this->contact
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$brand = $brand->changeRcs($rcsNew);
|
||||
|
||||
// THEN
|
||||
expect($brand->id())->toBe($id)
|
||||
->and($brand->code)->toBe($code)
|
||||
->and($brand->name)->toBe($name)
|
||||
->and($brand->logo)->toBe($logo)
|
||||
->and($brand->code)->toBe($this->code)
|
||||
->and($brand->name)->toBe($this->name)
|
||||
->and($brand->logo)->toBe($this->logo)
|
||||
->and($brand->rcs)->toBe($rcsNew)
|
||||
->and($brand->contact)->toBe($contact)
|
||||
->and($brand->contact)->toBe($this->contact)
|
||||
->and($brand->updatedAt)->not->toBeNull()
|
||||
->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 14:30:00');
|
||||
});
|
||||
|
|
@ -156,23 +179,26 @@ describe('brand', function (): void {
|
|||
// GIVEN
|
||||
CarbonImmutable::setTestNow('2026-06-15 16:45:00');
|
||||
$id = BrandId::generate();
|
||||
$code = new BrandCode('AAA01');
|
||||
$name = new BrandName('Name');
|
||||
$logo = new PicturePath('files/picture.jpg');
|
||||
$rcs = new Rcs('RCS PARIS 111 222 333');
|
||||
$contactOld = new Email('contact-old@example.com');
|
||||
$contactNew = new Email('contact-new@example.com');
|
||||
$brand = Brand::hydrate($id, $code, $name, $logo, $rcs, $contactOld);
|
||||
$brand = Brand::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->logo,
|
||||
$this->rcs,
|
||||
$contactOld
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$brand = $brand->changeContact($contactNew);
|
||||
|
||||
// THEN
|
||||
expect($brand->id())->toBe($id)
|
||||
->and($brand->code)->toBe($code)
|
||||
->and($brand->name)->toBe($name)
|
||||
->and($brand->logo)->toBe($logo)
|
||||
->and($brand->rcs)->toBe($rcs)
|
||||
->and($brand->code)->toBe($this->code)
|
||||
->and($brand->name)->toBe($this->name)
|
||||
->and($brand->logo)->toBe($this->logo)
|
||||
->and($brand->rcs)->toBe($this->rcs)
|
||||
->and($brand->contact)->toBe($contactNew)
|
||||
->and($brand->updatedAt)->not->toBeNull()
|
||||
->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 16:45:00');
|
||||
|
|
@ -184,11 +210,11 @@ describe('brand', function (): void {
|
|||
$id = new BrandId(Uuid::uuid7($expected));
|
||||
$brand = Brand::hydrate(
|
||||
$id,
|
||||
new BrandCode('AAA01'),
|
||||
new BrandName('Name'),
|
||||
new PicturePath('files/picture.jpg'),
|
||||
new Rcs('RCS PARIS 111 222 333'),
|
||||
new Email('contact@example.com'),
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->logo,
|
||||
$this->rcs,
|
||||
$this->contact
|
||||
);
|
||||
|
||||
// WHEN
|
||||
|
|
|
|||
263
tests/Unit/Domain/Models/Entities/CampaignRequestTest.php
Normal file
263
tests/Unit/Domain/Models/Entities/CampaignRequestTest.php
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\GS1;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Email;
|
||||
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.');
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
CarbonImmutable::setTestNow();
|
||||
});
|
||||
|
||||
it('hydrates a campaign request with all its fields', function (): void {
|
||||
// GIVEN
|
||||
$id = CampaignRequestId::generate();
|
||||
|
||||
// WHEN
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->applicant,
|
||||
$this->campaignType,
|
||||
$this->requestedCoupon,
|
||||
$this->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->updatedAt)->toBeNull()
|
||||
->and($campaignRequest->updatedAt())->toBeNull();
|
||||
});
|
||||
|
||||
it('hydrates a campaign request with an explicit updatedAt date', function (): void {
|
||||
// GIVEN
|
||||
$updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC'));
|
||||
|
||||
// WHEN
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->applicant,
|
||||
$this->campaignType,
|
||||
$this->requestedCoupon,
|
||||
$this->status,
|
||||
null,
|
||||
$updatedAt,
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($campaignRequest->updatedAt)->toBe($updatedAt)
|
||||
->and($campaignRequest->updatedAt())->toBe($updatedAt);
|
||||
});
|
||||
|
||||
it('hydrates a rejected campaign request with a rejection reason', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->applicant,
|
||||
$this->campaignType,
|
||||
$this->requestedCoupon,
|
||||
$this->rejectedStatus,
|
||||
$this->rejectionReason,
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($campaignRequest->status)->toBe($this->rejectedStatus)
|
||||
->and($campaignRequest->rejectionReason)->toBe($this->rejectionReason);
|
||||
});
|
||||
|
||||
it('rejects a rejected campaign request without a rejection reason', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$action = fn (): CampaignRequest => CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->applicant,
|
||||
$this->campaignType,
|
||||
$this->requestedCoupon,
|
||||
$this->rejectedStatus,
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(
|
||||
\DomainException::class,
|
||||
'A rejection reason is required when the campaign request is rejected.'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a non-rejected campaign request with a rejection reason', function (): void {
|
||||
// WHEN
|
||||
$action = fn (): CampaignRequest => CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->applicant,
|
||||
$this->campaignType,
|
||||
$this->requestedCoupon,
|
||||
$this->status,
|
||||
$this->rejectionReason,
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(
|
||||
\DomainException::class,
|
||||
'A rejection reason can only be set when the campaign request is rejected.'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a campaign request and sets the rejection reason and updatedAt', function (): void {
|
||||
// GIVEN
|
||||
CarbonImmutable::setTestNow('2026-06-15 12:00:00');
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->applicant,
|
||||
$this->campaignType,
|
||||
$this->requestedCoupon,
|
||||
$this->status,
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$rejected = $campaignRequest->reject($this->rejectionReason);
|
||||
|
||||
// THEN
|
||||
expect($rejected->status)->toBe(CampaignRequestStatus::REJECTED)
|
||||
->and($rejected->rejectionReason)->toBe($this->rejectionReason)
|
||||
->and($rejected->id())->toBe($campaignRequest->id())
|
||||
->and($rejected->brandCode)->toBe($campaignRequest->brandCode)
|
||||
->and($rejected->name)->toBe($campaignRequest->name)
|
||||
->and($rejected->applicant)->toBe($campaignRequest->applicant)
|
||||
->and($rejected->campaignType)->toBe($campaignRequest->campaignType)
|
||||
->and($rejected->requestedCoupon)->toBe($campaignRequest->requestedCoupon)
|
||||
->and($rejected->updatedAt)->not->toBeNull()
|
||||
->and($rejected->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00');
|
||||
});
|
||||
|
||||
it('rejects rejecting a transformed campaign request', function (): void {
|
||||
// GIVEN
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->applicant,
|
||||
$this->campaignType,
|
||||
$this->requestedCoupon,
|
||||
$this->transformedStatus,
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): CampaignRequest => $campaignRequest->reject($this->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 {
|
||||
// GIVEN
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->applicant,
|
||||
$this->campaignType,
|
||||
$this->requestedCoupon,
|
||||
$this->rejectedStatus,
|
||||
new RejectionReason('First reason.'),
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): CampaignRequest => $campaignRequest->reject(new RejectionReason('Second reason.'));
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.');
|
||||
});
|
||||
|
||||
it('returns the creation date extracted from the id', function (): void {
|
||||
// GIVEN
|
||||
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
|
||||
$id = new CampaignRequestId(Uuid::uuid7($expected));
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->applicant,
|
||||
$this->campaignType,
|
||||
$this->requestedCoupon,
|
||||
$this->status,
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$result = $campaignRequest->createdAt();
|
||||
|
||||
// THEN
|
||||
expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v'));
|
||||
});
|
||||
});
|
||||
204
tests/Unit/Domain/Models/Entities/CampaignTest.php
Normal file
204
tests/Unit/Domain/Models/Entities/CampaignTest.php
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
<?php
|
||||
|
||||
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\CampaignId;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponQuantity;
|
||||
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();
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
CarbonImmutable::setTestNow();
|
||||
});
|
||||
|
||||
it('creates a campaign', function (): void {
|
||||
// GIVEN
|
||||
$id = CampaignId::generate();
|
||||
|
||||
// WHEN
|
||||
$campaign = Campaign::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->publication,
|
||||
$this->printAllAtOnce,
|
||||
$this->coupons,
|
||||
$this->brandCode,
|
||||
$this->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->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 {
|
||||
// GIVEN
|
||||
$id = CampaignId::generate();
|
||||
$updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC'));
|
||||
|
||||
// WHEN
|
||||
$campaign = Campaign::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->publication,
|
||||
$this->printAllAtOnce,
|
||||
$this->coupons,
|
||||
$this->brandCode,
|
||||
$this->actorId,
|
||||
$updatedAt
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($campaign->updatedAt)->toBe($updatedAt)
|
||||
->and($campaign->updatedAt())->toBe($updatedAt);
|
||||
});
|
||||
|
||||
it('creates a new campaign with a freshly generated id', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$campaign = Campaign::create(
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->publication,
|
||||
$this->printAllAtOnce,
|
||||
$this->coupons,
|
||||
$this->brandCode,
|
||||
$this->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->updatedAt)->toBeNull()
|
||||
->and($campaign->updatedAt())->toBeNull();
|
||||
});
|
||||
|
||||
it('generates a different id for each created campaign', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$firstCampaign = Campaign::create(
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->publication,
|
||||
$this->printAllAtOnce,
|
||||
$this->coupons,
|
||||
$this->brandCode,
|
||||
$this->actorId
|
||||
);
|
||||
$secondCampaign = Campaign::create(
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->publication,
|
||||
$this->printAllAtOnce,
|
||||
$this->coupons,
|
||||
$this->brandCode,
|
||||
$this->actorId
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($firstCampaign->id()->equals($secondCampaign->id()))->toBeFalse();
|
||||
});
|
||||
|
||||
it('returns the creation date extracted from the id', function (): void {
|
||||
// GIVEN
|
||||
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
|
||||
$id = new CampaignId(Uuid::uuid7($expected));
|
||||
$campaign = Campaign::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->publication,
|
||||
$this->printAllAtOnce,
|
||||
$this->coupons,
|
||||
$this->brandCode,
|
||||
$this->actorId
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$result = $campaign->createdAt();
|
||||
|
||||
// THEN
|
||||
expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v'));
|
||||
});
|
||||
|
||||
it('rejects a campaign with a non-coupon element', function (): void {
|
||||
// GIVEN
|
||||
$id = CampaignId::generate();
|
||||
$coupons = ['PC1'];
|
||||
|
||||
// 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.');
|
||||
});
|
||||
});
|
||||
439
tests/Unit/Domain/Models/Entities/CouponTest.php
Normal file
439
tests/Unit/Domain/Models/Entities/CouponTest.php
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\Entities;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTime;
|
||||
use Domain\Enums\BarcodeFormat;
|
||||
use Shared\Enums\Country;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponId;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\CouponPicture;
|
||||
use Domain\Models\ValueObjects\GS1;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\Enums\Currency;
|
||||
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);
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
CarbonImmutable::setTestNow();
|
||||
});
|
||||
|
||||
it('creates a coupon', function (): 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,
|
||||
);
|
||||
|
||||
// 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->updatedAt)->toBeNull()
|
||||
->and($coupon->updatedAt())->toBeNull();
|
||||
});
|
||||
|
||||
it('hydrates a coupon without gs1', 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,
|
||||
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
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($coupon->availabilityCalendar)->toBeNull()
|
||||
->and($coupon->products)->toBe($this->products)
|
||||
->and($coupon->gs1)->toBe($this->gs1);
|
||||
});
|
||||
|
||||
it('hydrates a coupon without products', function (): void {
|
||||
// GIVEN
|
||||
$id = CouponId::generate();
|
||||
|
||||
// WHEN
|
||||
$coupon = Coupon::hydrate(
|
||||
id: $id,
|
||||
code: $this->code,
|
||||
name: $this->name,
|
||||
amount: $this->amount,
|
||||
picture: $this->picture,
|
||||
diffusion: $this->diffusion,
|
||||
validity: $this->validity,
|
||||
couponValidityInformation: $this->couponValidityInformation,
|
||||
couponLegalInformation: $this->couponLegalInformation,
|
||||
processingCode: $this->processingCode,
|
||||
brandCode: $this->brandCode,
|
||||
createdBy: $this->createdBy,
|
||||
availabilityCalendar: $this->availabilityCalendar,
|
||||
gs1: $this->gs1
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($coupon->products)->toBe([])
|
||||
->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar)
|
||||
->and($coupon->gs1)->toBe($this->gs1);
|
||||
});
|
||||
|
||||
it('hydrates a coupon with only required fields', function (): void {
|
||||
// GIVEN
|
||||
$id = CouponId::generate();
|
||||
|
||||
// WHEN
|
||||
$coupon = Coupon::hydrate(
|
||||
id: $id,
|
||||
code: $this->code,
|
||||
name: $this->name,
|
||||
amount: $this->amount,
|
||||
picture: $this->picture,
|
||||
diffusion: $this->diffusion,
|
||||
validity: $this->validity,
|
||||
couponValidityInformation: $this->couponValidityInformation,
|
||||
couponLegalInformation: $this->couponLegalInformation,
|
||||
processingCode: $this->processingCode,
|
||||
brandCode: $this->brandCode,
|
||||
createdBy: $this->createdBy,
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($coupon->id())->toBe($id)
|
||||
->and($coupon->products)->toBe([])
|
||||
->and($coupon->availabilityCalendar)->toBeNull()
|
||||
->and($coupon->gs1)->toBeNull()
|
||||
->and($coupon->updatedAt)->toBeNull()
|
||||
->and($coupon->updatedAt())->toBeNull();
|
||||
});
|
||||
|
||||
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 {
|
||||
// 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.');
|
||||
});
|
||||
|
||||
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 {
|
||||
// GIVEN
|
||||
$id = CouponId::generate();
|
||||
$updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC'));
|
||||
|
||||
// WHEN
|
||||
$coupon = Coupon::hydrate(
|
||||
$id,
|
||||
$this->code,
|
||||
$this->name,
|
||||
$this->amount,
|
||||
$this->picture,
|
||||
$this->diffusion,
|
||||
$this->validity,
|
||||
$this->couponValidityInformation,
|
||||
$this->couponLegalInformation,
|
||||
$this->processingCode,
|
||||
$this->brandCode,
|
||||
$this->createdBy,
|
||||
$this->products,
|
||||
$this->availabilityCalendar,
|
||||
$this->gs1,
|
||||
$updatedAt
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($coupon->updatedAt)->toBe($updatedAt)
|
||||
->and($coupon->updatedAt())->toBe($updatedAt);
|
||||
});
|
||||
|
||||
it('creates a new coupon with a freshly generated id', function (): void {
|
||||
// GIVEN / 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,
|
||||
);
|
||||
|
||||
// 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->updatedAt)->toBeNull()
|
||||
->and($coupon->updatedAt())->toBeNull();
|
||||
});
|
||||
|
||||
it('generates a different id for each created coupon', function (): 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,
|
||||
);
|
||||
$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,
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($firstCoupon->id()->equals($secondCoupon->id()))->toBeFalse();
|
||||
});
|
||||
|
||||
it('returns the creation date extracted from the id', function (): void {
|
||||
// GIVEN
|
||||
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
|
||||
$id = new CouponId(Uuid::uuid7($expected));
|
||||
$coupon = Coupon::hydrate(
|
||||
$id,
|
||||
$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,
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$result = $coupon->createdAt();
|
||||
|
||||
// THEN
|
||||
expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v'));
|
||||
});
|
||||
});
|
||||
150
tests/Unit/Domain/Models/Entities/StockTest.php
Normal file
150
tests/Unit/Domain/Models/Entities/StockTest.php
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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\Stock\StockAdjustment;
|
||||
use Domain\Models\ValueObjects\Stock\StockAmount;
|
||||
use Domain\Models\ValueObjects\Stock\StockId;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(Stock::class);
|
||||
|
||||
describe('stock', function (): void {
|
||||
beforeEach(function (): void {
|
||||
$this->stockAmount = new StockAmount(10);
|
||||
$this->campaignCode = new CampaignCode('AAA01');
|
||||
$this->processingCode = new ProcessingCode('PC1');
|
||||
$this->stockAdjustment = [new StockAdjustment(5, 'Inventory correction', ActorId::generate())];
|
||||
});
|
||||
|
||||
it('hydrates a stock', function (): void {
|
||||
// GIVEN
|
||||
$id = StockId::generate();
|
||||
|
||||
// WHEN
|
||||
$stock = Stock::hydrate(
|
||||
$id,
|
||||
$this->stockAmount,
|
||||
$this->campaignCode,
|
||||
$this->processingCode,
|
||||
$this->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->updatedAt)->toBeNull()
|
||||
->and($stock->updatedAt())->toBeNull();
|
||||
});
|
||||
|
||||
it('hydrates a stock with several stock adjustments', function (): void {
|
||||
// GIVEN
|
||||
$id = StockId::generate();
|
||||
$stockAdjustment = [
|
||||
new StockAdjustment(5, 'Inventory correction', ActorId::generate()),
|
||||
new StockAdjustment(10, 'Inventory correction', ActorId::generate()),
|
||||
];
|
||||
|
||||
// WHEN
|
||||
$stock = Stock::hydrate(
|
||||
$id,
|
||||
$this->stockAmount,
|
||||
$this->campaignCode,
|
||||
$this->processingCode,
|
||||
$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($stockAdjustment)
|
||||
->and($stock->updatedAt)->toBeNull()
|
||||
->and($stock->updatedAt())->toBeNull();
|
||||
});
|
||||
|
||||
it('hydrates a stock without stock adjustement', function (): void {
|
||||
// GIVEN
|
||||
$id = StockId::generate();
|
||||
|
||||
// WHEN
|
||||
$stock = Stock::hydrate(
|
||||
$id,
|
||||
$this->stockAmount,
|
||||
$this->campaignCode,
|
||||
$this->processingCode
|
||||
);
|
||||
|
||||
// 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([])
|
||||
->and($stock->updatedAt)->toBeNull()
|
||||
->and($stock->updatedAt())->toBeNull();
|
||||
});
|
||||
|
||||
it('rejects a stock with a non-stock-adjustment element', function (): void {
|
||||
// GIVEN
|
||||
$id = StockId::generate();
|
||||
$stockAdjustment = [new StockAmount(5)];
|
||||
|
||||
// WHEN / THEN
|
||||
expect(fn (): Stock => Stock::hydrate(
|
||||
$id,
|
||||
$this->stockAmount,
|
||||
$this->campaignCode,
|
||||
$this->processingCode,
|
||||
$stockAdjustment,
|
||||
))->toThrow(\DomainException::class, 'Stock adjustment must be StockAdjustment instances.');
|
||||
});
|
||||
|
||||
it('hydrates a stock with an explicit updatedAt date', function (): void {
|
||||
// GIVEN
|
||||
$updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC'));
|
||||
|
||||
// WHEN
|
||||
$stock = Stock::hydrate(
|
||||
StockId::generate(),
|
||||
$this->stockAmount,
|
||||
$this->campaignCode,
|
||||
$this->processingCode,
|
||||
$this->stockAdjustment,
|
||||
$updatedAt,
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($stock->updatedAt)->toBe($updatedAt)
|
||||
->and($stock->updatedAt())->toBe($updatedAt);
|
||||
});
|
||||
|
||||
it('returns the creation date extracted from the id', function (): void {
|
||||
// GIVEN
|
||||
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
|
||||
$id = new StockId(Uuid::uuid7($expected));
|
||||
$stock = Stock::hydrate(
|
||||
$id,
|
||||
$this->stockAmount,
|
||||
$this->campaignCode,
|
||||
$this->processingCode,
|
||||
$this->stockAdjustment
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$result = $stock->createdAt();
|
||||
|
||||
// THEN
|
||||
expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v'));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects\Actor;
|
||||
|
||||
use Domain\Models\ValueObjects\Actor\ActorDisplayName;
|
||||
|
||||
covers(ActorDisplayName::class);
|
||||
|
||||
describe('actor displayname', function (): void {
|
||||
beforeEach(function (): void {
|
||||
$this->value = 'Johnny';
|
||||
$this->emptyValue = '';
|
||||
});
|
||||
|
||||
it('creates a valid actor displayname', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$name = new ActorDisplayName(
|
||||
$this->value
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($name->value)->toBe($this->value);
|
||||
});
|
||||
|
||||
it('rejects invalid actor displayname', function (): void {
|
||||
// GIVEN / WHEN / THEN
|
||||
expect(fn (): ActorDisplayName => new ActorDisplayName(
|
||||
$this->emptyValue
|
||||
))->toThrow(\DomainException::class);
|
||||
});
|
||||
});
|
||||
28
tests/Unit/Domain/Models/ValueObjects/Actor/ActorIdTest.php
Normal file
28
tests/Unit/Domain/Models/ValueObjects/Actor/ActorIdTest.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects\Actor;
|
||||
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
|
||||
covers(ActorId::class);
|
||||
|
||||
describe('actor id', function (): void {
|
||||
it('generates a unique actor id', function (): void {
|
||||
// WHEN
|
||||
$id = ActorId::generate();
|
||||
|
||||
// THEN
|
||||
expect($id)->toBeInstanceOf(ActorId::class);
|
||||
});
|
||||
|
||||
it('generates different ids on each call', function (): void {
|
||||
// WHEN
|
||||
$first = ActorId::generate();
|
||||
$second = ActorId::generate();
|
||||
|
||||
// THEN
|
||||
expect((string) $first)->not->toBe((string) $second);
|
||||
});
|
||||
});
|
||||
|
|
@ -9,23 +9,26 @@ use Domain\Models\ValueObjects\Brand\BrandName;
|
|||
covers(BrandName::class);
|
||||
|
||||
describe('brand name', function (): void {
|
||||
it('creates a valid brand name', function (): void {
|
||||
// GIVEN
|
||||
$value = 'Name';
|
||||
beforeEach(function (): void {
|
||||
$this->value = 'Name';
|
||||
$this->emptyValue = '';
|
||||
});
|
||||
|
||||
// WHEN
|
||||
$name = new BrandName($value);
|
||||
it('creates a valid brand name', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$name = new BrandName(
|
||||
$this->value
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($name->value)->toBe('Name');
|
||||
expect($name->value)->toBe($this->value);
|
||||
});
|
||||
|
||||
it('rejects invalid brand name', function (): void {
|
||||
// GIVEN
|
||||
$value = '';
|
||||
|
||||
// GIVEN / THEN
|
||||
expect(fn (): BrandName => new BrandName($value))->toThrow(\DomainException::class);
|
||||
// GIVEN / WHEN / THEN
|
||||
expect(fn (): BrandName => new BrandName(
|
||||
$this->emptyValue
|
||||
))->toThrow(\DomainException::class);
|
||||
});
|
||||
|
||||
it('rejects a name that exceeds the maximum allowed length', function (): void {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects\Campaign;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
|
||||
covers(CampaignCode::class);
|
||||
|
||||
describe('campaign code', function (): void {
|
||||
beforeEach(function (): void {
|
||||
$this->code = 'Name';
|
||||
$this->emptyCode = '';
|
||||
});
|
||||
|
||||
it('creates a valid campaign code', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$name = new CampaignCode($this->code);
|
||||
|
||||
// THEN
|
||||
expect($name->code)->toBe($this->code);
|
||||
});
|
||||
|
||||
it('rejects invalid campaign code', function (): void {
|
||||
// GIVEN / WHEN / THEN
|
||||
expect(fn (): CampaignCode => new CampaignCode(
|
||||
$this->emptyCode
|
||||
))->toThrow(\DomainException::class);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
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\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);
|
||||
});
|
||||
|
||||
it('creates a valid campaign coupon', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$campaignCoupon = new CampaignCoupon(
|
||||
$this->processingCode,
|
||||
$this->quantity,
|
||||
$this->availableIn,
|
||||
$this->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);
|
||||
});
|
||||
|
||||
it('creates a valid campaign coupon whitout warning threshold quantity', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$campaignCoupon = new CampaignCoupon(
|
||||
processingCode: $this->processingCode,
|
||||
quantity: $this->quantity,
|
||||
availableIn: $this->availableIn,
|
||||
printRules: $this->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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects\Campaign;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignId;
|
||||
|
||||
covers(CampaignId::class);
|
||||
|
||||
describe('campaign', function (): void {
|
||||
it('generates a unique campaign id', function (): void {
|
||||
// WHEN
|
||||
$id = CampaignId::generate();
|
||||
|
||||
// THEN
|
||||
expect($id)->toBeInstanceOf(CampaignId::class);
|
||||
});
|
||||
|
||||
it('generates different ids on each call', function (): void {
|
||||
// WHEN
|
||||
$first = CampaignId::generate();
|
||||
$second = CampaignId::generate();
|
||||
|
||||
// THEN
|
||||
expect((string) $first)->not->toBe((string) $second);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects\Campaign;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
|
||||
covers(CampaignName::class);
|
||||
|
||||
describe('campaign name', function (): void {
|
||||
beforeEach(function (): void {
|
||||
$this->value = 'Name';
|
||||
$this->emptyValue = '';
|
||||
});
|
||||
|
||||
it('creates a valid campaign name', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$name = new CampaignName($this->value);
|
||||
|
||||
// THEN
|
||||
expect($name->value)->toBe($this->value);
|
||||
});
|
||||
|
||||
it('rejects invalid campaign name', function (): void {
|
||||
// GIVEN / WHEN / THEN
|
||||
expect(fn (): CampaignName => new CampaignName(
|
||||
$this->emptyValue
|
||||
))->toThrow(\DomainException::class);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
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');
|
||||
});
|
||||
|
||||
it('creates a valid applicant', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$applicant = new Applicant(
|
||||
$this->firstname,
|
||||
$this->lastname,
|
||||
$this->email
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($applicant)->toBeInstanceOf(Applicant::class)
|
||||
->and($applicant->firstname)->toBe($this->firstname)
|
||||
->and($applicant->lastname)->toBe($this->lastname)
|
||||
->and($applicant->email)->toBe($this->email);
|
||||
});
|
||||
|
||||
it('rejects an empty or whitespace-only firstname', function (string $firstname): void {
|
||||
// WHEN
|
||||
$action = fn (): Applicant => new Applicant(
|
||||
$firstname,
|
||||
$this->lastname,
|
||||
$this->email
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Applicant firstname cannot be empty.');
|
||||
})->with([
|
||||
'empty' => '',
|
||||
'spaces' => ' ',
|
||||
]);
|
||||
|
||||
it('rejects a firstname that exceeds the maximum allowed length', function (): void {
|
||||
// GIVEN
|
||||
$firstname = str_repeat('a', 201);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Applicant => new Applicant(
|
||||
$firstname,
|
||||
$this->lastname,
|
||||
$this->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 {
|
||||
// GIVEN
|
||||
$firstname = str_repeat('a', 200);
|
||||
|
||||
// WHEN
|
||||
$applicant = new Applicant(
|
||||
$firstname,
|
||||
$this->lastname,
|
||||
$this->email
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect(mb_strlen($applicant->firstname))->toBe(200);
|
||||
});
|
||||
|
||||
it('rejects an empty or whitespace-only lastname', function (string $lastname): void {
|
||||
// WHEN
|
||||
$action = fn (): Applicant => new Applicant(
|
||||
$this->firstname,
|
||||
$lastname,
|
||||
$this->email
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Applicant lastname cannot be empty.');
|
||||
})->with([
|
||||
'empty' => '',
|
||||
'spaces' => ' ',
|
||||
]);
|
||||
|
||||
it('rejects a lastname that exceeds the maximum allowed length', function (): void {
|
||||
// GIVEN
|
||||
$lastname = str_repeat('a', 201);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Applicant => new Applicant(
|
||||
$this->firstname,
|
||||
$lastname,
|
||||
$this->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 {
|
||||
// GIVEN
|
||||
$lastname = str_repeat('a', 200);
|
||||
|
||||
// WHEN
|
||||
$applicant = new Applicant(
|
||||
$this->firstname,
|
||||
$lastname,
|
||||
$this->email
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect(mb_strlen($applicant->lastname))->toBe(200);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestFilters;
|
||||
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;
|
||||
});
|
||||
|
||||
it('creates filters with both fields set', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$filters = new CampaignRequestFilters(
|
||||
brandCode: $this->brandCode,
|
||||
status: $this->status
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($filters->brandCode)->toBe($this->brandCode)
|
||||
->and($filters->status)->toBe($this->status);
|
||||
});
|
||||
|
||||
it('creates filters with only brandCode set', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$filters = new CampaignRequestFilters(
|
||||
brandCode: $this->brandCode
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($filters->brandCode)->toBe($this->brandCode)
|
||||
->and($filters->status)->toBeNull();
|
||||
});
|
||||
|
||||
it('creates filters with only status set', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$filters = new CampaignRequestFilters(
|
||||
status: $this->status
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($filters->brandCode)->toBeNull()
|
||||
->and($filters->status)->toBe($this->status);
|
||||
});
|
||||
|
||||
it('rejects filters without any field set', function (): void {
|
||||
// WHEN / THEN
|
||||
expect(fn (): CampaignRequestFilters => new CampaignRequestFilters())
|
||||
->toThrow(\InvalidArgumentException::class, 'CampaignRequestFilters require at least a brand code or a status.');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
||||
|
||||
covers(CampaignRequestId::class);
|
||||
|
||||
describe('campaign request id', function (): void {
|
||||
it('generates a unique campaign request id', function (): void {
|
||||
// WHEN
|
||||
$id = CampaignRequestId::generate();
|
||||
|
||||
// THEN
|
||||
expect($id)->toBeInstanceOf(CampaignRequestId::class);
|
||||
});
|
||||
|
||||
it('generates different ids on each call', function (): void {
|
||||
// WHEN
|
||||
$first = CampaignRequestId::generate();
|
||||
$second = CampaignRequestId::generate();
|
||||
|
||||
// THEN
|
||||
expect((string) $first)->not->toBe((string) $second);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortClause;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria;
|
||||
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 = [];
|
||||
});
|
||||
|
||||
|
||||
it('creates a valid campaign request sort criteria with one clause', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$campaignRequestSortCriteria = new CampaignRequestSortCriteria(
|
||||
$this->singleClauses
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($campaignRequestSortCriteria->clauses)->toBe($this->singleClauses);
|
||||
});
|
||||
|
||||
it('creates a valid campaign request sort criteria with several clauses', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$campaignRequestSortCriteria = new CampaignRequestSortCriteria(
|
||||
$this->clauses
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($campaignRequestSortCriteria->clauses)->toBe($this->clauses);
|
||||
});
|
||||
|
||||
it('rejects a campaign request sort criteria without clause', function (): void {
|
||||
// GIVEN / WHEN / THEN
|
||||
expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($this->emptyClauses))
|
||||
->toThrow(\DomainException::class, 'Campaign Request Sort Criteria requires at least one clause.');
|
||||
});
|
||||
|
||||
it('rejects a campaign request sort criteria with a non-clause element', function (): void {
|
||||
// GIVEN / WHEN / THEN
|
||||
expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($this->invalidClauses))
|
||||
->toThrow(\DomainException::class, 'CampaignRequestSortCriteria clauses must be CampaignRequestSortClause instances.');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects\CampaignRequest;
|
||||
|
||||
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
|
||||
|
||||
covers(RejectionReason::class);
|
||||
|
||||
describe('rejection reason', function (): void {
|
||||
beforeEach(function (): void {
|
||||
$this->value = 'Missing legal documents.';
|
||||
});
|
||||
|
||||
it('creates a valid rejection reason', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$reason = new RejectionReason($this->value);
|
||||
|
||||
// THEN
|
||||
expect($reason)->toBeInstanceOf(RejectionReason::class)
|
||||
->and($reason->value)->toBe($this->value);
|
||||
});
|
||||
|
||||
it('rejects an empty or whitespace-only reason', function (string $value): void {
|
||||
// WHEN
|
||||
$action = fn (): RejectionReason => new RejectionReason($value);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Rejection reason cannot be empty.');
|
||||
})->with([
|
||||
'empty' => '',
|
||||
'spaces' => ' ',
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
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 = [];
|
||||
});
|
||||
|
||||
it('creates a valid coupon availability calendar with one period', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$couponAvailabilityCalendar = new CouponAvailabilityCalendar(
|
||||
$this->singlePeriods
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($couponAvailabilityCalendar->periods)->toBe($this->singlePeriods);
|
||||
});
|
||||
|
||||
it('creates a valid coupon availability calendar with several periods', function (): void {
|
||||
// GIVEN / WHEN
|
||||
$couponAvailabilityCalendar = new CouponAvailabilityCalendar(
|
||||
$this->periods
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($couponAvailabilityCalendar->periods)->toBe($this->periods);
|
||||
});
|
||||
|
||||
it('rejects a coupon availability calendar without period', function (): void {
|
||||
// GIVEN / WHEN / THEN
|
||||
expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar(
|
||||
$this->emptyPeriods
|
||||
))->toThrow(\DomainException::class);
|
||||
});
|
||||
|
||||
it('rejects a coupon availability calendar with a non-period element', function (): void {
|
||||
// GIVEN / WHEN / THEN
|
||||
expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar(
|
||||
$this->invalidPeriods
|
||||
))->toThrow(\DomainException::class, 'CouponAvailabilityCalendar periods must be Period instances.');
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue