Merge branch 'coupon' into 'refonte'
Coupon See merge request ldc-web/projets/mesbr!57
This commit is contained in:
commit
53fb12f20f
162 changed files with 6511 additions and 2198 deletions
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Application\UseCases\Brand\Create;
|
||||
|
||||
use Domain\Exceptions\BrandCodeAlreadyExistsException;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Domain\Ports\Services\PictureStorage;
|
||||
|
|
@ -28,7 +27,7 @@ final readonly class CreateBrand implements UseCase, Transactional
|
|||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
if ($this->repository->existsByCode($input->code)) {
|
||||
throw new BrandCodeAlreadyExistsException($input->code);
|
||||
throw new \DomainException(sprintf('Brand code "%s" already exists.', $input->code->value));
|
||||
}
|
||||
|
||||
$logoPath = $this->pictureStorage->store($input->logo);
|
||||
|
|
@ -49,7 +48,6 @@ final readonly class CreateBrand implements UseCase, Transactional
|
|||
logo: $brand->logo,
|
||||
rcs: $brand->rcs,
|
||||
contact: $brand->contact,
|
||||
createdAt: $brand->createdAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ final readonly class Output implements UseCaseOutput
|
|||
public PicturePath $logo,
|
||||
public Rcs $rcs,
|
||||
public Email $contact,
|
||||
public \DateTimeInterface $createdAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ final readonly class FindBrand implements UseCase
|
|||
rcs: $brand->rcs,
|
||||
contact: $brand->contact,
|
||||
createdAt: $brand->createdAt(),
|
||||
updatedAt: $brand->updatedAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ final readonly class BrandFound implements Output
|
|||
public Rcs $rcs,
|
||||
public Email $contact,
|
||||
public \DateTimeInterface $createdAt,
|
||||
public ?\DateTimeInterface $updatedAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ final readonly class BrandUpdated implements Output
|
|||
public PicturePath $logo,
|
||||
public Rcs $rcs,
|
||||
public Email $contact,
|
||||
public \DateTimeInterface $createdAt,
|
||||
public ?\DateTimeInterface $updatedAt = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@ final readonly class UpdateBrand implements UseCase, Transactional
|
|||
logo: $brand->logo,
|
||||
rcs: $brand->rcs,
|
||||
contact: $brand->contact,
|
||||
createdAt: $brand->createdAt(),
|
||||
updatedAt: $brand->updatedAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Application\UseCases\CampaignRequest\Create;
|
||||
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Domain\Ports\Repositories\CampaignRequestRepository;
|
||||
|
|
@ -27,8 +26,8 @@ final readonly class CreateCampaignRequest implements UseCase, Transactional
|
|||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
if (!$this->brandRepository->existsByCode($input->brandCode)) {
|
||||
throw new BrandNotFoundException($input->brandCode);
|
||||
if (! $this->brandRepository->existsByCode($input->brandCode)) {
|
||||
throw new \DomainException(sprintf('Brand with code "%s" was not found.', $input->brandCode->value));
|
||||
}
|
||||
|
||||
$campaignRequest = CampaignRequest::create(
|
||||
|
|
@ -49,7 +48,6 @@ final readonly class CreateCampaignRequest implements UseCase, Transactional
|
|||
campaignType: $campaignRequest->campaignType,
|
||||
requestedCoupon: $campaignRequest->requestedCoupon,
|
||||
status: $campaignRequest->status,
|
||||
createdAt: $campaignRequest->createdAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ final readonly class Output implements UseCaseOutput
|
|||
public CampaignRequestType $campaignType,
|
||||
public RequestedCoupon $requestedCoupon,
|
||||
public CampaignRequestStatus $status,
|
||||
public \DateTimeInterface $createdAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ namespace Application\UseCases\CampaignRequest\Find;
|
|||
use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestFound;
|
||||
use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestNotFound;
|
||||
use Application\UseCases\CampaignRequest\Find\Output\Output;
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
|
|
@ -39,7 +38,7 @@ final readonly class FindCampaignRequest implements UseCase
|
|||
$brand = $this->brandRepository->findByCode($campaignRequest->brandCode);
|
||||
|
||||
if (! $brand instanceof Brand) {
|
||||
throw new BrandNotFoundException($campaignRequest->brandCode);
|
||||
throw new \DomainException(sprintf('Brand with code "%s" was not found.', $campaignRequest->brandCode->value));
|
||||
}
|
||||
|
||||
return new CampaignRequestFound(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ final readonly class CampaignRequestRejected implements Output
|
|||
public CampaignRequestId $id,
|
||||
public CampaignRequestStatus $status,
|
||||
public RejectionReason $rejectionReason,
|
||||
public ?\DateTimeInterface $updatedAt = null
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ final readonly class RejectCampaignRequest implements UseCase, Transactional
|
|||
id: $campaignRequest->id(),
|
||||
status: $campaignRequest->status,
|
||||
rejectionReason:$campaignRequest->rejectionReason(),
|
||||
updatedAt: $campaignRequest->updatedAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
72
src/Application/UseCases/Coupon/Create/CreateCoupon.php
Normal file
72
src/Application/UseCases/Coupon/Create/CreateCoupon.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Create;
|
||||
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\DDD\UseCases\UseCase;
|
||||
use Shared\DDD\UseCases\Transactional;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements UseCase<\Application\UseCases\Coupon\Create\Input, \Application\UseCases\Coupon\Create\Output> */
|
||||
final readonly class CreateCoupon implements UseCase, Transactional
|
||||
{
|
||||
public function __construct(
|
||||
private CouponRepository $couponRepository,
|
||||
private BrandRepository $brandRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
if (! $this->brandRepository->existsByCode($input->brandCode)) {
|
||||
throw new \DomainException(sprintf('Brand code "%s" was not found.', $input->brandCode->value));
|
||||
}
|
||||
|
||||
if ($this->couponRepository->existsByCode($input->code)) {
|
||||
throw new \DomainException(sprintf('Coupon code "%s" already exists.', $input->code->value));
|
||||
}
|
||||
|
||||
$coupon = Coupon::create(
|
||||
code: $input->code,
|
||||
name: $input->name,
|
||||
amount: $input->amount,
|
||||
picture: $input->picture,
|
||||
diffusion: $input->diffusion,
|
||||
validityInformations: $input->validityInformations,
|
||||
legalInformations: $input->legalInformations,
|
||||
processingCode: $input->processingCode,
|
||||
brandCode: $input->brandCode,
|
||||
createdBy: $input->createdBy,
|
||||
products: $input->products,
|
||||
movements: $input->movements,
|
||||
availabilityCalendar: $input->availabilityCalendar,
|
||||
);
|
||||
|
||||
$this->couponRepository->save($coupon);
|
||||
|
||||
return new Output(
|
||||
id: $coupon->id,
|
||||
code: $coupon->code,
|
||||
name: $coupon->name,
|
||||
amount: $coupon->amount,
|
||||
picture: $coupon->picture,
|
||||
diffusion: $coupon->diffusion,
|
||||
validityInformations: $coupon->validityInformations,
|
||||
legalInformations: $coupon->legalInformations,
|
||||
processingCode: $coupon->processingCode,
|
||||
brandCode: $coupon->brandCode,
|
||||
createdBy: $coupon->createdBy,
|
||||
products: $coupon->products,
|
||||
movements: $coupon->movements,
|
||||
availabilityCalendar: $coupon->availabilityCalendar,
|
||||
);
|
||||
}
|
||||
}
|
||||
40
src/Application/UseCases/Coupon/Create/Input.php
Normal file
40
src/Application/UseCases/Coupon/Create/Input.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Create;
|
||||
|
||||
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\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Shared\DDD\UseCases\Input as UseCaseInput;
|
||||
|
||||
final readonly class Input implements UseCaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public CouponCode $code,
|
||||
public CouponName $name,
|
||||
public MoneyAmount $amount,
|
||||
public CouponPicture $picture,
|
||||
public CouponDiffusion $diffusion,
|
||||
public CouponValidityInformationArray $validityInformations,
|
||||
public CouponLegalInformationArray $legalInformations,
|
||||
public ProcessingCodeArray $processingCode,
|
||||
public BrandCode $brandCode,
|
||||
public ActorId $createdBy,
|
||||
public CouponProductArray $products,
|
||||
public CouponMovementArray $movements,
|
||||
public ?CouponAvailabilityCalendar $availabilityCalendar = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
42
src/Application/UseCases/Coupon/Create/Output.php
Normal file
42
src/Application/UseCases/Coupon/Create/Output.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Create;
|
||||
|
||||
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\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
|
||||
final readonly class Output implements UseCaseOutput
|
||||
{
|
||||
public function __construct(
|
||||
public CouponId $id,
|
||||
public CouponCode $code,
|
||||
public CouponName $name,
|
||||
public MoneyAmount $amount,
|
||||
public CouponPicture $picture,
|
||||
public CouponDiffusion $diffusion,
|
||||
public CouponValidityInformationArray $validityInformations,
|
||||
public CouponLegalInformationArray $legalInformations,
|
||||
public ProcessingCodeArray $processingCode,
|
||||
public BrandCode $brandCode,
|
||||
public ActorId $createdBy,
|
||||
public CouponProductArray $products,
|
||||
public CouponMovementArray $movements,
|
||||
public ?CouponAvailabilityCalendar $availabilityCalendar = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
64
src/Application/UseCases/Coupon/Find/FindCoupon.php
Normal file
64
src/Application/UseCases/Coupon/Find/FindCoupon.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Find;
|
||||
|
||||
use Application\UseCases\Coupon\Find\Output\Output;
|
||||
use Application\UseCases\Coupon\Find\Output\CouponFound;
|
||||
use Application\UseCases\Coupon\Find\Output\CouponNotFound;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Domain\Ports\Repositories\CampaignRepository;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\DDD\UseCases\UseCase;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements UseCase<\Application\UseCases\Coupon\Find\Input, \Application\UseCases\Coupon\Find\Output\Output> */
|
||||
final readonly class FindCoupon implements UseCase
|
||||
{
|
||||
public function __construct(
|
||||
private CouponRepository $couponRepository,
|
||||
private BrandRepository $brandRepository,
|
||||
private CampaignRepository $campaignRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$coupon = $this->couponRepository->findByCode($input->code);
|
||||
|
||||
if (! $coupon instanceof Coupon) {
|
||||
return new CouponNotFound();
|
||||
}
|
||||
if (! $this->brandRepository->existsByCode($coupon->brandCode)) {
|
||||
throw new \DomainException(sprintf('Brand code "%s" was not found.', $coupon->brandCode->value));
|
||||
}
|
||||
|
||||
$campaigns = $this->campaignRepository->findByCouponCode($coupon->code);
|
||||
|
||||
return new CouponFound(
|
||||
code: $coupon->code,
|
||||
name: $coupon->name,
|
||||
amount: $coupon->amount,
|
||||
picture: $coupon->picture,
|
||||
diffusion: $coupon->diffusion,
|
||||
availabilityCalendar: $coupon->availabilityCalendar,
|
||||
validityInformations: $coupon->validityInformations,
|
||||
legalInformations: $coupon->legalInformations,
|
||||
processingCode: $coupon->processingCode,
|
||||
brandCode: $coupon->brandCode,
|
||||
createdBy: $coupon->createdBy,
|
||||
products: $coupon->products,
|
||||
movements: $coupon->movements,
|
||||
quantity: $coupon->quantity(),
|
||||
campaigns: $campaigns,
|
||||
createdAt: $coupon->createdAt(),
|
||||
updatedAt: $coupon->updatedAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/Application/UseCases/Coupon/Find/Input.php
Normal file
16
src/Application/UseCases/Coupon/Find/Input.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Find;
|
||||
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Shared\DDD\UseCases\Input as UseCaseInput;
|
||||
|
||||
final readonly class Input implements UseCaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public CouponCode $code,
|
||||
) {
|
||||
}
|
||||
}
|
||||
48
src/Application/UseCases/Coupon/Find/Output/CouponFound.php
Normal file
48
src/Application/UseCases/Coupon/Find/Output/CouponFound.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Find\Output;
|
||||
|
||||
use Domain\Models\Entities\Campaign;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
|
||||
final readonly class CouponFound implements Output
|
||||
{
|
||||
/**
|
||||
* @param array<Campaign> $campaigns
|
||||
*/
|
||||
|
||||
public function __construct(
|
||||
public CouponCode $code,
|
||||
public CouponName $name,
|
||||
public MoneyAmount $amount,
|
||||
public CouponPicture $picture,
|
||||
public CouponDiffusion $diffusion,
|
||||
public ?CouponAvailabilityCalendar $availabilityCalendar,
|
||||
public CouponValidityInformationArray $validityInformations,
|
||||
public CouponLegalInformationArray $legalInformations,
|
||||
public ProcessingCodeArray $processingCode,
|
||||
public BrandCode $brandCode,
|
||||
public ActorId $createdBy,
|
||||
public CouponProductArray $products,
|
||||
public CouponMovementArray $movements,
|
||||
public int $quantity,
|
||||
public array $campaigns,
|
||||
public \DateTimeInterface $createdAt,
|
||||
public ?\DateTimeInterface $updatedAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Find\Output;
|
||||
|
||||
final readonly class CouponNotFound implements Output
|
||||
{
|
||||
}
|
||||
11
src/Application/UseCases/Coupon/Find/Output/Output.php
Normal file
11
src/Application/UseCases/Coupon/Find/Output/Output.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Find\Output;
|
||||
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
|
||||
interface Output extends UseCaseOutput
|
||||
{
|
||||
}
|
||||
25
src/Application/UseCases/Coupon/List/CouponListItem.php
Normal file
25
src/Application/UseCases/Coupon/List/CouponListItem.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\List;
|
||||
|
||||
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\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
|
||||
final readonly class CouponListItem
|
||||
{
|
||||
public function __construct(
|
||||
public CouponCode $code,
|
||||
public CouponName $name,
|
||||
public BrandCode $brandCode,
|
||||
public CouponPicture $picture,
|
||||
public CouponDiffusion $diffusion,
|
||||
public ?CouponAvailabilityCalendar $availabilityCalendar = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
20
src/Application/UseCases/Coupon/List/Input.php
Normal file
20
src/Application/UseCases/Coupon/List/Input.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\List;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponSortCriteria;
|
||||
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 ?CouponSortCriteria $sort = null,
|
||||
public ?BrandCode $brandCode = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
53
src/Application/UseCases/Coupon/List/ListCoupons.php
Normal file
53
src/Application/UseCases/Coupon/List/ListCoupons.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\List;
|
||||
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
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\Coupon\List\Input, \Application\UseCases\Coupon\List\Output> */
|
||||
final readonly class ListCoupons implements UseCase
|
||||
{
|
||||
public function __construct(
|
||||
private CouponRepository $repository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$result = $this->repository->findAll(
|
||||
pagination: $input->pagination,
|
||||
sortCriteria: $input->sort,
|
||||
brandCode: $input->brandCode,
|
||||
);
|
||||
|
||||
$items = array_map(
|
||||
static fn (Coupon $coupon): CouponListItem => new CouponListItem(
|
||||
code: $coupon->code,
|
||||
name: $coupon->name,
|
||||
brandCode: $coupon->brandCode,
|
||||
picture: $coupon->picture,
|
||||
diffusion: $coupon->diffusion,
|
||||
availabilityCalendar: $coupon->availabilityCalendar,
|
||||
),
|
||||
$result->getItems(),
|
||||
);
|
||||
|
||||
return new Output(
|
||||
coupons: new Paginated(
|
||||
pagination: $result->getPagination(),
|
||||
items: $items,
|
||||
total: $result->getTotal(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/Application/UseCases/Coupon/List/Output.php
Normal file
17
src/Application/UseCases/Coupon/List/Output.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\List;
|
||||
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
|
||||
final readonly class Output implements UseCaseOutput
|
||||
{
|
||||
/** @param Paginated<CouponListItem> $coupons */
|
||||
public function __construct(
|
||||
public Paginated $coupons,
|
||||
) {
|
||||
}
|
||||
}
|
||||
29
src/Application/UseCases/Coupon/Update/Input.php
Normal file
29
src/Application/UseCases/Coupon/Update/Input.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Update;
|
||||
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Shared\DDD\UseCases\Input as UseCaseInput;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class Input implements UseCaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public CouponCode $code,
|
||||
public ?CouponPicture $picture = null,
|
||||
public ?CouponValidityInformationArray $validityInformations = null,
|
||||
public ?CouponLegalInformationArray $legalInformations = null,
|
||||
) {
|
||||
Assert::true(
|
||||
$this->picture instanceof CouponPicture
|
||||
|| $this->validityInformations instanceof CouponValidityInformationArray
|
||||
|| $this->legalInformations instanceof CouponLegalInformationArray,
|
||||
'At least one field must be provided to update the coupon.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Update\Output;
|
||||
|
||||
final readonly class CouponNotFound implements Output
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Update\Output;
|
||||
|
||||
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\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
|
||||
final readonly class CouponUpdated implements Output
|
||||
{
|
||||
public function __construct(
|
||||
public CouponCode $code,
|
||||
public CouponName $name,
|
||||
public MoneyAmount $amount,
|
||||
public CouponPicture $picture,
|
||||
public CouponDiffusion $diffusion,
|
||||
public ?CouponAvailabilityCalendar $availabilityCalendar,
|
||||
public CouponValidityInformationArray $validityInformations,
|
||||
public CouponLegalInformationArray $legalInformations,
|
||||
public ProcessingCodeArray $processingCode,
|
||||
public BrandCode $brandCode,
|
||||
public ActorId $createdBy,
|
||||
public CouponProductArray $products,
|
||||
public CouponMovementArray $movements,
|
||||
) {
|
||||
}
|
||||
}
|
||||
11
src/Application/UseCases/Coupon/Update/Output/Output.php
Normal file
11
src/Application/UseCases/Coupon/Update/Output/Output.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Update\Output;
|
||||
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
|
||||
interface Output extends UseCaseOutput
|
||||
{
|
||||
}
|
||||
71
src/Application/UseCases/Coupon/Update/UpdateCoupon.php
Normal file
71
src/Application/UseCases/Coupon/Update/UpdateCoupon.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\Update;
|
||||
|
||||
use Application\UseCases\Coupon\Update\Output\CouponNotFound;
|
||||
use Application\UseCases\Coupon\Update\Output\CouponUpdated;
|
||||
use Application\UseCases\Coupon\Update\Output\Output;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\DDD\UseCases\Transactional;
|
||||
use Shared\DDD\UseCases\UseCase;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements UseCase<\Application\UseCases\Coupon\Update\Input, \Application\UseCases\Coupon\Update\Output\Output> */
|
||||
final readonly class UpdateCoupon implements UseCase, Transactional
|
||||
{
|
||||
public function __construct(
|
||||
private CouponRepository $repository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$coupon = $this->repository->findByCode($input->code);
|
||||
|
||||
if (! $coupon instanceof Coupon) {
|
||||
return new CouponNotFound();
|
||||
}
|
||||
|
||||
if ($input->picture instanceof CouponPicture) {
|
||||
$coupon = $coupon->changePicture($input->picture);
|
||||
}
|
||||
|
||||
if (
|
||||
$input->validityInformations instanceof CouponValidityInformationArray
|
||||
|| $input->legalInformations instanceof CouponLegalInformationArray
|
||||
) {
|
||||
$coupon = $coupon->changeInformations(
|
||||
$input->validityInformations ?? $coupon->validityInformations,
|
||||
$input->legalInformations ?? $coupon->legalInformations,
|
||||
);
|
||||
}
|
||||
|
||||
$this->repository->save($coupon);
|
||||
|
||||
return new CouponUpdated(
|
||||
code: $coupon->code,
|
||||
name: $coupon->name,
|
||||
amount: $coupon->amount,
|
||||
picture: $coupon->picture,
|
||||
diffusion: $coupon->diffusion,
|
||||
availabilityCalendar: $coupon->availabilityCalendar,
|
||||
validityInformations: $coupon->validityInformations,
|
||||
legalInformations: $coupon->legalInformations,
|
||||
processingCode: $coupon->processingCode,
|
||||
brandCode: $coupon->brandCode,
|
||||
createdBy: $coupon->createdBy,
|
||||
products: $coupon->products,
|
||||
movements: $coupon->movements,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\PrintableCoupon\Create;
|
||||
|
||||
use Domain\Models\Entities\Campaign;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\Entities\PrintableCoupon;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
|
||||
use Domain\Models\ValueObjects\CrmHashcode;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Ports\Repositories\CampaignRepository;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Domain\Ports\Repositories\PrintableCouponRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\DDD\UseCases\UseCase;
|
||||
use Shared\DDD\UseCases\Transactional;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements UseCase<\Application\UseCases\PrintableCoupon\Create\Input, \Application\UseCases\PrintableCoupon\Create\Output> */
|
||||
final readonly class CreatePrintableCoupon implements UseCase, Transactional
|
||||
{
|
||||
public function __construct(
|
||||
private PrintableCouponRepository $printableCouponRepository,
|
||||
private CouponRepository $couponRepository,
|
||||
private CampaignRepository $campaignRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$campaign = $this->campaignRepository->findByCode($input->campaignCode);
|
||||
if (! $campaign instanceof Campaign) {
|
||||
throw new \DomainException(sprintf('Campaign from code "%s" not found.', $input->campaignCode->value));
|
||||
}
|
||||
if (! $campaign->publication) {
|
||||
throw new \DomainException('Campaign must be published.');
|
||||
}
|
||||
|
||||
$coupon = $this->couponRepository->findByCode($input->couponCode);
|
||||
if (! $coupon instanceof Coupon) {
|
||||
throw new \DomainException(sprintf('Coupon code "%s" not found.', $input->couponCode->value));
|
||||
}
|
||||
|
||||
$campaignCoupon = $this->campaignRepository->findCampaignCoupon($campaign->code, $coupon->code);
|
||||
if (! $campaignCoupon instanceof CampaignCoupon) {
|
||||
throw new \DomainException('Coupon must be associated to the associated campaign coupon.');
|
||||
}
|
||||
|
||||
$period = $coupon->getActivePeriod();
|
||||
|
||||
$this->checkPrintRulesFromCampaignCoupon($campaign, $campaignCoupon, $coupon, $input->crmCustomerHashcode);
|
||||
|
||||
$printableCoupon = PrintableCoupon::create(
|
||||
campaignCode: $campaign->code,
|
||||
couponCode: $coupon->code,
|
||||
couponName: $coupon->name,
|
||||
crmCustomerHashcode: $input->crmCustomerHashcode,
|
||||
distributableUntil: $period->end,
|
||||
);
|
||||
|
||||
$this->printableCouponRepository->save($printableCoupon);
|
||||
|
||||
return new Output(
|
||||
campaignCode: $printableCoupon->campaignCode,
|
||||
couponCode: $printableCoupon->couponCode,
|
||||
couponName: $printableCoupon->couponName,
|
||||
crmCustomerHashcode: $printableCoupon->crmCustomerHashcode,
|
||||
distributableUntil: $printableCoupon->distributableUntil,
|
||||
);
|
||||
}
|
||||
|
||||
public function checkPrintRulesFromCampaignCoupon(
|
||||
Campaign $campaign,
|
||||
CampaignCoupon $campaignCoupon,
|
||||
Coupon $coupon,
|
||||
CrmHashcode $crmCustomerHashcode,
|
||||
): void {
|
||||
if ($campaignCoupon->printRules instanceof PrintRules) {
|
||||
$countPrintedCoupon = $this->printableCouponRepository->countPrintedCouponForCustomer(
|
||||
$campaign->code,
|
||||
$coupon->code,
|
||||
$crmCustomerHashcode,
|
||||
);
|
||||
$campaignCoupon->printRules->hasPrintLimitNotReached($countPrintedCoupon);
|
||||
|
||||
$lastPrintedCoupon = $this->printableCouponRepository->findLastPrintedCouponForCustomer(
|
||||
$campaign->code,
|
||||
$coupon->code,
|
||||
$crmCustomerHashcode,
|
||||
);
|
||||
$diff = $lastPrintedCoupon->printedAt()->diff(new \DateTime());
|
||||
$campaignCoupon->printRules->hasReprintPeriodExpired($diff->days);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/Application/UseCases/PrintableCoupon/Create/Input.php
Normal file
20
src/Application/UseCases/PrintableCoupon/Create/Input.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\PrintableCoupon\Create;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\CrmHashcode;
|
||||
use Shared\DDD\UseCases\Input as UseCaseInput;
|
||||
|
||||
final readonly class Input implements UseCaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public CampaignCode $campaignCode,
|
||||
public CouponCode $couponCode,
|
||||
public CrmHashcode $crmCustomerHashcode,
|
||||
) {
|
||||
}
|
||||
}
|
||||
23
src/Application/UseCases/PrintableCoupon/Create/Output.php
Normal file
23
src/Application/UseCases/PrintableCoupon/Create/Output.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\PrintableCoupon\Create;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\CrmHashcode;
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
|
||||
final readonly class Output implements UseCaseOutput
|
||||
{
|
||||
public function __construct(
|
||||
public CampaignCode $campaignCode,
|
||||
public CouponCode $couponCode,
|
||||
public CouponName $couponName,
|
||||
public CrmHashcode $crmCustomerHashcode,
|
||||
public \DateTimeInterface $distributableUntil,
|
||||
) {
|
||||
}
|
||||
}
|
||||
22
src/Application/UseCases/PrintableCoupon/List/Input.php
Normal file
22
src/Application/UseCases/PrintableCoupon/List/Input.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\PrintableCoupon\List;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\CrmHashcode;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponSortCriteria;
|
||||
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 CampaignCode $campaignCode,
|
||||
public CrmHashcode $crmCustomerHashcode,
|
||||
public ?PrintableCouponSortCriteria $sortCriteria = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\PrintableCoupon\List;
|
||||
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\Entities\PrintableCoupon;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Domain\Ports\Repositories\PrintableCouponRepository;
|
||||
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\PrintableCoupon\List\Input, \Application\UseCases\PrintableCoupon\List\Output> */
|
||||
final readonly class ListPrintableCoupons implements UseCase
|
||||
{
|
||||
public function __construct(
|
||||
private PrintableCouponRepository $repository,
|
||||
private CouponRepository $couponRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$result = $this->repository->findAllAvailable(
|
||||
pagination: $input->pagination,
|
||||
campaignCode: $input->campaignCode,
|
||||
crmCustomerHashcode: $input->crmCustomerHashcode,
|
||||
sortCriteria: $input->sortCriteria,
|
||||
);
|
||||
|
||||
$printableCoupons = $result->getItems();
|
||||
|
||||
/** @var array<string, CouponPicture> $picturesByCouponCode */
|
||||
$picturesByCouponCode = [];
|
||||
|
||||
foreach ($printableCoupons as $printableCoupon) {
|
||||
$couponCode = $printableCoupon->couponCode;
|
||||
|
||||
if (isset($picturesByCouponCode[$couponCode->value])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$coupon = $this->couponRepository->findByCode($couponCode);
|
||||
if (!$coupon instanceof Coupon) {
|
||||
throw new \LogicException(sprintf('Coupon "%s" referenced by a printable coupon does not exist.', $couponCode->value));
|
||||
}
|
||||
|
||||
$picturesByCouponCode[$couponCode->value] = $coupon->picture;
|
||||
}
|
||||
|
||||
$items = array_map(
|
||||
static fn (PrintableCoupon $printableCoupon): PrintableCouponListItem => new PrintableCouponListItem(
|
||||
code: $printableCoupon->code,
|
||||
couponCode: $printableCoupon->couponCode,
|
||||
couponName: $printableCoupon->couponName,
|
||||
couponPicture: $picturesByCouponCode[$printableCoupon->couponCode->value],
|
||||
distributableUntil: $printableCoupon->distributableUntil,
|
||||
),
|
||||
$printableCoupons,
|
||||
);
|
||||
|
||||
return new Output(
|
||||
printableCoupons: new Paginated(
|
||||
pagination: $result->getPagination(),
|
||||
items: $items,
|
||||
total: $result->getTotal(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/Application/UseCases/PrintableCoupon/List/Output.php
Normal file
17
src/Application/UseCases/PrintableCoupon/List/Output.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\PrintableCoupon\List;
|
||||
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
|
||||
final readonly class Output implements UseCaseOutput
|
||||
{
|
||||
/** @param Paginated<PrintableCouponListItem> $printableCoupons */
|
||||
public function __construct(
|
||||
public Paginated $printableCoupons,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\PrintableCoupon\List;
|
||||
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponCode;
|
||||
|
||||
final readonly class PrintableCouponListItem
|
||||
{
|
||||
public function __construct(
|
||||
public PrintableCouponCode $code,
|
||||
public CouponCode $couponCode,
|
||||
public CouponName $couponName,
|
||||
public CouponPicture $couponPicture,
|
||||
public \DateTimeInterface $distributableUntil,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Enums;
|
||||
|
||||
enum BarcodeFormat: string
|
||||
{
|
||||
case EAN13 = 'ean';
|
||||
case CODE128 = 'code128';
|
||||
case GS1_128 = 'gs1';
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Exceptions;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
|
||||
/**
|
||||
* There cannot be two brands with the same code.
|
||||
*/
|
||||
final class BrandCodeAlreadyExistsException extends \DomainException
|
||||
{
|
||||
public function __construct(BrandCode $code)
|
||||
{
|
||||
parent::__construct(sprintf('Brand code "%s" already exists.', $code->value));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?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));
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ use Domain\Models\ValueObjects\Actor\ActorDisplayName;
|
|||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Shared\DDD\Models\AggregateRoot;
|
||||
use Shared\DDD\Models\HasTimestampIdentifier;
|
||||
use Shared\Helpers\DatesHelper;
|
||||
|
||||
/**
|
||||
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Actor\ActorId>
|
||||
|
|
@ -39,12 +40,11 @@ final readonly class Actor implements AggregateRoot, HasTimestampIdentifier
|
|||
public static function hydrate(
|
||||
ActorId $id,
|
||||
ActorDisplayName $displayName,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
displayName: $displayName,
|
||||
updatedAt: $updatedAt,
|
||||
updatedAt: DatesHelper::now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ final readonly class Brand implements AggregateRoot, HasTimestampIdentifier
|
|||
PicturePath $logo,
|
||||
Rcs $rcs,
|
||||
Email $contact,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): Brand {
|
||||
return new self(
|
||||
id: BrandId::generate(),
|
||||
|
|
@ -60,7 +59,6 @@ final readonly class Brand implements AggregateRoot, HasTimestampIdentifier
|
|||
logo: $logo,
|
||||
rcs: $rcs,
|
||||
contact: $contact,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +69,6 @@ final readonly class Brand implements AggregateRoot, HasTimestampIdentifier
|
|||
PicturePath $logo,
|
||||
Rcs $rcs,
|
||||
Email $contact,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
|
|
@ -80,7 +77,7 @@ final readonly class Brand implements AggregateRoot, HasTimestampIdentifier
|
|||
logo: $logo,
|
||||
rcs: $rcs,
|
||||
contact: $contact,
|
||||
updatedAt: $updatedAt,
|
||||
updatedAt: DatesHelper::now(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,36 +7,29 @@ namespace Domain\Models\Entities;
|
|||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCouponArray;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignId;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Shared\DDD\Models\AggregateRoot;
|
||||
use Shared\DDD\Models\HasTimestampIdentifier;
|
||||
use Shared\Helpers\DatesHelper;
|
||||
|
||||
/**
|
||||
* @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 CampaignCouponArray $coupons,
|
||||
public BrandCode $brandCode,
|
||||
public ActorId $actorId,
|
||||
public ?\DateTimeInterface $updatedAt = null,
|
||||
) {
|
||||
foreach ($this->coupons as $coupon) {
|
||||
if (!$coupon instanceof CampaignCoupon) {
|
||||
throw new \DomainException('Campaign coupons must be CampaignCoupon instances.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function id(): CampaignId
|
||||
|
|
@ -54,18 +47,14 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier
|
|||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<CampaignCoupon> $coupons
|
||||
*/
|
||||
public static function create(
|
||||
CampaignCode $code,
|
||||
CampaignName $name,
|
||||
bool $publication,
|
||||
bool $printAllAtOnce,
|
||||
array $coupons,
|
||||
CampaignCouponArray $coupons,
|
||||
BrandCode $brandCode,
|
||||
ActorId $actorId,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): Campaign {
|
||||
return new self(
|
||||
id: CampaignId::generate(),
|
||||
|
|
@ -76,23 +65,18 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier
|
|||
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,
|
||||
CampaignCouponArray $coupons,
|
||||
BrandCode $brandCode,
|
||||
ActorId $actorId,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
|
|
@ -103,7 +87,7 @@ final readonly class Campaign implements AggregateRoot, HasTimestampIdentifier
|
|||
coupons: $coupons,
|
||||
brandCode: $brandCode,
|
||||
actorId: $actorId,
|
||||
updatedAt: $updatedAt,
|
||||
updatedAt: DatesHelper::now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent
|
|||
Applicant $applicant,
|
||||
CampaignRequestType $campaignType,
|
||||
RequestedCoupon $requestedCoupon,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: CampaignRequestId::generate(),
|
||||
|
|
@ -72,8 +71,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent
|
|||
campaignType: $campaignType,
|
||||
requestedCoupon: $requestedCoupon,
|
||||
status: CampaignRequestStatus::CREATED,
|
||||
rejectionReason: null,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +83,6 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent
|
|||
RequestedCoupon $requestedCoupon,
|
||||
CampaignRequestStatus $status,
|
||||
?RejectionReason $rejectionReason = null,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
|
|
@ -97,7 +93,7 @@ final readonly class CampaignRequest implements AggregateRoot, HasTimestampIdent
|
|||
requestedCoupon: $requestedCoupon,
|
||||
status: $status,
|
||||
rejectionReason: $rejectionReason,
|
||||
updatedAt: $updatedAt,
|
||||
updatedAt: DatesHelper::now(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,30 +8,27 @@ use Domain\Models\ValueObjects\Actor\ActorId;
|
|||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCountryCoverage;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponId;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\CouponPicture;
|
||||
use Domain\Models\ValueObjects\GS1;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Shared\DDD\Models\AggregateRoot;
|
||||
use Shared\DDD\Models\HasTimestampIdentifier;
|
||||
use Shared\Helpers\DatesHelper;
|
||||
|
||||
/**
|
||||
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Coupon\CouponId>
|
||||
*/
|
||||
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,
|
||||
|
|
@ -39,28 +36,22 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier
|
|||
public MoneyAmount $amount,
|
||||
public CouponPicture $picture,
|
||||
public CouponDiffusion $diffusion,
|
||||
public CouponValidity $validity,
|
||||
public array $couponValidityInformation,
|
||||
public array $couponLegalInformation,
|
||||
public ProcessingCode $processingCode,
|
||||
public CouponValidityInformationArray $validityInformations,
|
||||
public CouponLegalInformationArray $legalInformations,
|
||||
public ProcessingCodeArray $processingCode,
|
||||
public BrandCode $brandCode,
|
||||
public ActorId $createdBy,
|
||||
public array $products = [],
|
||||
public CouponProductArray $products,
|
||||
public CouponMovementArray $movements,
|
||||
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.');
|
||||
}
|
||||
}
|
||||
CouponCountryCoverage::assert(
|
||||
$this->processingCode,
|
||||
$this->validityInformations,
|
||||
$this->legalInformations,
|
||||
);
|
||||
$this->validateCalendarPeriodsWithinDiffusionPeriod();
|
||||
}
|
||||
|
||||
public function id(): CouponId
|
||||
|
|
@ -78,27 +69,25 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier
|
|||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<CouponValidityInformation> $couponValidityInformation
|
||||
* @param array<CouponLegalInformation> $couponLegalInformation
|
||||
* @param array<IncoCode> $products
|
||||
*/
|
||||
public function quantity(): int
|
||||
{
|
||||
return $this->movements->quantity();
|
||||
}
|
||||
|
||||
public static function create(
|
||||
CouponCode $code,
|
||||
CouponName $name,
|
||||
MoneyAmount $amount,
|
||||
CouponPicture $picture,
|
||||
CouponDiffusion $diffusion,
|
||||
CouponValidity $validity,
|
||||
array $couponValidityInformation,
|
||||
array $couponLegalInformation,
|
||||
ProcessingCode $processingCode,
|
||||
CouponValidityInformationArray $validityInformations,
|
||||
CouponLegalInformationArray $legalInformations,
|
||||
ProcessingCodeArray $processingCode,
|
||||
BrandCode $brandCode,
|
||||
ActorId $createdBy,
|
||||
array $products = [],
|
||||
CouponProductArray $products,
|
||||
CouponMovementArray $movements,
|
||||
?CouponAvailabilityCalendar $availabilityCalendar = null,
|
||||
?GS1 $gs1 = null,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): Coupon {
|
||||
return new self(
|
||||
id: CouponId::generate(),
|
||||
|
|
@ -107,24 +96,17 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier
|
|||
amount: $amount,
|
||||
picture: $picture,
|
||||
diffusion: $diffusion,
|
||||
validity: $validity,
|
||||
couponValidityInformation: $couponValidityInformation,
|
||||
couponLegalInformation: $couponLegalInformation,
|
||||
validityInformations: $validityInformations,
|
||||
legalInformations: $legalInformations,
|
||||
processingCode: $processingCode,
|
||||
brandCode: $brandCode,
|
||||
createdBy: $createdBy,
|
||||
products: $products,
|
||||
movements: $movements,
|
||||
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,
|
||||
|
|
@ -132,16 +114,14 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier
|
|||
MoneyAmount $amount,
|
||||
CouponPicture $picture,
|
||||
CouponDiffusion $diffusion,
|
||||
CouponValidity $validity,
|
||||
array $couponValidityInformation,
|
||||
array $couponLegalInformation,
|
||||
ProcessingCode $processingCode,
|
||||
CouponValidityInformationArray $validityInformations,
|
||||
CouponLegalInformationArray $legalInformations,
|
||||
ProcessingCodeArray $processingCode,
|
||||
BrandCode $brandCode,
|
||||
ActorId $createdBy,
|
||||
array $products = [],
|
||||
CouponProductArray $products,
|
||||
CouponMovementArray $movements,
|
||||
?CouponAvailabilityCalendar $availabilityCalendar = null,
|
||||
?GS1 $gs1 = null,
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
|
|
@ -150,16 +130,89 @@ final readonly class Coupon implements AggregateRoot, HasTimestampIdentifier
|
|||
amount: $amount,
|
||||
picture: $picture,
|
||||
diffusion: $diffusion,
|
||||
validity: $validity,
|
||||
couponValidityInformation: $couponValidityInformation,
|
||||
couponLegalInformation: $couponLegalInformation,
|
||||
validityInformations: $validityInformations,
|
||||
legalInformations: $legalInformations,
|
||||
processingCode: $processingCode,
|
||||
brandCode: $brandCode,
|
||||
createdBy: $createdBy,
|
||||
products: $products,
|
||||
movements: $movements,
|
||||
availabilityCalendar: $availabilityCalendar,
|
||||
gs1: $gs1,
|
||||
updatedAt: $updatedAt,
|
||||
updatedAt: DatesHelper::now(),
|
||||
);
|
||||
}
|
||||
|
||||
public function changePicture(CouponPicture $picture): self
|
||||
{
|
||||
return new self(
|
||||
id: $this->id,
|
||||
code: $this->code,
|
||||
name: $this->name,
|
||||
amount: $this->amount,
|
||||
picture: $picture,
|
||||
diffusion: $this->diffusion,
|
||||
validityInformations: $this->validityInformations,
|
||||
legalInformations: $this->legalInformations,
|
||||
processingCode: $this->processingCode,
|
||||
brandCode: $this->brandCode,
|
||||
createdBy: $this->createdBy,
|
||||
products: $this->products,
|
||||
movements: $this->movements,
|
||||
availabilityCalendar: $this->availabilityCalendar,
|
||||
updatedAt: DatesHelper::now(),
|
||||
);
|
||||
}
|
||||
|
||||
public function changeInformations(CouponValidityInformationArray $validityInformations, CouponLegalInformationArray $legalInformations): self
|
||||
{
|
||||
return new self(
|
||||
id: $this->id,
|
||||
code: $this->code,
|
||||
name: $this->name,
|
||||
amount: $this->amount,
|
||||
picture: $this->picture,
|
||||
diffusion: $this->diffusion,
|
||||
validityInformations: $validityInformations,
|
||||
legalInformations: $legalInformations,
|
||||
processingCode: $this->processingCode,
|
||||
brandCode: $this->brandCode,
|
||||
createdBy: $this->createdBy,
|
||||
products: $this->products,
|
||||
movements: $this->movements,
|
||||
availabilityCalendar: $this->availabilityCalendar,
|
||||
updatedAt: DatesHelper::now(),
|
||||
);
|
||||
}
|
||||
|
||||
public function getActivePeriod(): Period
|
||||
{
|
||||
$currentDate = new \DateTime();
|
||||
$diffusionPeriod = $this->diffusion->period;
|
||||
$period = $this->availabilityCalendar?->isActiveAt($currentDate)
|
||||
? $this->availabilityCalendar->getCurrentPeriod($currentDate)
|
||||
?? $diffusionPeriod
|
||||
: $diffusionPeriod;
|
||||
|
||||
if (! $period->hasStarted()) {
|
||||
throw new \DomainException('The printing period has not yet begun.');
|
||||
}
|
||||
|
||||
if (! $period->hasEnded()) {
|
||||
throw new \DomainException('The printing period has expired.');
|
||||
}
|
||||
|
||||
return $period;
|
||||
}
|
||||
|
||||
public function validateCalendarPeriodsWithinDiffusionPeriod(): void
|
||||
{
|
||||
if ($this->availabilityCalendar instanceof CouponAvailabilityCalendar) {
|
||||
$diffusionPeriod = $this->diffusion->period;
|
||||
foreach ($this->availabilityCalendar->periods as $calendarPeriod) {
|
||||
if (! $diffusionPeriod->inRange($calendarPeriod->start) || ! $diffusionPeriod->inRange($calendarPeriod->end)) {
|
||||
throw new \DomainException('Calendar period must be in diffusion period.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
82
src/Domain/Models/Entities/PrintableCoupon.php
Normal file
82
src/Domain/Models/Entities/PrintableCoupon.php
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\Entities;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponCode;
|
||||
use Domain\Models\ValueObjects\CrmHashcode;
|
||||
use Shared\DDD\Models\AggregateRoot;
|
||||
use Shared\DDD\Models\HasTimestampIdentifier;
|
||||
use Shared\Helpers\DatesHelper;
|
||||
|
||||
/**
|
||||
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponCode>
|
||||
*/
|
||||
final readonly class PrintableCoupon implements AggregateRoot, HasTimestampIdentifier
|
||||
{
|
||||
private function __construct(
|
||||
public PrintableCouponCode $code,
|
||||
public CampaignCode $campaignCode,
|
||||
public CouponCode $couponCode,
|
||||
public CouponName $couponName,
|
||||
public CrmHashcode $crmCustomerHashcode,
|
||||
public \DateTimeInterface $distributableUntil,
|
||||
public ?\DateTimeInterface $updatedAt = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): PrintableCouponCode
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function createdAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->code->toDateTime();
|
||||
}
|
||||
|
||||
public function updatedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public static function create(
|
||||
CampaignCode $campaignCode,
|
||||
CouponCode $couponCode,
|
||||
CouponName $couponName,
|
||||
CrmHashcode $crmCustomerHashcode,
|
||||
\DateTimeInterface $distributableUntil,
|
||||
): self {
|
||||
return new self(
|
||||
code: PrintableCouponCode::generate(),
|
||||
campaignCode: $campaignCode,
|
||||
couponCode: $couponCode,
|
||||
couponName: $couponName,
|
||||
crmCustomerHashcode: $crmCustomerHashcode,
|
||||
distributableUntil: $distributableUntil,
|
||||
);
|
||||
}
|
||||
|
||||
public static function hydrate(
|
||||
PrintableCouponCode $code,
|
||||
CampaignCode $campaignCode,
|
||||
CouponCode $couponCode,
|
||||
CouponName $couponName,
|
||||
CrmHashcode $crmCustomerHashcode,
|
||||
\DateTimeInterface $distributableUntil,
|
||||
): self {
|
||||
return new self(
|
||||
code: $code,
|
||||
campaignCode: $campaignCode,
|
||||
couponCode: $couponCode,
|
||||
couponName: $couponName,
|
||||
crmCustomerHashcode: $crmCustomerHashcode,
|
||||
distributableUntil: $distributableUntil,
|
||||
updatedAt: DatesHelper::now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,13 @@ declare(strict_types=1);
|
|||
namespace Domain\Models\Entities;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Stock\StockAdjustment;
|
||||
use Domain\Models\ValueObjects\Stock\StockAmount;
|
||||
use Domain\Models\ValueObjects\Stock\StockId;
|
||||
use Shared\DDD\Models\AggregateRoot;
|
||||
use Shared\DDD\Models\HasTimestampIdentifier;
|
||||
use Shared\Helpers\DatesHelper;
|
||||
|
||||
/**
|
||||
* @implements \Shared\DDD\Models\AggregateRoot<\Domain\Models\ValueObjects\Stock\StockId>
|
||||
|
|
@ -21,7 +22,7 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier
|
|||
public StockId $id,
|
||||
public StockAmount $stockAmount,
|
||||
public CampaignCode $campaignCode,
|
||||
public ProcessingCode $processingCode,
|
||||
public CouponCode $couponCode,
|
||||
/** @var array<int, StockAdjustment> $stockAdjustment */
|
||||
public array $stockAdjustment,
|
||||
public ?\DateTimeInterface $updatedAt = null,
|
||||
|
|
@ -55,17 +56,16 @@ final readonly class Stock implements AggregateRoot, HasTimestampIdentifier
|
|||
StockId $id,
|
||||
StockAmount $stockAmount,
|
||||
CampaignCode $campaignCode,
|
||||
ProcessingCode $processingCode,
|
||||
CouponCode $couponCode,
|
||||
array $stockAdjustment = [],
|
||||
?\DateTimeInterface $updatedAt = null,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
stockAmount: $stockAmount,
|
||||
campaignCode: $campaignCode,
|
||||
processingCode: $processingCode,
|
||||
couponCode: $couponCode,
|
||||
stockAdjustment: $stockAdjustment,
|
||||
updatedAt: $updatedAt,
|
||||
updatedAt: DatesHelper::now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ namespace Domain\Models\ValueObjects\Campaign;
|
|||
final readonly class CampaignCode
|
||||
{
|
||||
public function __construct(
|
||||
public string $code,
|
||||
public string $value,
|
||||
) {
|
||||
if (mb_trim($this->code) === '') {
|
||||
if (mb_trim($this->value) === '') {
|
||||
throw new \DomainException('Campaign code cannot be empty.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,17 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Models\ValueObjects\Campaign;
|
||||
|
||||
use Domain\Models\ValueObjects\Coupon\CouponQuantity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Shared\Enums\Country;
|
||||
|
||||
final readonly class CampaignCoupon
|
||||
{
|
||||
public function __construct(
|
||||
public ProcessingCode $processingCode,
|
||||
public CouponQuantity $quantity,
|
||||
public Country $availableIn,
|
||||
public CouponCode $couponCode,
|
||||
public ?PrintRules $printRules = null,
|
||||
) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Campaign;
|
||||
|
||||
final readonly class CampaignCouponArray
|
||||
{
|
||||
public function __construct(
|
||||
/** @var CampaignCoupon[] */
|
||||
public array $value,
|
||||
) {
|
||||
if ($this->value === []) {
|
||||
throw new \DomainException('Coupons requires at least one CampaignCoupon.');
|
||||
}
|
||||
|
||||
foreach ($this->value as $coupon) {
|
||||
if (! $coupon instanceof CampaignCoupon) {
|
||||
throw new \DomainException('Coupons must be CampaignCoupon instances.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ final readonly class CampaignRequestFilters
|
|||
public ?BrandCode $brandCode = null,
|
||||
public ?CampaignRequestStatus $status = null,
|
||||
) {
|
||||
if (null === $this->brandCode && null === $this->status) {
|
||||
if (!$this->brandCode instanceof BrandCode && !$this->status instanceof CampaignRequestStatus) {
|
||||
throw new \InvalidArgumentException('CampaignRequestFilters require at least a brand code or a status.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ use DateTime;
|
|||
|
||||
final readonly class CouponAvailabilityCalendar implements Equatable
|
||||
{
|
||||
/**
|
||||
* @param array<Period> $periods
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var Period[] */
|
||||
public array $periods,
|
||||
) {
|
||||
if ($this->periods === []) {
|
||||
|
|
@ -19,7 +21,7 @@ final readonly class CouponAvailabilityCalendar implements Equatable
|
|||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
if (!$period instanceof Period) {
|
||||
if (! $period instanceof Period) {
|
||||
throw new \DomainException('CouponAvailabilityCalendar periods must be Period instances.');
|
||||
}
|
||||
}
|
||||
|
|
@ -27,23 +29,22 @@ final readonly class CouponAvailabilityCalendar implements Equatable
|
|||
|
||||
public function isActiveAt(DateTime $in): bool
|
||||
{
|
||||
foreach ($this->periods as $period) {
|
||||
if ($in >= $period->start && $in <= $period->end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return array_any($this->periods, fn ($period): bool => $period->inRange($in));
|
||||
}
|
||||
|
||||
public function beginsAt(): DateTime
|
||||
{
|
||||
return min(array_map(fn (Period $period) => $period->start, $this->periods));
|
||||
return min(array_map(fn (Period $period): DateTime => $period->start, $this->periods));
|
||||
}
|
||||
|
||||
public function endsAt(): DateTime
|
||||
{
|
||||
return max(array_map(fn (Period $period) => $period->end, $this->periods));
|
||||
return max(array_map(fn (Period $period): DateTime => $period->end, $this->periods));
|
||||
}
|
||||
|
||||
public function getCurrentPeriod(DateTime $in): ?Period
|
||||
{
|
||||
return array_first(array_filter($this->periods, fn ($period): bool => $period->inRange($in)));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Shared\Enums\Country;
|
||||
|
||||
final readonly class CouponCountryCoverage
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function assert(
|
||||
ProcessingCodeArray $processingCodes,
|
||||
CouponValidityInformationArray $validityInformations,
|
||||
CouponLegalInformationArray $legalInformations,
|
||||
): void {
|
||||
foreach ($processingCodes->value as $processingCode) {
|
||||
$country = $processingCode->availableIn;
|
||||
|
||||
if (! self::hasInformationFor($country, $validityInformations)) {
|
||||
throw new \DomainException(sprintf('A coupon requires validity information for %s.', $country->name));
|
||||
}
|
||||
|
||||
if (! self::hasInformationFor($country, $legalInformations)) {
|
||||
throw new \DomainException(sprintf('A coupon requires legal information for %s.', $country->name));
|
||||
}
|
||||
}
|
||||
|
||||
self::countDiffusionCountries($validityInformations, $processingCodes);
|
||||
self::countDiffusionCountries($legalInformations, $processingCodes);
|
||||
}
|
||||
|
||||
private static function hasInformationFor(
|
||||
Country $country,
|
||||
CouponValidityInformationArray | CouponLegalInformationArray $informations,
|
||||
): bool {
|
||||
return array_any(
|
||||
$informations->value,
|
||||
fn (CouponValidityInformation | CouponLegalInformation $information): bool => $information->forLocale === $country,
|
||||
);
|
||||
}
|
||||
|
||||
private static function countDiffusionCountries(
|
||||
CouponValidityInformationArray | CouponLegalInformationArray $informations,
|
||||
ProcessingCodeArray $processingCodes,
|
||||
): void {
|
||||
if (count($informations->value) !== count($processingCodes->value)) {
|
||||
throw new \DomainException('A coupon requires exactly one information per diffusion country.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ final readonly class CouponLegalInformation
|
|||
{
|
||||
public function __construct(
|
||||
public string $legalText,
|
||||
public Country $forLocale
|
||||
public Country $forLocale,
|
||||
) {
|
||||
if (mb_trim($this->legalText) === '') {
|
||||
throw new \DomainException('Legal text cannot be empty.');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Shared\Enums\Country;
|
||||
|
||||
final readonly class CouponLegalInformationArray
|
||||
{
|
||||
public function __construct(
|
||||
/** @var CouponLegalInformation[] */
|
||||
public array $value,
|
||||
) {
|
||||
if ($this->value === []) {
|
||||
throw new \DomainException('CouponLegalInformations requires at least one CouponLegalInformation.');
|
||||
}
|
||||
|
||||
$hasFrance = false;
|
||||
$countries = [];
|
||||
|
||||
foreach ($this->value as $couponLegalInformation) {
|
||||
if (! $couponLegalInformation instanceof CouponLegalInformation) {
|
||||
throw new \DomainException('Value of CouponLegalInformationArray must be CouponLegalInformation instances.');
|
||||
}
|
||||
|
||||
if (isset($countries[$couponLegalInformation->forLocale->name])) {
|
||||
throw new \DomainException(sprintf(
|
||||
'Only one CouponLegalInformation is allowed per country, duplicate for %s.',
|
||||
$couponLegalInformation->forLocale->name,
|
||||
));
|
||||
}
|
||||
|
||||
$countries[$couponLegalInformation->forLocale->name] = true;
|
||||
|
||||
if ($couponLegalInformation->forLocale === Country::FRANCE) {
|
||||
$hasFrance = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $hasFrance) {
|
||||
throw new \DomainException('CouponLegalInformations requires a CouponLegalInformation for France.');
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Domain/Models/ValueObjects/Coupon/CouponMovement.php
Normal file
17
src/Domain/Models/ValueObjects/Coupon/CouponMovement.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
final readonly class CouponMovement
|
||||
{
|
||||
public \DateTimeInterface $createdAt;
|
||||
|
||||
public function __construct(
|
||||
public int $quantity,
|
||||
?\DateTimeInterface $createdAt = null,
|
||||
) {
|
||||
$this->createdAt = $createdAt ?? new \DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
final readonly class CouponMovementArray
|
||||
{
|
||||
public function __construct(
|
||||
/** @var CouponMovement[] */
|
||||
public array $value,
|
||||
) {
|
||||
if ($this->value === []) {
|
||||
throw new \DomainException('CouponMovementArray requires at least one CouponMovement.');
|
||||
}
|
||||
|
||||
foreach ($this->value as $couponMovement) {
|
||||
if (! $couponMovement instanceof CouponMovement) {
|
||||
throw new \DomainException('Value of CouponMovementArray must be CouponMovement instances.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function quantity(): int
|
||||
{
|
||||
return array_sum(array_column($this->value, 'quantity'));
|
||||
}
|
||||
}
|
||||
22
src/Domain/Models/ValueObjects/Coupon/CouponName.php
Normal file
22
src/Domain/Models/ValueObjects/Coupon/CouponName.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
final readonly class CouponName
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
$trimmed = mb_trim($value);
|
||||
|
||||
if ($trimmed === '') {
|
||||
throw new \DomainException('Coupon name cannot be empty.');
|
||||
}
|
||||
|
||||
if (mb_strlen($trimmed) > 200) {
|
||||
throw new \DomainException('Coupon name exceeds the maximum allowed length.');
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/Domain/Models/ValueObjects/Coupon/CouponPicture.php
Normal file
57
src/Domain/Models/ValueObjects/Coupon/CouponPicture.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
|
||||
final readonly class CouponPicture
|
||||
{
|
||||
private const array ALLOWED_EXTENSIONS = ['jpg', 'jpeg'];
|
||||
|
||||
public const int DECORATION_WIDTH = 280;
|
||||
public const int DECORATION_HEIGHT = 260;
|
||||
|
||||
public function __construct(
|
||||
public PicturePath $decoration,
|
||||
public PicturePath $thumbnail,
|
||||
public PicturePath $mobileThumbnail,
|
||||
) {
|
||||
foreach ([$decoration, $thumbnail, $mobileThumbnail] as $picture) {
|
||||
$extension = strtolower(pathinfo($picture->value, PATHINFO_EXTENSION));
|
||||
|
||||
if (! in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
|
||||
throw new \DomainException(sprintf('Coupon picture "%s" must be a JPG file.', $picture));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function create(
|
||||
PicturePath $decoration,
|
||||
PicturePath $thumbnail,
|
||||
PicturePath $mobileThumbnail,
|
||||
): self {
|
||||
$picture = new self($decoration, $thumbnail, $mobileThumbnail);
|
||||
|
||||
$size = is_file($decoration->value) ? getimagesize($decoration->value) : false;
|
||||
|
||||
if ($size === false) {
|
||||
throw new \DomainException(sprintf('Unable to read the decoration picture "%s".', $decoration));
|
||||
}
|
||||
|
||||
[$width, $height] = $size;
|
||||
|
||||
if ($width !== self::DECORATION_WIDTH || $height !== self::DECORATION_HEIGHT) {
|
||||
throw new \DomainException(sprintf(
|
||||
'The decoration picture must be %d x %d px, %d x %d px given.',
|
||||
self::DECORATION_WIDTH,
|
||||
self::DECORATION_HEIGHT,
|
||||
$width,
|
||||
$height,
|
||||
));
|
||||
}
|
||||
|
||||
return $picture;
|
||||
}
|
||||
}
|
||||
21
src/Domain/Models/ValueObjects/Coupon/CouponProductArray.php
Normal file
21
src/Domain/Models/ValueObjects/Coupon/CouponProductArray.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
|
||||
final readonly class CouponProductArray
|
||||
{
|
||||
public function __construct(
|
||||
/** @var IncoCode[] */
|
||||
public array $value = [],
|
||||
) {
|
||||
foreach ($this->value as $product) {
|
||||
if (! $product instanceof IncoCode) {
|
||||
throw new \DomainException('Value of CouponProductArray must be IncoCode instances.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php
Normal file
14
src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
final readonly class CouponSortCriteria
|
||||
{
|
||||
public function __construct(
|
||||
public CouponSortField $field = CouponSortField::CreatedAt,
|
||||
public \SortDirection $direction = \SortDirection::Ascending,
|
||||
) {
|
||||
}
|
||||
}
|
||||
12
src/Domain/Models/ValueObjects/Coupon/CouponSortField.php
Normal file
12
src/Domain/Models/ValueObjects/Coupon/CouponSortField.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
enum CouponSortField: string
|
||||
{
|
||||
case BrandCode = 'brand_code';
|
||||
case Diffusion = 'diffusion';
|
||||
case CreatedAt = 'created_at';
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?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,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
use Shared\Enums\Country;
|
||||
|
||||
final readonly class CouponValidityInformationArray
|
||||
{
|
||||
public function __construct(
|
||||
/** @var CouponValidityInformation[] */
|
||||
public array $value,
|
||||
) {
|
||||
if ($this->value === []) {
|
||||
throw new \DomainException('CouponValidityInformations requires at least one CouponValidityInformation.');
|
||||
}
|
||||
|
||||
$hasFrance = false;
|
||||
$countries = [];
|
||||
|
||||
foreach ($this->value as $couponValidityInformation) {
|
||||
if (! $couponValidityInformation instanceof CouponValidityInformation) {
|
||||
throw new \DomainException('Value of CouponValidityInformationArray must be CouponValidityInformation instances.');
|
||||
}
|
||||
|
||||
if (isset($countries[$couponValidityInformation->forLocale->name])) {
|
||||
throw new \DomainException(sprintf(
|
||||
'Only one CouponValidityInformation is allowed per country, duplicate for %s.',
|
||||
$couponValidityInformation->forLocale->name,
|
||||
));
|
||||
}
|
||||
|
||||
$countries[$couponValidityInformation->forLocale->name] = true;
|
||||
|
||||
if ($couponValidityInformation->forLocale === Country::FRANCE) {
|
||||
$hasFrance = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $hasFrance) {
|
||||
throw new \DomainException('CouponValidityInformations requires a CouponValidityInformation for France.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?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
|
||||
) {
|
||||
}
|
||||
}
|
||||
28
src/Domain/Models/ValueObjects/CrmHashcode.php
Normal file
28
src/Domain/Models/ValueObjects/CrmHashcode.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Shared\DDD\Models\Equatable;
|
||||
use Shared\Helpers\UuidHelper;
|
||||
|
||||
final readonly class CrmHashcode implements Equatable
|
||||
{
|
||||
public function __construct(
|
||||
public UuidInterface $value,
|
||||
) {
|
||||
if (! UuidHelper::isV4($value)) {
|
||||
throw new \DomainException(
|
||||
'Hash code must be a valid UUID v4.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof self && $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
|
||||
final readonly class IncoCode implements Equatable
|
||||
final readonly class IncoCode
|
||||
{
|
||||
/**
|
||||
* @throws \DomainException
|
||||
|
|
@ -18,10 +16,4 @@ final readonly class IncoCode implements Equatable
|
|||
throw new \DomainException('Inco Code must be a positive integer.');
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof self && $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,31 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use DateTime;
|
||||
use Shared\Helpers\DatesHelper;
|
||||
|
||||
final readonly class Period
|
||||
{
|
||||
public function __construct(
|
||||
public DateTime $start,
|
||||
public DateTime $end,
|
||||
public \DateTime $start,
|
||||
public \DateTime $end,
|
||||
) {
|
||||
if ($this->end < $this->start) {
|
||||
throw new \DomainException('Period end date cannot be before its start date.');
|
||||
}
|
||||
}
|
||||
|
||||
public function hasStarted(): bool
|
||||
{
|
||||
return $this->start <= DatesHelper::now();
|
||||
}
|
||||
|
||||
public function hasEnded(): bool
|
||||
{
|
||||
return $this->end >= DatesHelper::now();
|
||||
}
|
||||
|
||||
public function inRange(\DateTime $date): bool
|
||||
{
|
||||
return $date >= $this->start && $date <= $this->end;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,33 @@ final readonly class PrintRules
|
|||
public ?int $cooldownDays = null,
|
||||
) {
|
||||
if (null === $this->maxPerCustomer && null === $this->cooldownDays) {
|
||||
throw new \InvalidArgumentException('PrintRules require at least a max per customer or a cooldown days.');
|
||||
throw new \DomainException('PrintRules require at least a max per customer or a cooldown days.');
|
||||
}
|
||||
|
||||
if (null !== $this->maxPerCustomer && $this->maxPerCustomer <= 0) {
|
||||
throw new \InvalidArgumentException('Max per customer must be greater than 0.');
|
||||
throw new \DomainException('Max per customer must be greater than 0.');
|
||||
}
|
||||
|
||||
if (null !== $this->cooldownDays && $this->cooldownDays <= 0) {
|
||||
throw new \InvalidArgumentException('Cooldown days must be greater than 0.');
|
||||
if (null !== $this->cooldownDays && $this->cooldownDays < 30) {
|
||||
throw new \DomainException('Cooldown days must be greater than 30.');
|
||||
}
|
||||
|
||||
if ($this->maxPerCustomer && $this->cooldownDays) {
|
||||
throw new \DomainException('No more than one print rule.');
|
||||
}
|
||||
}
|
||||
|
||||
public function hasPrintLimitNotReached(int $countPrintedCoupon): void
|
||||
{
|
||||
if ($this->maxPerCustomer && $this->maxPerCustomer < $countPrintedCoupon) {
|
||||
throw new \DomainException('Maximum number of prints reached.');
|
||||
}
|
||||
}
|
||||
|
||||
public function hasReprintPeriodExpired(int $diffDays): void
|
||||
{
|
||||
if ($this->cooldownDays && $diffDays < $this->cooldownDays) {
|
||||
throw new \DomainException('Reprint period has not expired.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\PrintableCoupon;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
|
||||
final readonly class MarketingDirectCode implements \Stringable, Equatable
|
||||
{
|
||||
private const string PATTERN = '/^\d{13}\z/';
|
||||
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
if (preg_match(self::PATTERN, $this->value) !== 1) {
|
||||
throw new \DomainException(
|
||||
'Marketing Direct Code must be exactly 13 numeric characters, e.g. 5425003944130.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromInt(int $sequence): self
|
||||
{
|
||||
if ($sequence < 0) {
|
||||
throw new \DomainException('Marketing Direct sequence must be positive.');
|
||||
}
|
||||
|
||||
return new self(str_pad((string) $sequence, 13, '0', STR_PAD_LEFT));
|
||||
}
|
||||
|
||||
public function toInt(): int
|
||||
{
|
||||
return (int) $this->value;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof self && $this->value === $other->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\PrintableCoupon;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\DDD\Models\Uuidv7EntityId;
|
||||
|
||||
final readonly class PrintableCouponCode extends Uuidv7EntityId
|
||||
{
|
||||
public static function generate(): self
|
||||
{
|
||||
return new self(Uuid::uuid7());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\PrintableCoupon;
|
||||
|
||||
final readonly class PrintableCouponSortCriteria
|
||||
{
|
||||
public function __construct(
|
||||
public PrintableCouponSortField $field = PrintableCouponSortField::Name,
|
||||
public \SortDirection $direction = \SortDirection::Ascending,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\PrintableCoupon;
|
||||
|
||||
enum PrintableCouponSortField: string
|
||||
{
|
||||
case CreatedAt = 'created_at';
|
||||
case Name = 'name';
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\PrintableCoupon;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\CrmHashcode;
|
||||
|
||||
final readonly class PrintedCoupon
|
||||
{
|
||||
public function __construct(
|
||||
public PrintedCouponId $id,
|
||||
public MarketingDirectCode $marketingCode,
|
||||
public PrintableCouponCode $printableCode,
|
||||
public CouponCode $couponCode,
|
||||
public CampaignCode $campaignCode,
|
||||
public CrmHashcode $customer,
|
||||
public \DateTimeInterface $printedAt,
|
||||
) {
|
||||
}
|
||||
|
||||
public function printedAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->id->toDateTime();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\PrintableCoupon;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\DDD\Models\Uuidv7EntityId;
|
||||
|
||||
final readonly class PrintedCouponId extends Uuidv7EntityId
|
||||
{
|
||||
public static function generate(): self
|
||||
{
|
||||
return new self(Uuid::uuid7());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\PrintableCoupon;
|
||||
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\CrmHashcode;
|
||||
|
||||
final readonly class UnprintedCoupon
|
||||
{
|
||||
public function __construct(
|
||||
public PrintableCouponCode $printableCode,
|
||||
public CouponCode $couponCode,
|
||||
public CampaignCode $campaignCode,
|
||||
public CrmHashcode $customer,
|
||||
public UnprintedReason $unprintedReason,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
namespace Domain\Models\ValueObjects\PrintableCoupon;
|
||||
|
||||
final readonly class CouponName
|
||||
final readonly class UnprintedReason
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
if (mb_trim($this->value) === '') {
|
||||
throw new \DomainException('Coupon name cannot be empty.');
|
||||
throw new \DomainException('Unprinted reason cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,14 +5,20 @@ declare(strict_types=1);
|
|||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
use Shared\Enums\Country;
|
||||
|
||||
final readonly class ProcessingCode implements \Stringable, Equatable
|
||||
{
|
||||
private const string PATTERN = '/^\d{13}\z/';
|
||||
|
||||
public function __construct(
|
||||
public string $value,
|
||||
public Country $availableIn,
|
||||
) {
|
||||
if (mb_trim($this->value) === '') {
|
||||
throw new \DomainException('Processing Code value cannot be empty.');
|
||||
if (preg_match(self::PATTERN, $this->value) !== 1) {
|
||||
throw new \DomainException(
|
||||
'Processing Code must be exactly 13 numeric characters, e.g. 5425003944130.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
45
src/Domain/Models/ValueObjects/ProcessingCodeArray.php
Normal file
45
src/Domain/Models/ValueObjects/ProcessingCodeArray.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use Shared\Enums\Country;
|
||||
|
||||
final readonly class ProcessingCodeArray
|
||||
{
|
||||
public function __construct(
|
||||
/** @var ProcessingCode[] */
|
||||
public array $value,
|
||||
) {
|
||||
if ($this->value === []) {
|
||||
throw new \DomainException('ProcessingCodes requires at least one ProcessingCode.');
|
||||
}
|
||||
|
||||
$hasFrance = false;
|
||||
$countries = [];
|
||||
|
||||
foreach ($this->value as $processingCode) {
|
||||
if (! $processingCode instanceof ProcessingCode) {
|
||||
throw new \DomainException('Value of ProcessingCodeArray must be ProcessingCode instances.');
|
||||
}
|
||||
|
||||
if (isset($countries[$processingCode->availableIn->name])) {
|
||||
throw new \DomainException(sprintf(
|
||||
'Only one ProcessingCode is allowed per country, duplicate for %s.',
|
||||
$processingCode->availableIn->name,
|
||||
));
|
||||
}
|
||||
|
||||
$countries[$processingCode->availableIn->name] = true;
|
||||
|
||||
if ($processingCode->availableIn === Country::FRANCE) {
|
||||
$hasFrance = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $hasFrance) {
|
||||
throw new \DomainException('ProcessingCodes requires a ProcessingCode for France.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,58 +4,35 @@ 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\Coupon\CouponCountryCoverage;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Shared\Enums\Country;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
|
||||
final readonly class RequestedCoupon
|
||||
{
|
||||
/**
|
||||
* @param array<CouponValidityInformation> $couponValidityInformation
|
||||
* @param array<IncoCode> $products
|
||||
*/
|
||||
public function __construct(
|
||||
public CouponName $name,
|
||||
public CouponName $couponName,
|
||||
public RequestedCouponQuantity $quantity,
|
||||
public Period $diffusion,
|
||||
public MoneyAmount $moneyAmount,
|
||||
public ProcessingCode $processingCode,
|
||||
public CouponValidity $couponValidity,
|
||||
public array $couponValidityInformation,
|
||||
public array $products = [],
|
||||
public CouponPicture $couponPicture,
|
||||
public ProcessingCodeArray $couponProcessingCodes,
|
||||
public CouponValidityInformationArray $couponValidityInformations,
|
||||
public CouponLegalInformationArray $couponLegalInformations,
|
||||
public CouponProductArray $couponProducts,
|
||||
public ?PrintRules $printRules = null,
|
||||
public ?GS1 $gs1 = null,
|
||||
) {
|
||||
foreach ($this->couponValidityInformation as $validityInformation) {
|
||||
if (!$validityInformation instanceof CouponValidityInformation) {
|
||||
throw new \DomainException('Coupon validity information must be CouponValidityInformation instances.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->hasValidityInformationFor(Country::FRANCE)) {
|
||||
throw new \DomainException('A coupon requires validity information for France.');
|
||||
}
|
||||
|
||||
if ($this->gs1 instanceof GS1 && !$this->hasValidityInformationFor(Country::BENELUX)) {
|
||||
throw new \DomainException('A GS1 coupon requires validity information for Benelux.');
|
||||
}
|
||||
}
|
||||
|
||||
private function hasValidityInformationFor(Country $country): bool
|
||||
{
|
||||
foreach ($this->couponValidityInformation as $validityInformation) {
|
||||
if ($validityInformation->forLocale === $country) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
CouponCountryCoverage::assert(
|
||||
$this->couponProcessingCodes,
|
||||
$this->couponValidityInformations,
|
||||
$this->couponLegalInformations,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Ports\Repositories;
|
||||
|
||||
use Domain\Models\Entities\Campaign;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Shared\DDD\Ports\Repository;
|
||||
|
||||
/**
|
||||
|
|
@ -11,4 +15,14 @@ use Shared\DDD\Ports\Repository;
|
|||
*/
|
||||
interface CampaignRepository extends Repository
|
||||
{
|
||||
/**
|
||||
* Returns the campaigns that currently contain the given coupon
|
||||
*
|
||||
* @return array<Campaign>
|
||||
*/
|
||||
public function findByCouponCode(CouponCode $couponCode): array;
|
||||
|
||||
public function findByCode(CampaignCode $code): ?Campaign;
|
||||
|
||||
public function findCampaignCoupon(CampaignCode $campaignCode, CouponCode $couponCode): ?CampaignCoupon;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,31 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Ports\Repositories;
|
||||
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponSortCriteria;
|
||||
use Shared\DDD\Ports\Repository;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
|
||||
/**
|
||||
* @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\Coupon\CouponId, \Domain\Models\Entities\Coupon>
|
||||
*/
|
||||
interface CouponRepository extends Repository
|
||||
{
|
||||
public function existsByCode(CouponCode $code): bool;
|
||||
|
||||
public function findByCode(CouponCode $code): ?Coupon;
|
||||
|
||||
/**
|
||||
* @return Paginated<Coupon>
|
||||
*/
|
||||
public function findAll(
|
||||
Pagination $pagination,
|
||||
?CouponSortCriteria $sortCriteria = null,
|
||||
?BrandCode $brandCode = null,
|
||||
): Paginated;
|
||||
|
||||
public function save(Coupon $brand): void;
|
||||
}
|
||||
|
|
|
|||
45
src/Domain/Ports/Repositories/PrintableCouponRepository.php
Normal file
45
src/Domain/Ports/Repositories/PrintableCouponRepository.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Ports\Repositories;
|
||||
|
||||
use Domain\Models\Entities\PrintableCoupon;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\CrmHashcode;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponSortCriteria;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintedCoupon;
|
||||
use Shared\DDD\Ports\Repository;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
|
||||
/**
|
||||
* @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponCode, \Domain\Models\Entities\PrintableCoupon>
|
||||
*/
|
||||
interface PrintableCouponRepository extends Repository
|
||||
{
|
||||
/**
|
||||
* @return Paginated<PrintableCoupon>
|
||||
*/
|
||||
public function findAllAvailable(
|
||||
Pagination $pagination,
|
||||
CampaignCode $campaignCode,
|
||||
CrmHashcode $crmCustomerHashcode,
|
||||
?PrintableCouponSortCriteria $sortCriteria = null,
|
||||
): Paginated;
|
||||
|
||||
public function countPrintedCouponForCustomer(
|
||||
CampaignCode $campaignCode,
|
||||
CouponCode $couponCode,
|
||||
CrmHashcode $crmCustomerHashcode,
|
||||
): int;
|
||||
|
||||
public function findLastPrintedCouponForCustomer(
|
||||
CampaignCode $campaignCode,
|
||||
CouponCode $couponCode,
|
||||
CrmHashcode $crmCustomerHashcode,
|
||||
): PrintedCoupon;
|
||||
|
||||
public function save(PrintableCoupon $printableCoupon): void;
|
||||
}
|
||||
|
|
@ -36,10 +36,10 @@ final class DatesHelper
|
|||
$now = self::now();
|
||||
|
||||
return match (true) {
|
||||
$from !== null && $to !== null => $now->between($from, $to),
|
||||
$from !== null => $now->greaterThanOrEqualTo($from),
|
||||
$to !== null => $now->lessThanOrEqualTo($to),
|
||||
default => false,
|
||||
$from instanceof CarbonInterface && $to instanceof CarbonInterface => $now->between($from, $to),
|
||||
$from instanceof CarbonInterface => $now->greaterThanOrEqualTo($from),
|
||||
$to instanceof CarbonInterface => $now->lessThanOrEqualTo($to),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ final class DatesHelper
|
|||
break;
|
||||
default:
|
||||
try {
|
||||
$output = (new \DateTimeImmutable($datetime, new \DateTimeZone('UTC')))->setTimezone(new \DateTimeZone('UTC'));
|
||||
$output = new \DateTimeImmutable($datetime, new \DateTimeZone('UTC'))->setTimezone(new \DateTimeZone('UTC'));
|
||||
} catch (\Exception) {
|
||||
throw InvalidDateFormatException::forDateString($datetime);
|
||||
}
|
||||
|
|
@ -96,21 +96,21 @@ final class DatesHelper
|
|||
return $output;
|
||||
}
|
||||
|
||||
protected static function parseFrench(string $datetime): \DateTimeInterface
|
||||
private static function parseFrench(string $datetime): \DateTimeInterface
|
||||
{
|
||||
$dt = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $datetime . ' 00:00:00', new \DateTimeZone('UTC'));
|
||||
|
||||
return $dt ?: self::parseFallback($datetime);
|
||||
}
|
||||
|
||||
protected static function parseFrenchInverted(string $datetime): \DateTimeInterface
|
||||
private static function parseFrenchInverted(string $datetime): \DateTimeInterface
|
||||
{
|
||||
$dt = \DateTimeImmutable::createFromFormat('d-m-Y H:i:s', $datetime . ' 00:00:00', new \DateTimeZone('UTC'));
|
||||
|
||||
return $dt ?: self::parseFallback($datetime);
|
||||
}
|
||||
|
||||
protected static function parseRfc3339(string $datetime): \DateTimeInterface
|
||||
private static function parseRfc3339(string $datetime): \DateTimeInterface
|
||||
{
|
||||
$dt = \DateTimeImmutable::createFromFormat(\DateTimeInterface::RFC3339, $datetime);
|
||||
if ($dt !== false) {
|
||||
|
|
@ -122,17 +122,17 @@ final class DatesHelper
|
|||
return $dt ? $dt->setTimezone(new \DateTimeZone('UTC')) : self::parseFallback($datetime);
|
||||
}
|
||||
|
||||
protected static function parseIso8601(string $datetime): \DateTimeInterface
|
||||
private static function parseIso8601(string $datetime): \DateTimeInterface
|
||||
{
|
||||
$dt = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $datetime);
|
||||
|
||||
return $dt ? $dt->setTimezone(new \DateTimeZone('UTC')) : self::parseFallback($datetime);
|
||||
}
|
||||
|
||||
protected static function parseFallback(string $datetime): \DateTimeInterface
|
||||
private static function parseFallback(string $datetime): \DateTimeInterface
|
||||
{
|
||||
try {
|
||||
return (new \DateTimeImmutable($datetime, new \DateTimeZone('UTC')))->setTimezone(new \DateTimeZone('UTC'));
|
||||
return new \DateTimeImmutable($datetime, new \DateTimeZone('UTC'))->setTimezone(new \DateTimeZone('UTC'));
|
||||
} catch (\Throwable) {
|
||||
throw InvalidDateFormatException::forDateString($datetime);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ final readonly class Paginated
|
|||
{
|
||||
/** @param T[] $items */
|
||||
public function __construct(
|
||||
private readonly Pagination $pagination,
|
||||
private Pagination $pagination,
|
||||
private array $items,
|
||||
private int $total
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use Shared\DDD\Models\Equatable;
|
|||
/**
|
||||
* Immutable value object representing a pagination range.
|
||||
*/
|
||||
final readonly class Pagination implements Equatable
|
||||
final readonly class Pagination implements Equatable, \Stringable
|
||||
{
|
||||
/** ERR_TOO_MANY_RECORDS fixed to 10k */
|
||||
public const int MAX_PAGE_SIZE = PageSize::MAX_PAGE_SIZE;
|
||||
|
|
|
|||
BIN
tests/Fixtures/Coupon/decoration.jpg
Normal file
BIN
tests/Fixtures/Coupon/decoration.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
tests/Fixtures/Coupon/invalid_decoration.jpg
Normal file
BIN
tests/Fixtures/Coupon/invalid_decoration.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -7,7 +7,6 @@ namespace Tests\Unit\Application\UseCases\Brand\Create;
|
|||
use Application\UseCases\Brand\Create\CreateBrand;
|
||||
use Application\UseCases\Brand\Create\Input;
|
||||
use Application\UseCases\Brand\Create\Output;
|
||||
use Domain\Exceptions\BrandCodeAlreadyExistsException;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
|
|
@ -77,7 +76,7 @@ describe('create brand use case validation rules', function (): void {
|
|||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(BrandCodeAlreadyExistsException::class);
|
||||
expect($action)->toThrow(\DomainException::class);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,16 +21,16 @@ use Shared\ValueObjects\Rcs;
|
|||
|
||||
covers(UpdateBrand::class, Input::class, BrandUpdated::class, BrandNotFound::class);
|
||||
|
||||
$existingBrand = static fn (BrandCode $code): Brand => Brand::hydrate(
|
||||
BrandId::generate(),
|
||||
$code,
|
||||
new BrandName('Name'),
|
||||
new PicturePath('files/picture-old.jpg'),
|
||||
new Rcs('RCS PARIS 111 222 333'),
|
||||
new Email('contact-old@example.com'),
|
||||
);
|
||||
describe('update brand use case', function (): void {
|
||||
$existingBrand = static fn (BrandCode $code): Brand => Brand::hydrate(
|
||||
BrandId::generate(),
|
||||
$code,
|
||||
new BrandName('Name'),
|
||||
new PicturePath('files/picture-old.jpg'),
|
||||
new Rcs('RCS PARIS 111 222 333'),
|
||||
new Email('contact-old@example.com'),
|
||||
);
|
||||
|
||||
describe('update brand use case', function () use ($existingBrand): void {
|
||||
it('updates every provided field', function () use ($existingBrand): void {
|
||||
// GIVEN
|
||||
$code = new BrandCode('AAA01');
|
||||
|
|
@ -47,8 +47,7 @@ describe('update brand use case', function () use ($existingBrand): void {
|
|||
&& $saved->rcs === $newRcs
|
||||
&& $saved->contact === $newContact
|
||||
&& $saved->code === $brand->code
|
||||
&& $saved->name === $brand->name
|
||||
&& $saved->updatedAt !== null,
|
||||
&& $saved->name === $brand->name,
|
||||
));
|
||||
|
||||
$useCase = new UpdateBrand($repository);
|
||||
|
|
@ -58,8 +57,10 @@ describe('update brand use case', function () use ($existingBrand): void {
|
|||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(BrandUpdated::class)
|
||||
->and($result->code)->toBe($brand->code)
|
||||
->and($result->updatedAt)->not->toBeNull();
|
||||
->and($result->logo)->toBe($newLogo)
|
||||
->and($result->rcs)->toBe($newRcs)
|
||||
->and($result->contact)->toBe($newContact)
|
||||
->and($result->code)->toBe($brand->code);
|
||||
});
|
||||
|
||||
it('updates only the contact and leaves logo/rcs untouched', function () use ($existingBrand): void {
|
||||
|
|
@ -76,8 +77,7 @@ describe('update brand use case', function () use ($existingBrand): void {
|
|||
&& $saved->logo === $brand->logo
|
||||
&& $saved->rcs === $brand->rcs
|
||||
&& $saved->code === $brand->code
|
||||
&& $saved->name === $brand->name
|
||||
&& $saved->updatedAt !== null,
|
||||
&& $saved->name === $brand->name,
|
||||
));
|
||||
|
||||
$useCase = new UpdateBrand($repository);
|
||||
|
|
@ -87,8 +87,8 @@ describe('update brand use case', function () use ($existingBrand): void {
|
|||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(BrandUpdated::class)
|
||||
->and($result->code)->toBe($brand->code)
|
||||
->and($result->updatedAt)->not->toBeNull();
|
||||
->and($result->contact)->toBe($newContact)
|
||||
->and($result->code)->toBe($brand->code);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,21 +8,26 @@ use Application\UseCases\CampaignRequest\Create\CreateCampaignRequest;
|
|||
use Application\UseCases\CampaignRequest\Create\Input;
|
||||
use Application\UseCases\CampaignRequest\Create\Output;
|
||||
use DateTime;
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
|
|
@ -34,33 +39,45 @@ use Shared\ValueObjects\Money;
|
|||
|
||||
covers(CreateCampaignRequest::class, Input::class, Output::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->input = new Input(
|
||||
describe('create campaign request use case', function (): void {
|
||||
$makeInput = fn (): Input => new Input(
|
||||
brandCode: new BrandCode('AAA01'),
|
||||
name: new CampaignName('Campaign name'),
|
||||
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
campaignType: CampaignRequestType::CLUB,
|
||||
requestedCoupon: new RequestedCoupon(
|
||||
name: new CouponName('Coupon A'),
|
||||
couponName: new CouponName('Coupon A'),
|
||||
quantity: new RequestedCouponQuantity(100),
|
||||
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
processingCode: new ProcessingCode('PC1'),
|
||||
couponValidity: new CouponValidity(30, new DateTime('2026-02-01')),
|
||||
couponValidityInformation: [
|
||||
couponPicture: new CouponPicture(
|
||||
new PicturePath('files/product.jpg'),
|
||||
new PicturePath('files/thumbnail.jpg'),
|
||||
new PicturePath('files/mobileThumbnail.jpg')
|
||||
),
|
||||
couponProcessingCodes: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
new ProcessingCode('4006381333931', Country::BENELUX),
|
||||
]),
|
||||
couponValidityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
],
|
||||
products: [new IncoCode(35784), new IncoCode(15522)],
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
]),
|
||||
couponLegalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
]),
|
||||
couponProducts: new CouponProductArray([new 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 {
|
||||
it('creates a campaign request when the brand exists', function () use ($makeInput): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($this->input->brandCode)->andReturn(true);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true);
|
||||
|
||||
$campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$campaignRequestRepository->shouldReceive('save')->once()->with(\Mockery::type(CampaignRequest::class));
|
||||
|
|
@ -68,20 +85,20 @@ describe('create campaign request use case', function (): void {
|
|||
$useCase = new CreateCampaignRequest($campaignRequestRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($this->input);
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class)
|
||||
->and($result->status)->toBe(CampaignRequestStatus::CREATED)
|
||||
->and($result->brandCode)->toBe($this->input->brandCode);
|
||||
->and($result->brandCode)->toBe($input->brandCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create campaign request use case validation rules', function (): void {
|
||||
it('throws when the brand does not exist', function (): void {
|
||||
it('throws when the brand does not exist', function () use ($makeInput): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($this->input->brandCode)->andReturn(false);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(false);
|
||||
|
||||
$campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$campaignRequestRepository->shouldNotReceive('save');
|
||||
|
|
@ -89,9 +106,9 @@ describe('create campaign request use case validation rules', function (): void
|
|||
$useCase = new CreateCampaignRequest($campaignRequestRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($this->input);
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(BrandNotFoundException::class);
|
||||
expect($action)->toThrow(\DomainException::class);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestFound;
|
|||
use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestNotFound;
|
||||
use Application\UseCases\CampaignRequest\Find\Output\Output;
|
||||
use DateTime;
|
||||
use Domain\Enums\BarcodeFormat;
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
|
|
@ -23,16 +21,20 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
|||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\GS1;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
|
|
@ -47,44 +49,52 @@ use Shared\ValueObjects\Rcs;
|
|||
covers(FindCampaignRequest::class, Input::class, CampaignRequestFound::class, CampaignRequestNotFound::class);
|
||||
|
||||
describe('find campaign request use case', function (): void {
|
||||
beforeEach(function (): void {
|
||||
$this->requestedCoupon = new RequestedCoupon(
|
||||
new CouponName('Coupon A'),
|
||||
new RequestedCouponQuantity(100),
|
||||
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
new ProcessingCode('PC1'),
|
||||
new CouponValidity(30, new DateTime('2026-02-01')),
|
||||
[
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
],
|
||||
[new IncoCode(35784), new IncoCode(15522)],
|
||||
new PrintRules(maxPerCustomer: 3),
|
||||
new GS1('01234567890128', BarcodeFormat::EAN13)
|
||||
);
|
||||
});
|
||||
$requestedCoupon = new RequestedCoupon(
|
||||
couponName: new CouponName('Coupon A'),
|
||||
quantity: new RequestedCouponQuantity(100),
|
||||
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
couponPicture: new CouponPicture(
|
||||
new PicturePath('files/product.jpg'),
|
||||
new PicturePath('files/thumbnail.jpg'),
|
||||
new PicturePath('files/mobileThumbnail.jpg')
|
||||
),
|
||||
couponProcessingCodes: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
new ProcessingCode('4006381333931', Country::BENELUX),
|
||||
]),
|
||||
couponValidityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
]),
|
||||
couponLegalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
]),
|
||||
couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]),
|
||||
printRules: new PrintRules(maxPerCustomer: 3),
|
||||
);
|
||||
|
||||
it('finds a campaign request by its id, along with its brand', function (): void {
|
||||
it('finds a campaign request by its id, along with its brand', function () use ($requestedCoupon): void {
|
||||
// GIVEN
|
||||
$id = CampaignRequestId::generate();
|
||||
$brandCode = new BrandCode('AAA01');
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
$id,
|
||||
$brandCode,
|
||||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->requestedCoupon,
|
||||
CampaignRequestStatus::CREATED,
|
||||
id: $id,
|
||||
brandCode: $brandCode,
|
||||
name: new CampaignName('Campaign A'),
|
||||
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
campaignType: CampaignRequestType::CLUB,
|
||||
requestedCoupon: $requestedCoupon,
|
||||
status: CampaignRequestStatus::CREATED,
|
||||
);
|
||||
$brand = Brand::hydrate(
|
||||
BrandId::generate(),
|
||||
$brandCode,
|
||||
new BrandName('Brand A'),
|
||||
new PicturePath('brands/logo.jpg'),
|
||||
new Rcs('RCS PARIS 111 222 333'),
|
||||
new Email('contact@example.com'),
|
||||
id: BrandId::generate(),
|
||||
code: $brandCode,
|
||||
name: new BrandName('Brand A'),
|
||||
logo: new PicturePath('brands/logo.jpg'),
|
||||
rcs: new Rcs('RCS PARIS 111 222 333'),
|
||||
contact: new Email('contact@example.com'),
|
||||
);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
|
|
@ -109,20 +119,20 @@ describe('find campaign request use case', function (): void {
|
|||
->and($result->rejectionReason)->toBeNull();
|
||||
});
|
||||
|
||||
it('finds a rejected campaign request with its rejection reason', function (): void {
|
||||
it('finds a rejected campaign request with its rejection reason', function () use ($requestedCoupon): void {
|
||||
// GIVEN
|
||||
$id = CampaignRequestId::generate();
|
||||
$code = new BrandCode('AAA01');
|
||||
$rejectionReason = new RejectionReason('Missing legal documents.');
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
$id,
|
||||
$code,
|
||||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->requestedCoupon,
|
||||
CampaignRequestStatus::REJECTED,
|
||||
$rejectionReason,
|
||||
id: $id,
|
||||
brandCode: $code,
|
||||
name: new CampaignName('Campaign A'),
|
||||
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
campaignType: CampaignRequestType::CLUB,
|
||||
requestedCoupon: $requestedCoupon,
|
||||
status: CampaignRequestStatus::REJECTED,
|
||||
rejectionReason: $rejectionReason,
|
||||
);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
|
|
@ -131,12 +141,12 @@ describe('find campaign request use case', function (): void {
|
|||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(
|
||||
Brand::hydrate(
|
||||
BrandId::generate(),
|
||||
$code,
|
||||
new BrandName('Brand A'),
|
||||
new PicturePath('brands/logo.jpg'),
|
||||
new Rcs('RCS PARIS 111 222 333'),
|
||||
new Email('contact@example.com'),
|
||||
id: BrandId::generate(),
|
||||
code: $code,
|
||||
name: new BrandName('Brand A'),
|
||||
logo: new PicturePath('brands/logo.jpg'),
|
||||
rcs: new Rcs('RCS PARIS 111 222 333'),
|
||||
contact: new Email('contact@example.com'),
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -170,18 +180,18 @@ describe('find campaign request use case', function (): void {
|
|||
expect($result)->toBeInstanceOf(CampaignRequestNotFound::class);
|
||||
});
|
||||
|
||||
it('throws when the campaign request references a brand that no longer exists', function (): void {
|
||||
it('throws when the campaign request references a brand that no longer exists', function () use ($requestedCoupon): void {
|
||||
// GIVEN
|
||||
$id = CampaignRequestId::generate();
|
||||
$brandCode = new BrandCode('AAA01');
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
$id,
|
||||
$brandCode,
|
||||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->requestedCoupon,
|
||||
CampaignRequestStatus::CREATED,
|
||||
id: $id,
|
||||
brandCode: $brandCode,
|
||||
name: new CampaignName('Campaign A'),
|
||||
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
campaignType: CampaignRequestType::CLUB,
|
||||
requestedCoupon: $requestedCoupon,
|
||||
status: CampaignRequestStatus::CREATED,
|
||||
);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
|
|
@ -196,7 +206,7 @@ describe('find campaign request use case', function (): void {
|
|||
$action = fn (): Output => $useCase->execute(new Input(id: $id));
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(BrandNotFoundException::class);
|
||||
expect($action)->toThrow(\DomainException::class);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||
namespace Application\UseCases\CampaignRequest\List;
|
||||
|
||||
use DateTime;
|
||||
use Domain\Enums\BarcodeFormat;
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
|
|
@ -17,15 +16,20 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria;
|
|||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\GS1;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
|
||||
use Domain\Ports\Repositories\CampaignRequestRepository;
|
||||
|
|
@ -39,69 +43,87 @@ use Shared\ValueObjects\Pagination\Pagination;
|
|||
covers(ListCampaignsRequest::class, Input::class, Output::class, CampaignRequestListItem::class);
|
||||
|
||||
describe('list campaigns request use case', function (): void {
|
||||
beforeEach(function (): void {
|
||||
$this->couponA = new RequestedCoupon(
|
||||
new CouponName('Coupon A'),
|
||||
new RequestedCouponQuantity(100),
|
||||
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
new ProcessingCode('PC1'),
|
||||
new CouponValidity(30, new DateTime('2026-02-01')),
|
||||
[
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
],
|
||||
[new IncoCode(35784), new IncoCode(15522)],
|
||||
new PrintRules(maxPerCustomer: 3),
|
||||
new GS1('01234567890128', BarcodeFormat::EAN13)
|
||||
);
|
||||
$couponA = new RequestedCoupon(
|
||||
couponName: new CouponName('Coupon A'),
|
||||
quantity: new RequestedCouponQuantity(100),
|
||||
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
couponPicture: new CouponPicture(
|
||||
new PicturePath('files/product.jpg'),
|
||||
new PicturePath('files/thumbnail.jpg'),
|
||||
new PicturePath('files/mobileThumbnail.jpg')
|
||||
),
|
||||
couponProcessingCodes: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
new ProcessingCode('4006381333931', Country::BENELUX),
|
||||
]),
|
||||
couponValidityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
]),
|
||||
couponLegalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
]),
|
||||
couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]),
|
||||
printRules: new PrintRules(maxPerCustomer: 3),
|
||||
);
|
||||
|
||||
$this->couponB = new RequestedCoupon(
|
||||
new CouponName('Coupon B'),
|
||||
new RequestedCouponQuantity(100),
|
||||
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
new ProcessingCode('PC2'),
|
||||
new CouponValidity(30, new DateTime('2026-02-01')),
|
||||
[
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
],
|
||||
[new IncoCode(24514), new IncoCode(46259)],
|
||||
new PrintRules(maxPerCustomer: 3),
|
||||
new GS1('01234567890128', BarcodeFormat::EAN13)
|
||||
);
|
||||
$couponB = new RequestedCoupon(
|
||||
couponName: new CouponName('Coupon B'),
|
||||
quantity: new RequestedCouponQuantity(100),
|
||||
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
couponPicture: new CouponPicture(
|
||||
new PicturePath('files/product.jpg'),
|
||||
new PicturePath('files/thumbnail.jpg'),
|
||||
new PicturePath('files/mobileThumbnail.jpg')
|
||||
),
|
||||
couponProcessingCodes: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
new ProcessingCode('4006381333931', Country::BENELUX),
|
||||
]),
|
||||
couponValidityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
]),
|
||||
couponLegalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
]),
|
||||
couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]),
|
||||
printRules: new PrintRules(maxPerCustomer: 3),
|
||||
);
|
||||
|
||||
$this->pagination = Pagination::firstPage();
|
||||
$pagination = Pagination::firstPage();
|
||||
|
||||
$this->sort = new CampaignRequestSortCriteria(
|
||||
clauses: [
|
||||
new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending),
|
||||
]
|
||||
);
|
||||
});
|
||||
$sort = new CampaignRequestSortCriteria(
|
||||
clauses: [
|
||||
new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending),
|
||||
]
|
||||
);
|
||||
|
||||
it('maps repository campaigns request to list items, preserving pagination and sort', function (): void {
|
||||
it('maps repository campaigns request to list items, preserving pagination and sort', function () use ($couponA, $couponB, $pagination): void {
|
||||
// GIVEN
|
||||
$campaignRequest1 = CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
new BrandCode('AAA01'),
|
||||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->couponA,
|
||||
CampaignRequestStatus::CREATED,
|
||||
id: CampaignRequestId::generate(),
|
||||
brandCode: new BrandCode('AAA01'),
|
||||
name: new CampaignName('Campaign A'),
|
||||
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
campaignType: CampaignRequestType::CLUB,
|
||||
requestedCoupon: $couponA,
|
||||
status: CampaignRequestStatus::CREATED,
|
||||
);
|
||||
$campaignRequest2 = CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
new BrandCode('AAA02'),
|
||||
new CampaignName('Campaign B'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->couponB,
|
||||
CampaignRequestStatus::CREATED,
|
||||
id: CampaignRequestId::generate(),
|
||||
brandCode: new BrandCode('AAA02'),
|
||||
name: new CampaignName('Campaign B'),
|
||||
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
campaignType: CampaignRequestType::CLUB,
|
||||
requestedCoupon: $couponB,
|
||||
status: CampaignRequestStatus::CREATED,
|
||||
);
|
||||
$paginated = new Paginated($this->pagination, [$campaignRequest1, $campaignRequest2], 2);
|
||||
$paginated = new Paginated($pagination, [$campaignRequest1, $campaignRequest2], 2);
|
||||
|
||||
$sort = new CampaignRequestSortCriteria(
|
||||
clauses: [
|
||||
|
|
@ -110,12 +132,12 @@ describe('list campaigns request use case', function (): void {
|
|||
]
|
||||
);
|
||||
$filters = new CampaignRequestFilters($campaignRequest2->brandCode, CampaignRequestStatus::CREATED);
|
||||
$input = new Input(pagination: $this->pagination, sort: $sort, filters: $filters);
|
||||
$input = new Input(pagination: $pagination, sort: $sort, filters: $filters);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($this->pagination, CampaignRequestSortCriteria::class, CampaignRequestFilters::class)
|
||||
->with($pagination, CampaignRequestSortCriteria::class, CampaignRequestFilters::class)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCampaignsRequest($repository);
|
||||
|
|
@ -135,29 +157,29 @@ describe('list campaigns request use case', function (): void {
|
|||
->and($items[0]->campaignType)->toBe($campaignRequest1->campaignType)
|
||||
->and($items[0]->status)->toBe($campaignRequest1->status)
|
||||
->and($result->campaignsRequest->getTotal())->toBe(2)
|
||||
->and($result->campaignsRequest->getPagination())->toBe($this->pagination);
|
||||
->and($result->campaignsRequest->getPagination())->toBe($pagination);
|
||||
});
|
||||
|
||||
it('lists campaigns request with filters', function (): void {
|
||||
it('lists campaigns request with filters', function () use ($couponA, $pagination, $sort): void {
|
||||
// GIVEN
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
new BrandCode('AAA01'),
|
||||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->couponA,
|
||||
CampaignRequestStatus::CREATED,
|
||||
id: CampaignRequestId::generate(),
|
||||
brandCode: new BrandCode('AAA01'),
|
||||
name: new CampaignName('Campaign A'),
|
||||
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
campaignType: CampaignRequestType::CLUB,
|
||||
requestedCoupon: $couponA,
|
||||
status: CampaignRequestStatus::CREATED,
|
||||
);
|
||||
$paginated = new Paginated($this->pagination, [$campaignRequest], 1);
|
||||
$paginated = new Paginated($pagination, [$campaignRequest], 1);
|
||||
|
||||
$filters = new CampaignRequestFilters($campaignRequest->brandCode, CampaignRequestStatus::CREATED);
|
||||
$input = new Input(pagination: $this->pagination, sort: $this->sort, filters: $filters);
|
||||
$input = new Input(pagination: $pagination, sort: $sort, filters: $filters);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($this->pagination, $this->sort, $filters)
|
||||
->with($pagination, $sort, $filters)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCampaignsRequest($repository);
|
||||
|
|
@ -173,10 +195,10 @@ describe('list campaigns request use case', function (): void {
|
|||
->and($items[0]->name)->toBe($campaignRequest->name)
|
||||
->and($items[0]->status)->toBe($campaignRequest->status)
|
||||
->and($result->campaignsRequest->getTotal())->toBe(1)
|
||||
->and($result->campaignsRequest->getPagination())->toBe($this->pagination);
|
||||
->and($result->campaignsRequest->getPagination())->toBe($pagination);
|
||||
});
|
||||
|
||||
it('lists campaigns request with sort but without filters', function (): void {
|
||||
it('lists campaigns request with sort but without filters', function () use ($couponA, $couponB, $pagination, $sort): void {
|
||||
// GIVEN
|
||||
$campaignRequest1 = CampaignRequest::hydrate(
|
||||
CampaignRequestId::generate(),
|
||||
|
|
@ -184,7 +206,7 @@ describe('list campaigns request use case', function (): void {
|
|||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->couponA,
|
||||
$couponA,
|
||||
CampaignRequestStatus::CREATED,
|
||||
);
|
||||
$campaignRequest2 = CampaignRequest::hydrate(
|
||||
|
|
@ -193,22 +215,22 @@ describe('list campaigns request use case', function (): void {
|
|||
new CampaignName('Campaign B'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->couponB,
|
||||
$couponB,
|
||||
CampaignRequestStatus::CREATED,
|
||||
);
|
||||
$paginated = new Paginated($this->pagination, [$campaignRequest2, $campaignRequest1], 2);
|
||||
$paginated = new Paginated($pagination, [$campaignRequest2, $campaignRequest1], 2);
|
||||
|
||||
$sort = new CampaignRequestSortCriteria(
|
||||
clauses: [
|
||||
new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Descending),
|
||||
]
|
||||
);
|
||||
$input = new Input(pagination: $this->pagination, sort: $sort);
|
||||
$input = new Input(pagination: $pagination, sort: $sort);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($this->pagination, $sort, null)
|
||||
->with($pagination, $sort, null)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCampaignsRequest($repository);
|
||||
|
|
@ -222,18 +244,18 @@ describe('list campaigns request use case', function (): void {
|
|||
->and($items[0]->brandCode)->toBe($campaignRequest2->brandCode)
|
||||
->and($items[1]->brandCode)->toBe($campaignRequest1->brandCode)
|
||||
->and($result->campaignsRequest->getTotal())->toBe(2)
|
||||
->and($result->campaignsRequest->getPagination())->toBe($this->pagination);
|
||||
->and($result->campaignsRequest->getPagination())->toBe($pagination);
|
||||
});
|
||||
|
||||
it('returns an empty list without error when there are no campaigns request', function (): void {
|
||||
it('returns an empty list without error when there are no campaigns request', function () use ($pagination, $sort): void {
|
||||
// GIVEN
|
||||
$paginated = new Paginated($this->pagination, [], 0);
|
||||
$input = new Input(pagination: $this->pagination, sort: $this->sort);
|
||||
$paginated = new Paginated($pagination, [], 0);
|
||||
$input = new Input(pagination: $pagination, sort: $sort);
|
||||
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($this->pagination, $this->sort, null)
|
||||
->with($pagination, $sort, null)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCampaignsRequest($repository);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ use Application\UseCases\CampaignRequest\Reject\Output\Output;
|
|||
use Application\UseCases\CampaignRequest\Reject\RejectCampaignRequest;
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTime;
|
||||
use Domain\Enums\BarcodeFormat;
|
||||
use Domain\Models\Entities\CampaignRequest;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
|
|
@ -20,15 +19,20 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
|
|||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
|
||||
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\CouponName;
|
||||
use Domain\Models\ValueObjects\GS1;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
|
||||
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
|
||||
use Domain\Ports\Repositories\CampaignRequestRepository;
|
||||
|
|
@ -40,31 +44,39 @@ use Shared\ValueObjects\Money;
|
|||
covers(RejectCampaignRequest::class, Input::class, CampaignRequestRejected::class, CampaignRequestNotFound::class);
|
||||
|
||||
describe('reject campaign request use case', function (): void {
|
||||
beforeEach(function (): void {
|
||||
$this->requestedCoupon = new RequestedCoupon(
|
||||
new CouponName('Coupon A'),
|
||||
new RequestedCouponQuantity(100),
|
||||
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
new ProcessingCode('PC1'),
|
||||
new CouponValidity(30, new DateTime('2026-02-01')),
|
||||
[
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
],
|
||||
[new IncoCode(35784), new IncoCode(15522)],
|
||||
new PrintRules(maxPerCustomer: 3),
|
||||
new GS1('01234567890128', BarcodeFormat::EAN13)
|
||||
);
|
||||
});
|
||||
$requestedCoupon = new RequestedCoupon(
|
||||
couponName: new CouponName('Coupon A'),
|
||||
quantity: new RequestedCouponQuantity(100),
|
||||
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
couponPicture: new CouponPicture(
|
||||
new PicturePath('files/product.jpg'),
|
||||
new PicturePath('files/thumbnail.jpg'),
|
||||
new PicturePath('files/mobileThumbnail.jpg')
|
||||
),
|
||||
couponProcessingCodes: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
new ProcessingCode('4006381333931', Country::BENELUX),
|
||||
]),
|
||||
couponValidityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
]),
|
||||
couponLegalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
|
||||
]),
|
||||
couponProducts: new CouponProductArray([new IncoCode(35784), new IncoCode(15522)]),
|
||||
printRules: new PrintRules(maxPerCustomer: 3),
|
||||
);
|
||||
|
||||
afterEach(function (): void {
|
||||
CarbonImmutable::setTestNow();
|
||||
});
|
||||
|
||||
it('rejects a campaign request and persists it', function (): void {
|
||||
it('rejects a campaign request and persists it', function () use ($requestedCoupon): void {
|
||||
// GIVEN
|
||||
CarbonImmutable::setTestNow('2026-06-15 12:00:00');
|
||||
CarbonImmutable::setTestNow('2026-06-15 12:00:00 UTC');
|
||||
$id = CampaignRequestId::generate();
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
$id,
|
||||
|
|
@ -72,7 +84,7 @@ describe('reject campaign request use case', function (): void {
|
|||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->requestedCoupon,
|
||||
$requestedCoupon,
|
||||
CampaignRequestStatus::CREATED,
|
||||
);
|
||||
$reason = new RejectionReason('Missing legal documents.');
|
||||
|
|
@ -81,8 +93,7 @@ describe('reject campaign request use case', function (): void {
|
|||
$repository->shouldReceive('findById')->once()->with($id)->andReturn($campaignRequest);
|
||||
$repository->shouldReceive('save')->once()->with(\Mockery::on(
|
||||
fn (CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED
|
||||
&& $saved->rejectionReason === $reason
|
||||
&& $saved->updatedAt !== null,
|
||||
&& $saved->rejectionReason === $reason,
|
||||
));
|
||||
|
||||
$useCase = new RejectCampaignRequest($repository);
|
||||
|
|
@ -94,8 +105,7 @@ describe('reject campaign request use case', function (): void {
|
|||
expect($result)->toBeInstanceOf(CampaignRequestRejected::class)
|
||||
->and($result->id)->toBe($id)
|
||||
->and($result->status)->toBe(CampaignRequestStatus::REJECTED)
|
||||
->and($result->rejectionReason)->toBe($reason)
|
||||
->and($result->updatedAt->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00');
|
||||
->and($result->rejectionReason)->toBe($reason);
|
||||
});
|
||||
|
||||
it('returns CampaignRequestNotFound when the id is not found', function (): void {
|
||||
|
|
@ -115,7 +125,7 @@ describe('reject campaign request use case', function (): void {
|
|||
expect($result)->toBeInstanceOf(CampaignRequestNotFound::class);
|
||||
});
|
||||
|
||||
it('propagates the domain exception when the campaign request cannot be rejected', function (): void {
|
||||
it('propagates the domain exception when the campaign request cannot be rejected', function () use ($requestedCoupon): void {
|
||||
// GIVEN
|
||||
$id = CampaignRequestId::generate();
|
||||
$campaignRequest = CampaignRequest::hydrate(
|
||||
|
|
@ -124,7 +134,7 @@ describe('reject campaign request use case', function (): void {
|
|||
new CampaignName('Campaign A'),
|
||||
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
|
||||
CampaignRequestType::CLUB,
|
||||
$this->requestedCoupon,
|
||||
$requestedCoupon,
|
||||
CampaignRequestStatus::TRANSFORMED,
|
||||
);
|
||||
|
||||
|
|
@ -142,19 +152,4 @@ describe('reject campaign request use case', function (): void {
|
|||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.');
|
||||
});
|
||||
|
||||
it('never touches the repository when a rejection reason is missing', function (): void {
|
||||
// GIVEN
|
||||
$repository = \Mockery::mock(CampaignRequestRepository::class);
|
||||
$repository->shouldNotReceive('findById');
|
||||
$repository->shouldNotReceive('save');
|
||||
|
||||
$useCase = new RejectCampaignRequest($repository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute(new Input(id: CampaignRequestId::generate()));
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\ArgumentCountError::class);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,343 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\Coupon\Create;
|
||||
|
||||
use Application\UseCases\Coupon\Create\CreateCoupon;
|
||||
use Application\UseCases\Coupon\Create\Input;
|
||||
use Application\UseCases\Coupon\Create\Output;
|
||||
use DateTime;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponId;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovement;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Money;
|
||||
|
||||
covers(CreateCoupon::class, Input::class, Output::class);
|
||||
|
||||
describe('create coupon use case', function (): void {
|
||||
$makeInput = fn (array $overrides = []): Input => new Input(...array_merge([
|
||||
'code' => new CouponCode('COU01'),
|
||||
'name' => new CouponName('Coupon name'),
|
||||
'amount' => new MoneyAmount(new Money(Currency::EUR, 100)),
|
||||
'picture' => new CouponPicture(
|
||||
new PicturePath('files/product.jpg'),
|
||||
new PicturePath('files/thumbnail.jpg'),
|
||||
new PicturePath('files/mobileThumbnail.jpg'),
|
||||
),
|
||||
'diffusion' => new CouponDiffusion(
|
||||
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
),
|
||||
'validityInformations' => new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
]),
|
||||
'legalInformations' => new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
]),
|
||||
'processingCode' => new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
]),
|
||||
'brandCode' => new BrandCode('TST89'),
|
||||
'createdBy' => ActorId::generate(),
|
||||
'products' => new CouponProductArray(),
|
||||
'movements' => new CouponMovementArray([
|
||||
new CouponMovement(100),
|
||||
]),
|
||||
'availabilityCalendar' => null,
|
||||
], $overrides));
|
||||
|
||||
it('creates a coupon when the brand exists and the code is unique', function () use ($makeInput): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('existsByCode')->once()->with($input->code)->andReturn(false);
|
||||
$couponRepository->shouldReceive('save')->once()->with(\Mockery::type(Coupon::class));
|
||||
|
||||
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class)
|
||||
->and($result->id)->toBeInstanceOf(CouponId::class)
|
||||
->and($result->code)->toEqual($input->code)
|
||||
->and($result->name)->toEqual($input->name)
|
||||
->and($result->amount)->toEqual($input->amount)
|
||||
->and($result->picture)->toEqual($input->picture)
|
||||
->and($result->diffusion)->toEqual($input->diffusion)
|
||||
->and($result->validityInformations)->toEqual($input->validityInformations)
|
||||
->and($result->legalInformations)->toEqual($input->legalInformations)
|
||||
->and($result->processingCode)->toEqual($input->processingCode)
|
||||
->and($result->brandCode)->toEqual($input->brandCode)
|
||||
->and($result->createdBy)->toEqual($input->createdBy)
|
||||
->and($result->products)->toEqual($input->products)
|
||||
->and($result->movements)->toEqual($input->movements)
|
||||
->and($result->availabilityCalendar)->toBeNull();
|
||||
});
|
||||
|
||||
it('throws when the brand does not exist', function () use ($makeInput): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(false);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldNotReceive('existsByCode');
|
||||
$couponRepository->shouldNotReceive('save');
|
||||
|
||||
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Brand code "TST89" was not found.');
|
||||
});
|
||||
|
||||
it('throws when the coupon code already exists', function () use ($makeInput): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('existsByCode')->once()->with($input->code)->andReturn(true);
|
||||
$couponRepository->shouldNotReceive('save');
|
||||
|
||||
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Coupon code "COU01" already exists.');
|
||||
});
|
||||
|
||||
it('creates a coupon with an availability calendar', function () use ($makeInput): void {
|
||||
// GIVEN
|
||||
$calendar = new CouponAvailabilityCalendar([
|
||||
new Period(new DateTime('2026-01-05'), new DateTime('2026-01-15')),
|
||||
new Period(new DateTime('2026-01-20'), new DateTime('2026-01-25')),
|
||||
]);
|
||||
$input = $makeInput(['availabilityCalendar' => $calendar]);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->andReturn(true);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('existsByCode')->once()->andReturn(false);
|
||||
$couponRepository->shouldReceive('save')->once()->with(\Mockery::on(
|
||||
fn (Coupon $saved): bool => $saved->availabilityCalendar === $calendar,
|
||||
));
|
||||
|
||||
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result->availabilityCalendar)->toBe($calendar);
|
||||
});
|
||||
|
||||
it('creates a GS1 coupon with France and Benelux', function () use ($makeInput): void {
|
||||
// GIVEN
|
||||
$processingCode = new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
new ProcessingCode('5425003944147', Country::BENELUX),
|
||||
]);
|
||||
$validityInformations = new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
||||
]);
|
||||
$legalInformations = new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
||||
]);
|
||||
$input = $makeInput([
|
||||
'processingCode' => $processingCode,
|
||||
'validityInformations' => $validityInformations,
|
||||
'legalInformations' => $legalInformations,
|
||||
]);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->andReturn(true);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('existsByCode')->once()->andReturn(false);
|
||||
$couponRepository->shouldReceive('save')->once()->with(\Mockery::type(Coupon::class));
|
||||
|
||||
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result->processingCode)->toBe($processingCode)
|
||||
->and($result->validityInformations)->toBe($validityInformations)
|
||||
->and($result->legalInformations)->toBe($legalInformations);
|
||||
});
|
||||
|
||||
it('rejects a coupon that targets only benelux', function () use ($makeInput): void {
|
||||
// GIVEN / WHEN
|
||||
$action = fn (): Input => $makeInput([
|
||||
'processingCode' => new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944147', Country::BENELUX),
|
||||
]),
|
||||
'validityInformations' => new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
||||
]),
|
||||
'legalInformations' => new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
||||
]),
|
||||
]);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class);
|
||||
});
|
||||
|
||||
it('rejects a benelux processing code without benelux legal information', function () use ($makeInput): void {
|
||||
// GIVEN
|
||||
$input = $makeInput([
|
||||
'processingCode' => new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
new ProcessingCode('5425003944147', Country::BENELUX),
|
||||
]),
|
||||
'validityInformations' => new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
||||
]),
|
||||
]);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->andReturn(true);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('existsByCode')->andReturn(false);
|
||||
$couponRepository->shouldNotReceive('save');
|
||||
|
||||
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.');
|
||||
});
|
||||
|
||||
it('rejects a benelux processing code without benelux validity information', function () use ($makeInput): void {
|
||||
// GIVEN
|
||||
$input = $makeInput([
|
||||
'processingCode' => new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
new ProcessingCode('5425003944147', Country::BENELUX),
|
||||
]),
|
||||
'legalInformations' => new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
||||
]),
|
||||
]);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->andReturn(true);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('existsByCode')->andReturn(false);
|
||||
$couponRepository->shouldNotReceive('save');
|
||||
|
||||
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'A coupon requires validity information for BENELUX.');
|
||||
});
|
||||
|
||||
it('rejects two processing codes for the same country', function () use ($makeInput): void {
|
||||
// GIVEN / WHEN
|
||||
$action = fn (): Input => $makeInput([
|
||||
'processingCode' => new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
new ProcessingCode('5425003944147', Country::FRANCE),
|
||||
]),
|
||||
]);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class);
|
||||
});
|
||||
|
||||
it('rejects two legal informations for the same country', function () use ($makeInput): void {
|
||||
// GIVEN / WHEN
|
||||
$action = fn (): Input => $makeInput([
|
||||
'legalInformations' => new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
]),
|
||||
]);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class);
|
||||
});
|
||||
|
||||
it('rejects two validity informations for the same country', function () use ($makeInput): void {
|
||||
// GIVEN / WHEN
|
||||
$action = fn (): Input => $makeInput([
|
||||
'validityInformations' => new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
||||
]),
|
||||
]);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create coupon use case assertions', function (): void {
|
||||
it('rejects an input of the wrong type', function (): void {
|
||||
// GIVEN
|
||||
$useCase = new CreateCoupon(
|
||||
\Mockery::mock(CouponRepository::class),
|
||||
\Mockery::mock(BrandRepository::class),
|
||||
);
|
||||
$invalidInput = new class () implements BaseInput
|
||||
{
|
||||
};
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($invalidInput);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
208
tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php
Normal file
208
tests/Unit/Application/UseCases/Coupon/Find/FindCouponTest.php
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\Coupon\Find;
|
||||
|
||||
use Application\UseCases\Coupon\Find\FindCoupon;
|
||||
use Application\UseCases\Coupon\Find\Input;
|
||||
use Application\UseCases\Coupon\Find\Output\CouponFound;
|
||||
use Application\UseCases\Coupon\Find\Output\CouponNotFound;
|
||||
use Application\UseCases\Coupon\Find\Output\Output;
|
||||
use DateTime;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\Entities\Campaign;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCouponArray;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovement;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Domain\Ports\Repositories\CampaignRepository;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Email;
|
||||
use Shared\ValueObjects\Money;
|
||||
use Shared\ValueObjects\Rcs;
|
||||
|
||||
covers(FindCoupon::class, Input::class, CouponFound::class, CouponNotFound::class);
|
||||
|
||||
describe('find coupon use case', function (): void {
|
||||
$brand = Brand::create(
|
||||
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'),
|
||||
);
|
||||
$coupon = Coupon::create(
|
||||
code: new CouponCode('COU01'),
|
||||
name: new CouponName('Coupon name'),
|
||||
amount: new MoneyAmount(new Money(Currency::EUR, 100)),
|
||||
picture: new CouponPicture(
|
||||
new PicturePath('files/product.jpg'),
|
||||
new PicturePath('files/thumbnail.jpg'),
|
||||
new PicturePath('files/mobile_thumbnail.jpg'),
|
||||
),
|
||||
diffusion: new CouponDiffusion(
|
||||
new Period(
|
||||
new DateTime('2026-01-01'),
|
||||
new DateTime('2026-02-01'),
|
||||
),
|
||||
),
|
||||
validityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation(
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
||||
Country::FRANCE
|
||||
),
|
||||
]),
|
||||
legalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation(
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
||||
Country::FRANCE
|
||||
),
|
||||
]),
|
||||
processingCode: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
]),
|
||||
brandCode: $brand->code,
|
||||
createdBy: ActorId::generate(),
|
||||
products: new CouponProductArray([
|
||||
new IncoCode(35784),
|
||||
new IncoCode(15522),
|
||||
]),
|
||||
movements: new CouponMovementArray([
|
||||
new CouponMovement(1000)
|
||||
]),
|
||||
);
|
||||
$campaign = Campaign::create(
|
||||
code: new CampaignCode('CMP01'),
|
||||
name: new CampaignName('Campaign name'),
|
||||
publication: true,
|
||||
printAllAtOnce: true,
|
||||
coupons: new CampaignCouponArray([
|
||||
new CampaignCoupon($coupon->code),
|
||||
]),
|
||||
brandCode: $brand->code,
|
||||
actorId: ActorId::generate(),
|
||||
);
|
||||
|
||||
it('finds a coupon by its code', function () use ($coupon, $campaign): void {
|
||||
// GIVEN
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($coupon->brandCode)->andReturnTrue();
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCouponCode')->once()->with($coupon->code)->andReturn([$campaign]);
|
||||
|
||||
$useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(code: $coupon->code));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CouponFound::class)
|
||||
->and($result->code)->toBe($coupon->code)
|
||||
->and($result->name)->toBe($coupon->name)
|
||||
->and($result->amount)->toBe($coupon->amount)
|
||||
->and($result->picture)->toBe($coupon->picture)
|
||||
->and($result->diffusion)->toBe($coupon->diffusion)
|
||||
->and($result->availabilityCalendar)->toBe($coupon->availabilityCalendar)
|
||||
->and($result->validityInformations)->toBe($coupon->validityInformations)
|
||||
->and($result->legalInformations)->toBe($coupon->legalInformations)
|
||||
->and($result->processingCode)->toBe($coupon->processingCode)
|
||||
->and($result->brandCode)->toBe($coupon->brandCode)
|
||||
->and($result->createdBy)->toBe($coupon->createdBy)
|
||||
->and($result->products)->toBe($coupon->products)
|
||||
->and($result->movements)->toBe($coupon->movements)
|
||||
->and($result->quantity)->toBe($coupon->quantity())
|
||||
->and($result->campaigns)->toBe([$campaign])
|
||||
->and($result->createdAt)->toBeInstanceOf(\DateTimeInterface::class)
|
||||
->and($result->updatedAt)->toBeNull();
|
||||
});
|
||||
|
||||
it('returns CouponNotFound when the code is not found', function (): void {
|
||||
// GIVEN
|
||||
$code = new CouponCode('COU02');
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(null);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldNotReceive('existsByCode');
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldNotReceive('findByCouponCode');
|
||||
|
||||
$useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(code: $code));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CouponNotFound::class);
|
||||
});
|
||||
|
||||
it('throws when the brand of the coupon is not found', function () use ($coupon): void {
|
||||
// GIVEN
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($coupon->brandCode)->andReturnFalse();
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldNotReceive('findByCouponCode');
|
||||
|
||||
$useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository);
|
||||
|
||||
// WHEN / THEN
|
||||
expect(fn (): Output => $useCase->execute(new Input(code: $coupon->code)))
|
||||
->toThrow(\DomainException::class);
|
||||
});
|
||||
|
||||
it('finds a coupon not attached to any campaign', function () use ($coupon): void {
|
||||
// GIVEN
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$brandRepository = \Mockery::mock(BrandRepository::class);
|
||||
$brandRepository->shouldReceive('existsByCode')->once()->with($coupon->brandCode)->andReturnTrue();
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCouponCode')->once()->with($coupon->code)->andReturn([]);
|
||||
|
||||
$useCase = new FindCoupon($couponRepository, $brandRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute(new Input(code: $coupon->code));
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CouponFound::class)
|
||||
->and($result->campaigns)->toBe([]);
|
||||
});
|
||||
});
|
||||
228
tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php
Normal file
228
tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\Coupon\List;
|
||||
|
||||
use Application\UseCases\Coupon\List\CouponListItem;
|
||||
use Application\UseCases\Coupon\List\Input;
|
||||
use Application\UseCases\Coupon\List\ListCoupons;
|
||||
use Application\UseCases\Coupon\List\Output;
|
||||
use DateTime;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponId;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovement;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponSortCriteria;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponSortField;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Money;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
|
||||
covers(ListCoupons::class, Input::class, Output::class, CouponListItem::class);
|
||||
|
||||
describe('list coupons use case', function (): void {
|
||||
$coupon1 = Coupon::hydrate(
|
||||
id: CouponId::generate(),
|
||||
code: new CouponCode('AAA01'),
|
||||
name: new CouponName('Coupon 1'),
|
||||
amount: new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
picture: new CouponPicture(
|
||||
new PicturePath('files/product-1.jpg'),
|
||||
new PicturePath('files/thumbnail-1.jpg'),
|
||||
new PicturePath('files/mobileThumbnail-1.jpg')
|
||||
),
|
||||
diffusion: new CouponDiffusion(new Period(
|
||||
new DateTime('2026-01-01'),
|
||||
new DateTime('2026-02-01'),
|
||||
)),
|
||||
validityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet.', Country::FRANCE),
|
||||
]),
|
||||
legalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet.', Country::FRANCE),
|
||||
]),
|
||||
processingCode: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
]),
|
||||
brandCode: new BrandCode('AAA01'),
|
||||
createdBy: ActorId::generate(),
|
||||
products: new CouponProductArray([new IncoCode(35784)]),
|
||||
movements: new CouponMovementArray([new CouponMovement(100)]),
|
||||
availabilityCalendar: new CouponAvailabilityCalendar([
|
||||
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
]),
|
||||
);
|
||||
$coupon2 = Coupon::hydrate(
|
||||
id: CouponId::generate(),
|
||||
code: new CouponCode('BBB02'),
|
||||
name: new CouponName('Coupon 2'),
|
||||
amount: new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
picture: new CouponPicture(
|
||||
new PicturePath('files/product-2.jpg'),
|
||||
new PicturePath('files/thumbnail-2.jpg'),
|
||||
new PicturePath('files/mobileThumbnail-2.jpg')
|
||||
),
|
||||
diffusion: new CouponDiffusion(new Period(
|
||||
new DateTime('2027-01-01'),
|
||||
new DateTime('2027-02-01'),
|
||||
)),
|
||||
validityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet.', Country::FRANCE),
|
||||
]),
|
||||
legalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet.', Country::FRANCE),
|
||||
]),
|
||||
processingCode: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944147', Country::FRANCE),
|
||||
]),
|
||||
brandCode: new BrandCode('BBB02'),
|
||||
createdBy: ActorId::generate(),
|
||||
products: new CouponProductArray([new IncoCode(35784)]),
|
||||
movements: new CouponMovementArray([new CouponMovement(100)]),
|
||||
);
|
||||
$pagination = Pagination::firstPage();
|
||||
$brandCode = new BrandCode('AAA01');
|
||||
|
||||
it('maps repository coupons to list items, preserving pagination and sort', function () use (
|
||||
$coupon1,
|
||||
$coupon2,
|
||||
$pagination,
|
||||
$brandCode,
|
||||
): void {
|
||||
// GIVEN
|
||||
$paginated = new Paginated($pagination, [$coupon1, $coupon2], 2);
|
||||
$sort = new CouponSortCriteria();
|
||||
$input = new Input(pagination: $pagination, sort: $sort, brandCode: $brandCode);
|
||||
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($pagination, CouponSortCriteria::class, $brandCode)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCoupons($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class);
|
||||
|
||||
$items = $result->coupons->getItems();
|
||||
expect($items)->toHaveCount(2)
|
||||
->and($items[0])->toBeInstanceOf(CouponListItem::class)
|
||||
->and($items[0]->code)->toBe($coupon1->code)
|
||||
->and($items[0]->name)->toBe($coupon1->name)
|
||||
->and($items[0]->brandCode)->toBe($coupon1->brandCode)
|
||||
->and($items[0]->picture)->toBe($coupon1->picture)
|
||||
->and($items[0]->diffusion)->toBe($coupon1->diffusion)
|
||||
->and($items[0]->availabilityCalendar)->toBe($coupon1->availabilityCalendar)
|
||||
->and($items[1]->code)->toBe($coupon2->code)
|
||||
->and($items[1]->brandCode)->toBe($coupon2->brandCode)
|
||||
->and($items[1]->availabilityCalendar)->toBeNull()
|
||||
->and($result->coupons->getTotal())->toBe(2)
|
||||
->and($result->coupons->getPagination())->toBe($pagination);
|
||||
});
|
||||
|
||||
it('maps repository coupons to list items, preserving specific pagination and sort', function () use (
|
||||
$coupon1,
|
||||
$coupon2,
|
||||
$pagination,
|
||||
): void {
|
||||
// GIVEN
|
||||
$paginated = new Paginated($pagination, [$coupon2, $coupon1], 2);
|
||||
$sort = new CouponSortCriteria(field: CouponSortField::BrandCode, direction: \SortDirection::Descending);
|
||||
$input = new Input(pagination: $pagination, sort: $sort);
|
||||
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($pagination, CouponSortCriteria::class, null)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCoupons($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class);
|
||||
|
||||
$items = $result->coupons->getItems();
|
||||
expect($items)->toHaveCount(2)
|
||||
->and($items[1])->toBeInstanceOf(CouponListItem::class)
|
||||
->and($items[1]->code)->toBe($coupon1->code)
|
||||
->and($items[1]->name)->toBe($coupon1->name)
|
||||
->and($items[1]->brandCode)->toBe($coupon1->brandCode)
|
||||
->and($items[1]->picture)->toBe($coupon1->picture)
|
||||
->and($items[1]->diffusion)->toBe($coupon1->diffusion)
|
||||
->and($items[1]->availabilityCalendar)->toBe($coupon1->availabilityCalendar)
|
||||
->and($items[0]->code)->toBe($coupon2->code)
|
||||
->and($items[0]->brandCode)->toBe($coupon2->brandCode)
|
||||
->and($items[0]->availabilityCalendar)->toBeNull()
|
||||
->and($result->coupons->getTotal())->toBe(2)
|
||||
->and($result->coupons->getPagination())->toBe($pagination);
|
||||
});
|
||||
|
||||
it('returns an empty list without error when there are no coupons', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage();
|
||||
$paginated = new Paginated($pagination, [], 0);
|
||||
$input = new Input(pagination: $pagination);
|
||||
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($pagination, null, null)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCoupons($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result->coupons->getItems())->toBe([])
|
||||
->and($result->coupons->getTotal())->toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list coupons use case assertions', function (): void {
|
||||
it('rejects an input of the wrong type', function (): void {
|
||||
// GIVEN
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$useCase = new ListCoupons($repository);
|
||||
$invalidInput = new class () implements BaseInput
|
||||
{
|
||||
};
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($invalidInput);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\Coupon\Update;
|
||||
|
||||
use Application\UseCases\Coupon\Update\Input;
|
||||
use Application\UseCases\Coupon\Update\Output\CouponNotFound;
|
||||
use Application\UseCases\Coupon\Update\Output\CouponUpdated;
|
||||
use Application\UseCases\Coupon\Update\UpdateCoupon;
|
||||
use DateTime;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponId;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovement;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Money;
|
||||
|
||||
covers(UpdateCoupon::class, Input::class, CouponUpdated::class, CouponNotFound::class);
|
||||
|
||||
describe('update coupon use case', function (): void {
|
||||
$makeCoupon = static fn (): Coupon => Coupon::hydrate(
|
||||
id: CouponId::generate(),
|
||||
code: new CouponCode('COU01'),
|
||||
name: new CouponName('Coupon name'),
|
||||
amount: new MoneyAmount(new Money(Currency::EUR, 100)),
|
||||
picture: new CouponPicture(
|
||||
new PicturePath('files/decoration-old.jpg'),
|
||||
new PicturePath('files/thumbnail-old.jpg'),
|
||||
new PicturePath('files/mobile-thumbnail-old.jpg'),
|
||||
),
|
||||
diffusion: new CouponDiffusion(
|
||||
new Period(
|
||||
new DateTime('2026-01-01'),
|
||||
new DateTime('2026-02-01'),
|
||||
),
|
||||
),
|
||||
validityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation(
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
||||
Country::FRANCE,
|
||||
),
|
||||
]),
|
||||
legalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation(
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
||||
Country::FRANCE,
|
||||
),
|
||||
]),
|
||||
processingCode: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
]),
|
||||
brandCode: new BrandCode('AAA01'),
|
||||
createdBy: ActorId::generate(),
|
||||
products: new CouponProductArray([
|
||||
new IncoCode(35784),
|
||||
new IncoCode(15522),
|
||||
]),
|
||||
movements: new CouponMovementArray([
|
||||
new CouponMovement(1000),
|
||||
]),
|
||||
);
|
||||
|
||||
it('updates the picture and keeps the other fields', function () use ($makeCoupon): void {
|
||||
// GIVEN
|
||||
$coupon = $makeCoupon();
|
||||
$newPicture = new CouponPicture(
|
||||
new PicturePath('files/decoration-new.jpg'),
|
||||
new PicturePath('files/thumbnail-new.jpg'),
|
||||
new PicturePath('files/mobile-thumbnail-new.jpg'),
|
||||
);
|
||||
$input = new Input(code: $coupon->code, picture: $newPicture);
|
||||
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$repository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
$repository->shouldReceive('save')->once()->with(\Mockery::on(
|
||||
fn (Coupon $saved): bool => $saved->picture === $newPicture
|
||||
&& $saved->id === $coupon->id
|
||||
&& $saved->code === $coupon->code
|
||||
&& $saved->name === $coupon->name
|
||||
&& $saved->amount === $coupon->amount
|
||||
&& $saved->diffusion === $coupon->diffusion
|
||||
&& $saved->updatedAt instanceof \DateTimeInterface,
|
||||
));
|
||||
|
||||
$useCase = new UpdateCoupon($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CouponUpdated::class)
|
||||
->and($result->code)->toBe($coupon->code)
|
||||
->and($result->picture)->toBe($newPicture);
|
||||
});
|
||||
|
||||
it('returns CouponNotFound when the coupon does not exist', function (): void {
|
||||
// GIVEN
|
||||
$code = new CouponCode('COU01');
|
||||
$input = new Input(
|
||||
code: $code,
|
||||
picture: new CouponPicture(
|
||||
new PicturePath('files/decoration-new.jpg'),
|
||||
new PicturePath('files/thumbnail-new.jpg'),
|
||||
new PicturePath('files/mobile-thumbnail-new.jpg'),
|
||||
),
|
||||
);
|
||||
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$repository->shouldReceive('findByCode')->once()->with($code)->andReturn(null);
|
||||
$repository->shouldNotReceive('save');
|
||||
|
||||
$useCase = new UpdateCoupon($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CouponNotFound::class);
|
||||
});
|
||||
|
||||
it('updates the legal and validity information and keeps the other fields', function () use ($makeCoupon): void {
|
||||
// GIVEN
|
||||
$coupon = $makeCoupon();
|
||||
$newValidityInformations = new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('New validity information.', Country::FRANCE),
|
||||
]);
|
||||
$newLegalInformations = new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('New legal information.', Country::FRANCE),
|
||||
]);
|
||||
$input = new Input(
|
||||
code: $coupon->code,
|
||||
validityInformations: $newValidityInformations,
|
||||
legalInformations: $newLegalInformations,
|
||||
);
|
||||
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$repository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
$repository->shouldReceive('save')->once()->with(\Mockery::on(
|
||||
fn (Coupon $saved): bool => $saved->validityInformations === $newValidityInformations
|
||||
&& $saved->legalInformations === $newLegalInformations
|
||||
&& $saved->id === $coupon->id
|
||||
&& $saved->name === $coupon->name
|
||||
&& $saved->amount === $coupon->amount
|
||||
&& $saved->picture === $coupon->picture
|
||||
&& $saved->diffusion === $coupon->diffusion
|
||||
&& $saved->movements === $coupon->movements
|
||||
&& $saved->updatedAt instanceof \DateTimeInterface,
|
||||
));
|
||||
|
||||
$useCase = new UpdateCoupon($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(CouponUpdated::class)
|
||||
->and($result->validityInformations)->toBe($newValidityInformations)
|
||||
->and($result->legalInformations)->toBe($newLegalInformations);
|
||||
});
|
||||
|
||||
it('rejects an input with nothing to update', function (): void {
|
||||
// GIVEN
|
||||
$code = new CouponCode('COU01');
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Input => new Input(code: $code);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,429 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\PrintableCoupon\Create;
|
||||
|
||||
use Application\UseCases\PrintableCoupon\Create\Input;
|
||||
use Application\UseCases\PrintableCoupon\Create\Output;
|
||||
use Application\UseCases\PrintableCoupon\Create\CreatePrintableCoupon;
|
||||
use DateTime;
|
||||
use Domain\Models\Entities\Campaign;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\Entities\PrintableCoupon;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCouponArray;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovement;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\CrmHashcode;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\MarketingDirectCode;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponCode;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintedCoupon;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintedCouponId;
|
||||
use Domain\Models\ValueObjects\PrintRules;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Domain\Ports\Repositories\CampaignRepository;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Domain\Ports\Repositories\PrintableCouponRepository;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\Helpers\DatesHelper;
|
||||
use Shared\ValueObjects\Money;
|
||||
|
||||
covers(CreatePrintableCoupon::class, Input::class, Output::class);
|
||||
|
||||
describe('create coupon use case', function (): void {
|
||||
$crmCustomerHashcode = new CrmHashcode(Uuid::uuid4());
|
||||
$brandCode = new BrandCode('AAA01');
|
||||
$campaignCode = new CampaignCode('CMP01');
|
||||
$couponCode = new CouponCode('COU01');
|
||||
$couponName = new CouponName('Coupon name');
|
||||
$couponDiffusionStart = new DateTime()->modify('-1 month');
|
||||
$couponDiffusionEnd = new DateTime()->modify('+1 month');
|
||||
$makeCoupon = fn (array $overrides = []): Coupon => Coupon::create(
|
||||
code: $couponCode,
|
||||
name: $couponName,
|
||||
amount: new MoneyAmount(new Money(Currency::EUR, 100)),
|
||||
picture: new CouponPicture(
|
||||
new PicturePath('files/product.jpg'),
|
||||
new PicturePath('files/thumbnail.jpg'),
|
||||
new PicturePath('files/mobile_thumbnail.jpg'),
|
||||
),
|
||||
diffusion: new CouponDiffusion(
|
||||
new Period(
|
||||
$overrides['diffusionStart'] ?? $couponDiffusionStart,
|
||||
$overrides['diffusionEnd'] ?? $couponDiffusionEnd,
|
||||
),
|
||||
),
|
||||
validityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation(
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
||||
Country::FRANCE,
|
||||
),
|
||||
]),
|
||||
legalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation(
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
||||
Country::FRANCE,
|
||||
),
|
||||
]),
|
||||
processingCode: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
]),
|
||||
brandCode: $brandCode,
|
||||
createdBy: ActorId::generate(),
|
||||
products: new CouponProductArray([
|
||||
new IncoCode(35784) ,
|
||||
new IncoCode(15522),
|
||||
]),
|
||||
movements: new CouponMovementArray([
|
||||
new CouponMovement(1000)
|
||||
]),
|
||||
availabilityCalendar: $overrides['availabilityCalendar'] ?? null,
|
||||
);
|
||||
$printRules = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null): ?PrintRules => ($maxPerCustomer || $cooldownDays) ? new PrintRules(
|
||||
maxPerCustomer: $maxPerCustomer,
|
||||
cooldownDays: $cooldownDays,
|
||||
) : null;
|
||||
$campaignCoupon = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null): CampaignCoupon => new CampaignCoupon(
|
||||
$couponCode,
|
||||
$printRules($maxPerCustomer, $cooldownDays),
|
||||
);
|
||||
$makeCampaign = fn (?int $maxPerCustomer = null, ?int $cooldownDays = null, bool $published = true): Campaign => Campaign::create(
|
||||
code: $campaignCode,
|
||||
name: new CampaignName('Campaign name'),
|
||||
publication: $published,
|
||||
printAllAtOnce: true,
|
||||
coupons: new CampaignCouponArray([
|
||||
$campaignCoupon($maxPerCustomer, $cooldownDays),
|
||||
]),
|
||||
brandCode: $brandCode,
|
||||
actorId: ActorId::generate(),
|
||||
);
|
||||
$printedCoupon = new PrintedCoupon(
|
||||
id: PrintedCouponId::generate(),
|
||||
marketingCode: new MarketingDirectCode('0000000000001'),
|
||||
printableCode: PrintableCouponCode::generate(),
|
||||
couponCode: $couponCode,
|
||||
campaignCode: $campaignCode,
|
||||
customer: $crmCustomerHashcode,
|
||||
printedAt: DatesHelper::now(),
|
||||
);
|
||||
$makeInput = fn (): Input => new Input(
|
||||
campaignCode: $campaignCode,
|
||||
couponCode: $couponCode,
|
||||
crmCustomerHashcode: $crmCustomerHashcode,
|
||||
);
|
||||
|
||||
it('creates a printable coupon', function () use ($makeInput, $crmCustomerHashcode, $makeCampaign, $makeCoupon, $campaignCoupon): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
$campaign = $makeCampaign();
|
||||
$coupon = $makeCoupon();
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
|
||||
$campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code)
|
||||
->andReturn($campaignCoupon());
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
$printableCouponRepository->shouldReceive('save')->once()->with(\Mockery::type(PrintableCoupon::class));
|
||||
|
||||
$useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class)
|
||||
->and($result->campaignCode)->toEqual($campaign->code)
|
||||
->and($result->couponCode)->toEqual($coupon->code)
|
||||
->and($result->crmCustomerHashcode)->toEqual($crmCustomerHashcode)
|
||||
->and($result->distributableUntil)->toEqual($coupon->diffusion->period->end);
|
||||
});
|
||||
|
||||
it('rejects a printable coupon when campaign not found', function () use ($makeInput, $makeCampaign): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
$campaign = $makeCampaign();
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn(null);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
|
||||
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
|
||||
$useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, sprintf('Campaign from code "%s" not found.', $input->campaignCode->value));
|
||||
});
|
||||
|
||||
it('rejects a printable coupon when campaign is not published', function () use ($makeInput, $makeCampaign): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
$campaign = $makeCampaign(published: false);
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
|
||||
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
|
||||
$useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Campaign must be published.');
|
||||
});
|
||||
|
||||
it('rejects a printable coupon when coupon is not found', function () use ($makeInput, $makeCampaign, $makeCoupon): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
$campaign = $makeCampaign();
|
||||
$coupon = $makeCoupon();
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn(null);
|
||||
|
||||
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
|
||||
$useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, sprintf('Coupon code "%s" not found.', $input->couponCode->value));
|
||||
});
|
||||
|
||||
it('rejects a printable coupon when campaign coupon is not found', function () use ($makeInput, $makeCampaign, $makeCoupon): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
$campaign = $makeCampaign();
|
||||
$coupon = $makeCoupon();
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
|
||||
$campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code)
|
||||
->andReturn(null);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
|
||||
$useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Coupon must be associated to the associated campaign coupon.');
|
||||
});
|
||||
|
||||
it('rejects a printable coupon when outside of diffusion start', function () use ($makeInput, $makeCampaign, $makeCoupon, $campaignCoupon): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
$campaign = $makeCampaign();
|
||||
$coupon = $makeCoupon(['diffusionStart' => new DateTime()->modify('+1 day')]);
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
|
||||
$campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code)
|
||||
->andReturn($campaignCoupon());
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
|
||||
$useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'The printing period has not yet begun.');
|
||||
});
|
||||
|
||||
it('rejects a printable coupon when outside of diffusion end', function () use ($makeInput, $makeCampaign, $makeCoupon, $campaignCoupon): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
$campaign = $makeCampaign();
|
||||
$coupon = $makeCoupon(['diffusionEnd' => new DateTime()->modify('-1 day')]);
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
|
||||
$campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code)
|
||||
->andReturn($campaignCoupon());
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
|
||||
$useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'The printing period has expired.');
|
||||
});
|
||||
|
||||
it('creates a printable coupon when calendar period is inside diffusion period', function () use (
|
||||
$makeInput,
|
||||
$crmCustomerHashcode,
|
||||
$makeCampaign,
|
||||
$makeCoupon,
|
||||
$campaignCoupon,
|
||||
): void {
|
||||
// GIVEN
|
||||
$calendarStart = new DateTime()->modify('-1 day');
|
||||
$calendarEnd = new DateTime()->modify('+1 day');
|
||||
$input = $makeInput();
|
||||
$campaign = $makeCampaign();
|
||||
$coupon = $makeCoupon([
|
||||
'availabilityCalendar' => new CouponAvailabilityCalendar([
|
||||
new Period(
|
||||
start: $calendarStart,
|
||||
end: $calendarEnd,
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
|
||||
$campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code)
|
||||
->andReturn($campaignCoupon());
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
$printableCouponRepository->shouldReceive('save')->once()->with(\Mockery::type(PrintableCoupon::class));
|
||||
|
||||
$useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class)
|
||||
->and($result->campaignCode)->toEqual($campaign->code)
|
||||
->and($result->crmCustomerHashcode)->toEqual($crmCustomerHashcode)
|
||||
->and($result->distributableUntil)->toEqual($calendarEnd);
|
||||
});
|
||||
|
||||
it('rejects a printable coupon when maximum number of prints reached', function () use (
|
||||
$makeInput,
|
||||
$crmCustomerHashcode,
|
||||
$makeCampaign,
|
||||
$makeCoupon,
|
||||
$campaignCoupon,
|
||||
): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
$campaign = $makeCampaign(maxPerCustomer: 1);
|
||||
$coupon = $makeCoupon();
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
|
||||
$campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code)
|
||||
->andReturn($campaignCoupon(maxPerCustomer: 1));
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
$printableCouponRepository->shouldReceive('countPrintedCouponForCustomer')->once()->with(
|
||||
$campaign->code,
|
||||
$coupon->code,
|
||||
$crmCustomerHashcode,
|
||||
)->andReturn(5);
|
||||
|
||||
$useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Maximum number of prints reached.');
|
||||
});
|
||||
|
||||
it('rejects a printable coupon when reprint period has expired', function () use (
|
||||
$makeInput,
|
||||
$crmCustomerHashcode,
|
||||
$makeCampaign,
|
||||
$makeCoupon,
|
||||
$campaignCoupon,
|
||||
$printedCoupon,
|
||||
): void {
|
||||
// GIVEN
|
||||
$input = $makeInput();
|
||||
$campaign = $makeCampaign(cooldownDays: 30);
|
||||
$coupon = $makeCoupon();
|
||||
|
||||
$campaignRepository = \Mockery::mock(CampaignRepository::class);
|
||||
$campaignRepository->shouldReceive('findByCode')->once()->with($campaign->code)->andReturn($campaign);
|
||||
$campaignRepository->shouldReceive('findCampaignCoupon')->once()->with($campaign->code, $coupon->code)
|
||||
->andReturn($campaignCoupon(cooldownDays: 30));
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->with($coupon->code)->andReturn($coupon);
|
||||
|
||||
$printableCouponRepository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
$printableCouponRepository->shouldReceive('countPrintedCouponForCustomer')->once()->with(
|
||||
$campaign->code,
|
||||
$coupon->code,
|
||||
$crmCustomerHashcode,
|
||||
)->andReturn(5);
|
||||
$printableCouponRepository->shouldReceive('findLastPrintedCouponForCustomer')->once()->with(
|
||||
$campaign->code,
|
||||
$coupon->code,
|
||||
$crmCustomerHashcode,
|
||||
)->andReturn($printedCoupon);
|
||||
|
||||
$useCase = new CreatePrintableCoupon($printableCouponRepository, $couponRepository, $campaignRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\DomainException::class, 'Reprint period has not expired.');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\PrintableCoupon\List;
|
||||
|
||||
use Application\UseCases\PrintableCoupon\List\Input;
|
||||
use Application\UseCases\PrintableCoupon\List\ListPrintableCoupons;
|
||||
use Application\UseCases\PrintableCoupon\List\Output;
|
||||
use Application\UseCases\PrintableCoupon\List\PrintableCouponListItem;
|
||||
use DateTime;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\Entities\PrintableCoupon;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Campaign\CampaignCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovement;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\CrmHashcode;
|
||||
use Domain\Models\ValueObjects\IncoCode;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponCode;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponSortCriteria;
|
||||
use Domain\Models\ValueObjects\PrintableCoupon\PrintableCouponSortField;
|
||||
use Domain\Models\ValueObjects\ProcessingCode;
|
||||
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Domain\Ports\Repositories\PrintableCouponRepository;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Money;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
|
||||
covers(ListPrintableCoupons::class, Input::class, Output::class, PrintableCouponListItem::class);
|
||||
|
||||
describe('list printable coupons use case', function (): void {
|
||||
$createPicture = fn (string $name): CouponPicture => new CouponPicture(
|
||||
new PicturePath("files/{$name}.jpg"),
|
||||
new PicturePath("files/{$name}-thumbnail.jpg"),
|
||||
new PicturePath("files/{$name}-mobile.jpg"),
|
||||
);
|
||||
|
||||
$createInput = fn (PrintableCouponSortCriteria | null $sortCriteria = null): Input => new Input(
|
||||
pagination: Pagination::firstPage(),
|
||||
campaignCode: new CampaignCode('AAA01'),
|
||||
crmCustomerHashcode: new CrmHashcode(Uuid::uuid4()),
|
||||
sortCriteria: $sortCriteria ?? new PrintableCouponSortCriteria(
|
||||
field: PrintableCouponSortField::CreatedAt,
|
||||
direction: \SortDirection::Descending,
|
||||
),
|
||||
);
|
||||
|
||||
$createPrintableCoupon = fn (CouponCode $couponCode): PrintableCoupon => PrintableCoupon::hydrate(
|
||||
code: PrintableCouponCode::generate(),
|
||||
campaignCode: new CampaignCode('AAA01'),
|
||||
couponCode: $couponCode,
|
||||
couponName: new CouponName('Coupon name'),
|
||||
crmCustomerHashcode: new CrmHashcode(Uuid::uuid4()),
|
||||
distributableUntil: new DateTime('2027-01-01'),
|
||||
);
|
||||
|
||||
$createCoupon = fn (CouponCode $code, CouponPicture $picture): Coupon => Coupon::create(
|
||||
code: $code,
|
||||
name: new CouponName('Coupon name'),
|
||||
amount: new MoneyAmount(new Money(Currency::EUR, 100)),
|
||||
picture: $picture,
|
||||
diffusion: new CouponDiffusion(
|
||||
new Period(
|
||||
new DateTime('2026-01-01'),
|
||||
new DateTime('2026-02-01'),
|
||||
),
|
||||
),
|
||||
validityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation(
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
||||
Country::FRANCE
|
||||
),
|
||||
]),
|
||||
legalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation(
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
||||
Country::FRANCE
|
||||
),
|
||||
]),
|
||||
processingCode: new ProcessingCodeArray([
|
||||
new ProcessingCode('5425003944130', Country::FRANCE),
|
||||
]),
|
||||
brandCode: new BrandCode('AAA01'),
|
||||
createdBy: ActorId::generate(),
|
||||
products: new CouponProductArray([
|
||||
new IncoCode(35784),
|
||||
new IncoCode(15522),
|
||||
]),
|
||||
movements: new CouponMovementArray([
|
||||
new CouponMovement(1000)
|
||||
]),
|
||||
);
|
||||
|
||||
it('returns the printable coupons with the picture of their coupon', function () use (
|
||||
$createInput,
|
||||
$createPicture,
|
||||
$createCoupon,
|
||||
$createPrintableCoupon,
|
||||
): void {
|
||||
// GIVEN
|
||||
$input = $createInput();
|
||||
$picture = $createPicture('picture');
|
||||
$couponCode = new CouponCode('AAA01');
|
||||
$printable = $createPrintableCoupon($couponCode);
|
||||
$coupon = $createCoupon($couponCode, $picture);
|
||||
|
||||
$paginated = new Paginated(pagination: $input->pagination, items: [$printable], total: 1);
|
||||
|
||||
$repository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
$repository->shouldReceive('findAllAvailable')
|
||||
->once()
|
||||
->with($input->pagination, $input->campaignCode, $input->crmCustomerHashcode, $input->sortCriteria)
|
||||
->andReturn($paginated);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')
|
||||
->once()
|
||||
->with($printable->couponCode)
|
||||
->andReturn($coupon);
|
||||
|
||||
$useCase = new ListPrintableCoupons($repository, $couponRepository);
|
||||
|
||||
// WHEN
|
||||
$output = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
$items = $output->printableCoupons->getItems();
|
||||
|
||||
expect($items)->toHaveCount(1)
|
||||
->and($output->printableCoupons->getTotal())->toBe(1)
|
||||
->and($items[0]->code)->toBe($printable->code)
|
||||
->and($items[0]->couponCode)->toBe($printable->couponCode)
|
||||
->and($items[0]->couponName)->toBe($printable->couponName)
|
||||
->and($items[0]->couponPicture)->toBe($picture)
|
||||
->and($items[0]->distributableUntil)->toBe($printable->distributableUntil);
|
||||
});
|
||||
|
||||
it('forwards the sort criteria and keeps the order returned by the repository', function () use (
|
||||
$createInput,
|
||||
$createPicture,
|
||||
$createCoupon,
|
||||
$createPrintableCoupon,
|
||||
): void {
|
||||
// GIVEN
|
||||
$sortCriteria = new PrintableCouponSortCriteria(
|
||||
field: PrintableCouponSortField::Name,
|
||||
direction: \SortDirection::Descending,
|
||||
);
|
||||
$input = $createInput($sortCriteria);
|
||||
|
||||
$code1 = new CouponCode('AAA01');
|
||||
$code2 = new CouponCode('BBB02');
|
||||
$picture1 = $createPicture('picture-1');
|
||||
$picture2 = $createPicture('picture-2');
|
||||
|
||||
$printableCoupon1 = $createPrintableCoupon($code1);
|
||||
$printableCoupon2 = $createPrintableCoupon($code2);
|
||||
|
||||
$repository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
$repository->shouldReceive('findAllAvailable')
|
||||
->once()
|
||||
->with($input->pagination, $input->campaignCode, $input->crmCustomerHashcode, $sortCriteria)
|
||||
->andReturn(new Paginated(pagination: $input->pagination, items: [$printableCoupon2, $printableCoupon1], total: 2));
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->with($code2)->once()
|
||||
->andReturn($createCoupon($code2, $picture2));
|
||||
$couponRepository->shouldReceive('findByCode')->with($code1)->once()
|
||||
->andReturn($createCoupon($code1, $picture1));
|
||||
|
||||
$useCase = new ListPrintableCoupons($repository, $couponRepository);
|
||||
|
||||
// WHEN
|
||||
$items = $useCase->execute($input)->printableCoupons->getItems();
|
||||
|
||||
// THEN
|
||||
expect($items)->toHaveCount(2)
|
||||
->and($items[0]->code)->toBe($printableCoupon2->code)
|
||||
->and($items[0]->couponName)->toBe($printableCoupon2->couponName)
|
||||
->and($items[0]->couponPicture)->toBe($picture2)
|
||||
->and($items[1]->code)->toBe($printableCoupon1->code)
|
||||
->and($items[1]->couponName)->toBe($printableCoupon1->couponName)
|
||||
->and($items[1]->couponPicture)->toBe($picture1);
|
||||
});
|
||||
|
||||
it('returns an empty list without error when there is no printable coupon', function () use ($createInput): void {
|
||||
// GIVEN
|
||||
$input = $createInput();
|
||||
$paginated = new Paginated(pagination: $input->pagination, items: [], total: 0);
|
||||
|
||||
$repository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
$repository->shouldReceive('findAllAvailable')->once()->andReturn($paginated);
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldNotReceive('findByCode');
|
||||
|
||||
$useCase = new ListPrintableCoupons($repository, $couponRepository);
|
||||
|
||||
// WHEN
|
||||
$output = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($output->printableCoupons->getItems())->toBe([])
|
||||
->and($output->printableCoupons->getTotal())->toBe(0);
|
||||
});
|
||||
|
||||
it('loads a shared coupon only once', function () use (
|
||||
$createInput,
|
||||
$createPicture,
|
||||
$createPrintableCoupon,
|
||||
$createCoupon,
|
||||
): void {
|
||||
// GIVEN
|
||||
$input = $createInput();
|
||||
$picture = $createPicture('picture');
|
||||
$couponCode = new CouponCode('AAA01');
|
||||
$items = [$createPrintableCoupon($couponCode), $createPrintableCoupon($couponCode)];
|
||||
|
||||
$repository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
$repository->shouldReceive('findAllAvailable')
|
||||
->once()
|
||||
->andReturn(new Paginated(pagination: $input->pagination, items: $items, total: 2));
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')
|
||||
->once()
|
||||
->andReturn($createCoupon($couponCode, $picture));
|
||||
|
||||
$useCase = new ListPrintableCoupons($repository, $couponRepository);
|
||||
|
||||
// WHEN
|
||||
$output = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($output->printableCoupons->getItems())->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('throws when the coupon of a printable coupon does not exist', function () use ($createInput, $createPrintableCoupon): void {
|
||||
// GIVEN
|
||||
$input = $createInput();
|
||||
$printable = $createPrintableCoupon(new CouponCode('BBB02'));
|
||||
|
||||
$repository = \Mockery::mock(PrintableCouponRepository::class);
|
||||
$repository->shouldReceive('findAllAvailable')
|
||||
->once()
|
||||
->andReturn(new Paginated(pagination: $input->pagination, items: [$printable], total: 1));
|
||||
|
||||
$couponRepository = \Mockery::mock(CouponRepository::class);
|
||||
$couponRepository->shouldReceive('findByCode')->once()->andReturn(null);
|
||||
|
||||
$useCase = new ListPrintableCoupons($repository, $couponRepository);
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\LogicException::class);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue