test: ✅ update on coupon Entity and related VO

This commit is contained in:
Alexis Gousseau 2026-10-12 17:08:07 +02:00
parent fdbde84728
commit 680c71ea87
No known key found for this signature in database
GPG key ID: 7F3C786EE12DBB72
59 changed files with 2351 additions and 1701 deletions

View file

@ -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);
});
});

View file

@ -48,7 +48,7 @@ describe('update brand use case', function () use ($existingBrand): void {
&& $saved->contact === $newContact
&& $saved->code === $brand->code
&& $saved->name === $brand->name
&& $saved->updatedAt !== null,
&& $saved->updatedAt instanceof \DateTimeInterface,
));
$useCase = new UpdateBrand($repository);
@ -77,7 +77,7 @@ describe('update brand use case', function () use ($existingBrand): void {
&& $saved->rcs === $brand->rcs
&& $saved->code === $brand->code
&& $saved->name === $brand->name
&& $saved->updatedAt !== null,
&& $saved->updatedAt instanceof \DateTimeInterface,
));
$useCase = new UpdateBrand($repository);

View file

@ -8,21 +8,26 @@ use Application\UseCases\CampaignRequest\Create\CreateCampaignRequest;
use Application\UseCases\CampaignRequest\Create\Input;
use Application\UseCases\CampaignRequest\Create\Output;
use DateTime;
use Domain\Exceptions\BrandNotFoundException;
use Domain\Models\Entities\CampaignRequest;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Campaign\CampaignName;
use Domain\Models\ValueObjects\CampaignRequest\Applicant;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
use Domain\Models\ValueObjects\Coupon\CouponValidity;
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponName;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\CouponName;
use Domain\Models\ValueObjects\IncoCode;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\Period;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
use Domain\Ports\Repositories\BrandRepository;
@ -34,33 +39,44 @@ use Shared\ValueObjects\Money;
covers(CreateCampaignRequest::class, Input::class, Output::class);
beforeEach(function (): void {
$this->input = new Input(
describe('create campaign request use case', function (): void {
$makeInput = fn (): Input => new Input(
brandCode: new BrandCode('AAA01'),
name: new CampaignName('Campaign name'),
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
campaignType: CampaignRequestType::CLUB,
requestedCoupon: new RequestedCoupon(
name: new CouponName('Coupon A'),
couponName: new CouponName('Coupon A'),
quantity: new RequestedCouponQuantity(100),
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
processingCode: new ProcessingCode('PC1'),
couponValidity: new CouponValidity(30, new DateTime('2026-02-01')),
couponValidityInformation: [
couponPicture: new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobileThumbnail.jpg')
),
couponProcessingCodes: new CouponProcessingCodeArray([
new CouponProcessingCode('ABC1234567890', Country::FRANCE),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
],
products: [new IncoCode(35784), new IncoCode(15522)],
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponLegalInformations: new CouponLegalInformationArray([
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]),
printRules: new PrintRules(maxPerCustomer: 3)
),
);
});
describe('create campaign request use case', function (): void {
it('creates a campaign request when the brand exists', function (): void {
it('creates a campaign request when the brand exists', function () use ($makeInput): void {
// GIVEN
$input = $makeInput();
$brandRepository = \Mockery::mock(BrandRepository::class);
$brandRepository->shouldReceive('existsByCode')->once()->with($this->input->brandCode)->andReturn(true);
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true);
$campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class);
$campaignRequestRepository->shouldReceive('save')->once()->with(\Mockery::type(CampaignRequest::class));
@ -68,20 +84,20 @@ describe('create campaign request use case', function (): void {
$useCase = new CreateCampaignRequest($campaignRequestRepository, $brandRepository);
// WHEN
$result = $useCase->execute($this->input);
$result = $useCase->execute($input);
// THEN
expect($result)->toBeInstanceOf(Output::class)
->and($result->status)->toBe(CampaignRequestStatus::CREATED)
->and($result->brandCode)->toBe($this->input->brandCode);
->and($result->brandCode)->toBe($input->brandCode);
});
});
describe('create campaign request use case validation rules', function (): void {
it('throws when the brand does not exist', function (): void {
it('throws when the brand does not exist', function () use ($makeInput): void {
// GIVEN
$input = $makeInput();
$brandRepository = \Mockery::mock(BrandRepository::class);
$brandRepository->shouldReceive('existsByCode')->once()->with($this->input->brandCode)->andReturn(false);
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(false);
$campaignRequestRepository = \Mockery::mock(CampaignRequestRepository::class);
$campaignRequestRepository->shouldNotReceive('save');
@ -89,9 +105,9 @@ describe('create campaign request use case validation rules', function (): void
$useCase = new CreateCampaignRequest($campaignRequestRepository, $brandRepository);
// WHEN
$action = fn (): Output => $useCase->execute($this->input);
$action = fn (): Output => $useCase->execute($input);
// THEN
expect($action)->toThrow(BrandNotFoundException::class);
expect($action)->toThrow(\DomainException::class);
});
});

View file

@ -10,8 +10,6 @@ use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestFound;
use Application\UseCases\CampaignRequest\Find\Output\CampaignRequestNotFound;
use Application\UseCases\CampaignRequest\Find\Output\Output;
use DateTime;
use Domain\Enums\BarcodeFormat;
use Domain\Exceptions\BrandNotFoundException;
use Domain\Models\Entities\Brand;
use Domain\Models\Entities\CampaignRequest;
use Domain\Models\ValueObjects\Brand\BrandCode;
@ -23,16 +21,20 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
use Domain\Models\ValueObjects\Coupon\CouponValidity;
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponName;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\CouponName;
use Domain\Models\ValueObjects\GS1;
use Domain\Models\ValueObjects\IncoCode;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\Period;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
use Domain\Ports\Repositories\BrandRepository;
@ -47,44 +49,51 @@ use Shared\ValueObjects\Rcs;
covers(FindCampaignRequest::class, Input::class, CampaignRequestFound::class, CampaignRequestNotFound::class);
describe('find campaign request use case', function (): void {
beforeEach(function (): void {
$this->requestedCoupon = new RequestedCoupon(
new CouponName('Coupon A'),
new RequestedCouponQuantity(100),
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
new MoneyAmount(new Money(Currency::EUR, 300)),
new ProcessingCode('PC1'),
new CouponValidity(30, new DateTime('2026-02-01')),
[
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
],
[new IncoCode(35784), new IncoCode(15522)],
new PrintRules(maxPerCustomer: 3),
new GS1('01234567890128', BarcodeFormat::EAN13)
);
});
$requestedCoupon = new RequestedCoupon(
couponName: new CouponName('Coupon A'),
quantity: new RequestedCouponQuantity(100),
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
couponPicture: new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobileThumbnail.jpg')
),
couponProcessingCodes: new CouponProcessingCodeArray([
new CouponProcessingCode('ABC1234567890', Country::FRANCE),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponLegalInformations: new CouponLegalInformationArray([
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]),
printRules: new PrintRules(maxPerCustomer: 3),
);
it('finds a campaign request by its id, along with its brand', function (): void {
it('finds a campaign request by its id, along with its brand', function () use ($requestedCoupon): void {
// GIVEN
$id = CampaignRequestId::generate();
$brandCode = new BrandCode('AAA01');
$campaignRequest = CampaignRequest::hydrate(
$id,
$brandCode,
new CampaignName('Campaign A'),
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
CampaignRequestType::CLUB,
$this->requestedCoupon,
CampaignRequestStatus::CREATED,
id: $id,
brandCode: $brandCode,
name: new CampaignName('Campaign A'),
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
campaignType: CampaignRequestType::CLUB,
requestedCoupon: $requestedCoupon,
status: CampaignRequestStatus::CREATED,
);
$brand = Brand::hydrate(
BrandId::generate(),
$brandCode,
new BrandName('Brand A'),
new PicturePath('brands/logo.jpg'),
new Rcs('RCS PARIS 111 222 333'),
new Email('contact@example.com'),
id: BrandId::generate(),
code: $brandCode,
name: new BrandName('Brand A'),
logo: new PicturePath('brands/logo.jpg'),
rcs: new Rcs('RCS PARIS 111 222 333'),
contact: new Email('contact@example.com'),
);
$repository = \Mockery::mock(CampaignRequestRepository::class);
@ -109,20 +118,20 @@ describe('find campaign request use case', function (): void {
->and($result->rejectionReason)->toBeNull();
});
it('finds a rejected campaign request with its rejection reason', function (): void {
it('finds a rejected campaign request with its rejection reason', function () use ($requestedCoupon): void {
// GIVEN
$id = CampaignRequestId::generate();
$code = new BrandCode('AAA01');
$rejectionReason = new RejectionReason('Missing legal documents.');
$campaignRequest = CampaignRequest::hydrate(
$id,
$code,
new CampaignName('Campaign A'),
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
CampaignRequestType::CLUB,
$this->requestedCoupon,
CampaignRequestStatus::REJECTED,
$rejectionReason,
id: $id,
brandCode: $code,
name: new CampaignName('Campaign A'),
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
campaignType: CampaignRequestType::CLUB,
requestedCoupon: $requestedCoupon,
status: CampaignRequestStatus::REJECTED,
rejectionReason: $rejectionReason,
);
$repository = \Mockery::mock(CampaignRequestRepository::class);
@ -131,12 +140,12 @@ describe('find campaign request use case', function (): void {
$brandRepository = \Mockery::mock(BrandRepository::class);
$brandRepository->shouldReceive('findByCode')->once()->with($code)->andReturn(
Brand::hydrate(
BrandId::generate(),
$code,
new BrandName('Brand A'),
new PicturePath('brands/logo.jpg'),
new Rcs('RCS PARIS 111 222 333'),
new Email('contact@example.com'),
id: BrandId::generate(),
code: $code,
name: new BrandName('Brand A'),
logo: new PicturePath('brands/logo.jpg'),
rcs: new Rcs('RCS PARIS 111 222 333'),
contact: new Email('contact@example.com'),
)
);
@ -170,18 +179,18 @@ describe('find campaign request use case', function (): void {
expect($result)->toBeInstanceOf(CampaignRequestNotFound::class);
});
it('throws when the campaign request references a brand that no longer exists', function (): void {
it('throws when the campaign request references a brand that no longer exists', function () use ($requestedCoupon): void {
// GIVEN
$id = CampaignRequestId::generate();
$brandCode = new BrandCode('AAA01');
$campaignRequest = CampaignRequest::hydrate(
$id,
$brandCode,
new CampaignName('Campaign A'),
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
CampaignRequestType::CLUB,
$this->requestedCoupon,
CampaignRequestStatus::CREATED,
id: $id,
brandCode: $brandCode,
name: new CampaignName('Campaign A'),
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
campaignType: CampaignRequestType::CLUB,
requestedCoupon: $requestedCoupon,
status: CampaignRequestStatus::CREATED,
);
$repository = \Mockery::mock(CampaignRequestRepository::class);
@ -196,7 +205,7 @@ describe('find campaign request use case', function (): void {
$action = fn (): Output => $useCase->execute(new Input(id: $id));
// THEN
expect($action)->toThrow(BrandNotFoundException::class);
expect($action)->toThrow(\DomainException::class);
});
});

View file

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Application\UseCases\CampaignRequest\List;
use DateTime;
use Domain\Enums\BarcodeFormat;
use Domain\Models\Entities\CampaignRequest;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Campaign\CampaignName;
@ -17,15 +16,20 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortCriteria;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
use Domain\Models\ValueObjects\Coupon\CouponValidity;
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponName;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\CouponName;
use Domain\Models\ValueObjects\GS1;
use Domain\Models\ValueObjects\IncoCode;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\Period;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
use Domain\Ports\Repositories\CampaignRequestRepository;
@ -39,69 +43,85 @@ use Shared\ValueObjects\Pagination\Pagination;
covers(ListCampaignsRequest::class, Input::class, Output::class, CampaignRequestListItem::class);
describe('list campaigns request use case', function (): void {
beforeEach(function (): void {
$this->couponA = new RequestedCoupon(
new CouponName('Coupon A'),
new RequestedCouponQuantity(100),
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
new MoneyAmount(new Money(Currency::EUR, 300)),
new ProcessingCode('PC1'),
new CouponValidity(30, new DateTime('2026-02-01')),
[
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
],
[new IncoCode(35784), new IncoCode(15522)],
new PrintRules(maxPerCustomer: 3),
new GS1('01234567890128', BarcodeFormat::EAN13)
);
$couponA = new RequestedCoupon(
couponName: new CouponName('Coupon A'),
quantity: new RequestedCouponQuantity(100),
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
couponPicture: new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobileThumbnail.jpg')
),
couponProcessingCodes: new CouponProcessingCodeArray([
new CouponProcessingCode('ABC1234567890', Country::FRANCE),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponLegalInformations: new CouponLegalInformationArray([
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]),
printRules: new PrintRules(maxPerCustomer: 3),
);
$this->couponB = new RequestedCoupon(
new CouponName('Coupon B'),
new RequestedCouponQuantity(100),
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
new MoneyAmount(new Money(Currency::EUR, 300)),
new ProcessingCode('PC2'),
new CouponValidity(30, new DateTime('2026-02-01')),
[
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
],
[new IncoCode(24514), new IncoCode(46259)],
new PrintRules(maxPerCustomer: 3),
new GS1('01234567890128', BarcodeFormat::EAN13)
);
$couponB = new RequestedCoupon(
couponName: new CouponName('Coupon B'),
quantity: new RequestedCouponQuantity(100),
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
couponPicture: new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobileThumbnail.jpg')
),
couponProcessingCodes: new CouponProcessingCodeArray([
new CouponProcessingCode('ABC1234567890', Country::FRANCE),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponLegalInformations: new CouponLegalInformationArray([
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]),
printRules: new PrintRules(maxPerCustomer: 3),
);
$this->pagination = Pagination::firstPage();
$pagination = Pagination::firstPage();
$this->sort = new CampaignRequestSortCriteria(
clauses: [
new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending),
]
);
});
$sort = new CampaignRequestSortCriteria(
clauses: [
new CampaignRequestSortClause(field: CampaignRequestSortField::CreatedAt, direction: \SortDirection::Descending),
]
);
it('maps repository campaigns request to list items, preserving pagination and sort', function (): void {
it('maps repository campaigns request to list items, preserving pagination and sort', function () use ($couponA, $couponB, $pagination): void {
// GIVEN
$campaignRequest1 = CampaignRequest::hydrate(
CampaignRequestId::generate(),
new BrandCode('AAA01'),
new CampaignName('Campaign A'),
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
CampaignRequestType::CLUB,
$this->couponA,
CampaignRequestStatus::CREATED,
id: CampaignRequestId::generate(),
brandCode: new BrandCode('AAA01'),
name: new CampaignName('Campaign A'),
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
campaignType: CampaignRequestType::CLUB,
requestedCoupon: $couponA,
status: CampaignRequestStatus::CREATED,
);
$campaignRequest2 = CampaignRequest::hydrate(
CampaignRequestId::generate(),
new BrandCode('AAA02'),
new CampaignName('Campaign B'),
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
CampaignRequestType::CLUB,
$this->couponB,
CampaignRequestStatus::CREATED,
id: CampaignRequestId::generate(),
brandCode: new BrandCode('AAA02'),
name: new CampaignName('Campaign B'),
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
campaignType: CampaignRequestType::CLUB,
requestedCoupon: $couponB,
status: CampaignRequestStatus::CREATED,
);
$paginated = new Paginated($this->pagination, [$campaignRequest1, $campaignRequest2], 2);
$paginated = new Paginated($pagination, [$campaignRequest1, $campaignRequest2], 2);
$sort = new CampaignRequestSortCriteria(
clauses: [
@ -110,12 +130,12 @@ describe('list campaigns request use case', function (): void {
]
);
$filters = new CampaignRequestFilters($campaignRequest2->brandCode, CampaignRequestStatus::CREATED);
$input = new Input(pagination: $this->pagination, sort: $sort, filters: $filters);
$input = new Input(pagination: $pagination, sort: $sort, filters: $filters);
$repository = \Mockery::mock(CampaignRequestRepository::class);
$repository->shouldReceive('findAll')
->once()
->with($this->pagination, CampaignRequestSortCriteria::class, CampaignRequestFilters::class)
->with($pagination, CampaignRequestSortCriteria::class, CampaignRequestFilters::class)
->andReturn($paginated);
$useCase = new ListCampaignsRequest($repository);
@ -135,29 +155,29 @@ describe('list campaigns request use case', function (): void {
->and($items[0]->campaignType)->toBe($campaignRequest1->campaignType)
->and($items[0]->status)->toBe($campaignRequest1->status)
->and($result->campaignsRequest->getTotal())->toBe(2)
->and($result->campaignsRequest->getPagination())->toBe($this->pagination);
->and($result->campaignsRequest->getPagination())->toBe($pagination);
});
it('lists campaigns request with filters', function (): void {
it('lists campaigns request with filters', function () use ($couponA, $pagination, $sort): void {
// GIVEN
$campaignRequest = CampaignRequest::hydrate(
CampaignRequestId::generate(),
new BrandCode('AAA01'),
new CampaignName('Campaign A'),
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
CampaignRequestType::CLUB,
$this->couponA,
CampaignRequestStatus::CREATED,
id: CampaignRequestId::generate(),
brandCode: new BrandCode('AAA01'),
name: new CampaignName('Campaign A'),
applicant: new Applicant('John', 'Doe', new Email('john.doe@example.com')),
campaignType: CampaignRequestType::CLUB,
requestedCoupon: $couponA,
status: CampaignRequestStatus::CREATED,
);
$paginated = new Paginated($this->pagination, [$campaignRequest], 1);
$paginated = new Paginated($pagination, [$campaignRequest], 1);
$filters = new CampaignRequestFilters($campaignRequest->brandCode, CampaignRequestStatus::CREATED);
$input = new Input(pagination: $this->pagination, sort: $this->sort, filters: $filters);
$input = new Input(pagination: $pagination, sort: $sort, filters: $filters);
$repository = \Mockery::mock(CampaignRequestRepository::class);
$repository->shouldReceive('findAll')
->once()
->with($this->pagination, $this->sort, $filters)
->with($pagination, $sort, $filters)
->andReturn($paginated);
$useCase = new ListCampaignsRequest($repository);
@ -173,10 +193,10 @@ describe('list campaigns request use case', function (): void {
->and($items[0]->name)->toBe($campaignRequest->name)
->and($items[0]->status)->toBe($campaignRequest->status)
->and($result->campaignsRequest->getTotal())->toBe(1)
->and($result->campaignsRequest->getPagination())->toBe($this->pagination);
->and($result->campaignsRequest->getPagination())->toBe($pagination);
});
it('lists campaigns request with sort but without filters', function (): void {
it('lists campaigns request with sort but without filters', function () use ($couponA, $couponB, $pagination, $sort): void {
// GIVEN
$campaignRequest1 = CampaignRequest::hydrate(
CampaignRequestId::generate(),
@ -184,7 +204,7 @@ describe('list campaigns request use case', function (): void {
new CampaignName('Campaign A'),
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
CampaignRequestType::CLUB,
$this->couponA,
$couponA,
CampaignRequestStatus::CREATED,
);
$campaignRequest2 = CampaignRequest::hydrate(
@ -193,22 +213,22 @@ describe('list campaigns request use case', function (): void {
new CampaignName('Campaign B'),
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
CampaignRequestType::CLUB,
$this->couponB,
$couponB,
CampaignRequestStatus::CREATED,
);
$paginated = new Paginated($this->pagination, [$campaignRequest2, $campaignRequest1], 2);
$paginated = new Paginated($pagination, [$campaignRequest2, $campaignRequest1], 2);
$sort = new CampaignRequestSortCriteria(
clauses: [
new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Descending),
]
);
$input = new Input(pagination: $this->pagination, sort: $sort);
$input = new Input(pagination: $pagination, sort: $sort);
$repository = \Mockery::mock(CampaignRequestRepository::class);
$repository->shouldReceive('findAll')
->once()
->with($this->pagination, $sort, null)
->with($pagination, $sort, null)
->andReturn($paginated);
$useCase = new ListCampaignsRequest($repository);
@ -222,18 +242,18 @@ describe('list campaigns request use case', function (): void {
->and($items[0]->brandCode)->toBe($campaignRequest2->brandCode)
->and($items[1]->brandCode)->toBe($campaignRequest1->brandCode)
->and($result->campaignsRequest->getTotal())->toBe(2)
->and($result->campaignsRequest->getPagination())->toBe($this->pagination);
->and($result->campaignsRequest->getPagination())->toBe($pagination);
});
it('returns an empty list without error when there are no campaigns request', function (): void {
it('returns an empty list without error when there are no campaigns request', function () use ($pagination, $sort): void {
// GIVEN
$paginated = new Paginated($this->pagination, [], 0);
$input = new Input(pagination: $this->pagination, sort: $this->sort);
$paginated = new Paginated($pagination, [], 0);
$input = new Input(pagination: $pagination, sort: $sort);
$repository = \Mockery::mock(CampaignRequestRepository::class);
$repository->shouldReceive('findAll')
->once()
->with($this->pagination, $this->sort, null)
->with($pagination, $sort, null)
->andReturn($paginated);
$useCase = new ListCampaignsRequest($repository);

View file

@ -11,7 +11,6 @@ use Application\UseCases\CampaignRequest\Reject\Output\Output;
use Application\UseCases\CampaignRequest\Reject\RejectCampaignRequest;
use Carbon\CarbonImmutable;
use DateTime;
use Domain\Enums\BarcodeFormat;
use Domain\Models\Entities\CampaignRequest;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Campaign\CampaignName;
@ -20,15 +19,20 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
use Domain\Models\ValueObjects\Coupon\CouponValidity;
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponName;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\CouponName;
use Domain\Models\ValueObjects\GS1;
use Domain\Models\ValueObjects\IncoCode;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\Period;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
use Domain\Ports\Repositories\CampaignRequestRepository;
@ -40,29 +44,36 @@ use Shared\ValueObjects\Money;
covers(RejectCampaignRequest::class, Input::class, CampaignRequestRejected::class, CampaignRequestNotFound::class);
describe('reject campaign request use case', function (): void {
beforeEach(function (): void {
$this->requestedCoupon = new RequestedCoupon(
new CouponName('Coupon A'),
new RequestedCouponQuantity(100),
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
new MoneyAmount(new Money(Currency::EUR, 300)),
new ProcessingCode('PC1'),
new CouponValidity(30, new DateTime('2026-02-01')),
[
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
],
[new IncoCode(35784), new IncoCode(15522)],
new PrintRules(maxPerCustomer: 3),
new GS1('01234567890128', BarcodeFormat::EAN13)
);
});
$requestedCoupon = new RequestedCoupon(
couponName: new CouponName('Coupon A'),
quantity: new RequestedCouponQuantity(100),
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
couponPicture: new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobileThumbnail.jpg')
),
couponProcessingCodes: new CouponProcessingCodeArray([
new CouponProcessingCode('ABC1234567890', Country::FRANCE),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponLegalInformations: new CouponLegalInformationArray([
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]),
printRules: new PrintRules(maxPerCustomer: 3),
);
afterEach(function (): void {
CarbonImmutable::setTestNow();
});
it('rejects a campaign request and persists it', function (): void {
it('rejects a campaign request and persists it', function () use ($requestedCoupon): void {
// GIVEN
CarbonImmutable::setTestNow('2026-06-15 12:00:00 UTC');
$id = CampaignRequestId::generate();
@ -72,7 +83,7 @@ describe('reject campaign request use case', function (): void {
new CampaignName('Campaign A'),
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
CampaignRequestType::CLUB,
$this->requestedCoupon,
$requestedCoupon,
CampaignRequestStatus::CREATED,
);
$reason = new RejectionReason('Missing legal documents.');
@ -82,7 +93,7 @@ describe('reject campaign request use case', function (): void {
$repository->shouldReceive('save')->once()->with(\Mockery::on(
fn (CampaignRequest $saved): bool => $saved->status === CampaignRequestStatus::REJECTED
&& $saved->rejectionReason === $reason
&& $saved->updatedAt !== null,
&& $saved->updatedAt instanceof \DateTimeInterface,
));
$useCase = new RejectCampaignRequest($repository);
@ -115,7 +126,7 @@ describe('reject campaign request use case', function (): void {
expect($result)->toBeInstanceOf(CampaignRequestNotFound::class);
});
it('propagates the domain exception when the campaign request cannot be rejected', function (): void {
it('propagates the domain exception when the campaign request cannot be rejected', function () use ($requestedCoupon): void {
// GIVEN
$id = CampaignRequestId::generate();
$campaignRequest = CampaignRequest::hydrate(
@ -124,7 +135,7 @@ describe('reject campaign request use case', function (): void {
new CampaignName('Campaign A'),
new Applicant('John', 'Doe', new Email('john.doe@example.com')),
CampaignRequestType::CLUB,
$this->requestedCoupon,
$requestedCoupon,
CampaignRequestStatus::TRANSFORMED,
);
@ -142,19 +153,4 @@ describe('reject campaign request use case', function (): void {
// THEN
expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.');
});
it('never touches the repository when a rejection reason is missing', function (): void {
// GIVEN
$repository = \Mockery::mock(CampaignRequestRepository::class);
$repository->shouldNotReceive('findById');
$repository->shouldNotReceive('save');
$useCase = new RejectCampaignRequest($repository);
// WHEN
$action = fn (): Output => $useCase->execute(new Input(id: CampaignRequestId::generate()));
// THEN
expect($action)->toThrow(\ArgumentCountError::class);
});
});

View file

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Shared\Enums;
use Domain\Enums\BarcodeFormat;
covers(BarcodeFormat::class);
describe('barcode format', function (): void {
it('has the expected cases with their backing values', function (): void {
// THEN
expect(BarcodeFormat::EAN13->value)->toBe('ean')
->and(BarcodeFormat::CODE128->value)->toBe('code128')
->and(BarcodeFormat::GS1_128->value)->toBe('gs1');
});
it('exposes its case name', function (): void {
// THEN
expect(BarcodeFormat::EAN13->name)->toBe('EAN13');
});
});

View file

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Exceptions;
use Domain\Exceptions\BrandCodeAlreadyExistsException;
use Domain\Models\ValueObjects\Brand\BrandCode;
covers(BrandCodeAlreadyExistsException::class);
describe('brand code already exists exception', function (): void {
it('get the correct message', function (): void {
// GIVEN
$code = new BrandCode('AAA01');
// WHEN
$exception = new BrandCodeAlreadyExistsException($code);
// THEN
expect($exception->getMessage())->toBe('Brand code "AAA01" already exists.')
->and($exception)->toBeInstanceOf(\DomainException::class);
});
});

View file

@ -12,36 +12,34 @@ use Ramsey\Uuid\Uuid;
covers(Actor::class);
describe('actor', function (): void {
beforeEach(function (): void {
$this->displayName = new ActorDisplayName('John Doe');
});
$displayName = new ActorDisplayName('John Doe');
it('hydrates an actor', function (): void {
it('hydrates an actor', function () use ($displayName): void {
// GIVEN
$id = ActorId::generate();
// WHEN
$actor = Actor::hydrate(
$id,
$this->displayName
$displayName,
);
// THEN
expect($actor->id())->toBe($id)
->and($actor->displayName)->toBe($this->displayName)
->and($actor->displayName)->toBe($displayName)
->and($actor->updatedAt)->toBeNull()
->and($actor->updatedAt())->toBeNull();
});
it('hydrates an actor with an explicit updatedAt date', function (): void {
it('hydrates an actor with an explicit updatedAt date', function () use ($displayName): void {
// GIVEN
$updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC'));
// WHEN
$actor = Actor::hydrate(
ActorId::generate(),
$this->displayName,
$updatedAt
$displayName,
$updatedAt,
);
// THEN
@ -49,13 +47,13 @@ describe('actor', function (): void {
->and($actor->updatedAt())->toBe($updatedAt);
});
it('returns the creation date extracted from the id', function (): void {
it('returns the creation date extracted from the id', function () use ($displayName): void {
// GIVEN
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
$id = new ActorId(Uuid::uuid7($expected));
$actor = Actor::hydrate(
$id,
$this->displayName
$displayName,
);
// WHEN

View file

@ -17,44 +17,42 @@ use Shared\ValueObjects\Rcs;
covers(Brand::class);
describe('brand', function (): void {
beforeEach(function (): void {
$this->code = new BrandCode('AAA01');
$this->name = new BrandName('Name');
$this->logo = new PicturePath('files/picture.jpg');
$this->rcs = new Rcs('RCS PARIS 111 222 333');
$this->contact = new Email('contact@example.com');
});
$code = new BrandCode('AAA01');
$name = new BrandName('Name');
$logo = new PicturePath('files/picture.jpg');
$rcs = new Rcs('RCS PARIS 111 222 333');
$contact = new Email('contact@example.com');
afterEach(function (): void {
CarbonImmutable::setTestNow();
});
it('creates a brand', function (): void {
it('creates a brand', function () use ($code, $name, $logo, $rcs, $contact): void {
// GIVEN
$id = BrandId::generate();
// WHEN
$brand = Brand::hydrate(
$id,
$this->code,
$this->name,
$this->logo,
$this->rcs,
$this->contact
$code,
$name,
$logo,
$rcs,
$contact,
);
// THEN
expect($brand->id())->toBe($id)
->and($brand->code)->toBe($this->code)
->and($brand->name)->toBe($this->name)
->and($brand->logo)->toBe($this->logo)
->and($brand->rcs)->toBe($this->rcs)
->and($brand->contact)->toBe($this->contact)
->and($brand->code)->toBe($code)
->and($brand->name)->toBe($name)
->and($brand->logo)->toBe($logo)
->and($brand->rcs)->toBe($rcs)
->and($brand->contact)->toBe($contact)
->and($brand->updatedAt)->toBeNull()
->and($brand->updatedAt())->toBeNull();
});
it('creates a brand with an explicit updatedAt date', function (): void {
it('creates a brand with an explicit updatedAt date', function () use ($code, $name, $logo, $rcs, $contact): void {
// GIVEN
$id = BrandId::generate();
$updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC'));
@ -62,12 +60,12 @@ describe('brand', function (): void {
// WHEN
$brand = Brand::hydrate(
$id,
$this->code,
$this->name,
$this->logo,
$this->rcs,
$this->contact,
$updatedAt
$code,
$name,
$logo,
$rcs,
$contact,
$updatedAt,
);
// THEN
@ -75,49 +73,49 @@ describe('brand', function (): void {
->and($brand->updatedAt())->toBe($updatedAt);
});
it('creates a new brand with a freshly generated id', function (): void {
it('creates a new brand with a freshly generated id', function () use ($code, $name, $logo, $rcs, $contact): void {
// GIVEN / WHEN
$brand = Brand::create(
$this->code,
$this->name,
$this->logo,
$this->rcs,
$this->contact
$code,
$name,
$logo,
$rcs,
$contact,
);
// THEN
expect($brand->id())->toBeInstanceOf(BrandId::class)
->and($brand->code)->toBe($this->code)
->and($brand->name)->toBe($this->name)
->and($brand->logo)->toBe($this->logo)
->and($brand->rcs)->toBe($this->rcs)
->and($brand->contact)->toBe($this->contact)
->and($brand->code)->toBe($code)
->and($brand->name)->toBe($name)
->and($brand->logo)->toBe($logo)
->and($brand->rcs)->toBe($rcs)
->and($brand->contact)->toBe($contact)
->and($brand->updatedAt)->toBeNull()
->and($brand->updatedAt())->toBeNull();
});
it('generates a different id for each created brand', function (): void {
it('generates a different id for each created brand', function () use ($code, $name, $logo, $rcs, $contact): void {
// GIVEN / WHEN
$first = Brand::create(
$this->code,
$this->name,
$this->logo,
$this->rcs,
$this->contact
$code,
$name,
$logo,
$rcs,
$contact,
);
$second = Brand::create(
$this->code,
$this->name,
$this->logo,
$this->rcs,
$this->contact
$code,
$name,
$logo,
$rcs,
$contact,
);
// THEN
expect($first->id()->equals($second->id()))->toBeFalse();
});
it('updates a brand logo and sets updatedAt timestamp', function (): void {
it('updates a brand logo and sets updatedAt timestamp', function () use ($code, $name, $rcs, $contact): void {
// GIVEN
CarbonImmutable::setTestNow('2026-06-15 12:00:00 UTC');
$id = BrandId::generate();
@ -125,11 +123,11 @@ describe('brand', function (): void {
$logoNew = new PicturePath('files/picture-new.jpg');
$brand = Brand::hydrate(
$id,
$this->code,
$this->name,
$code,
$name,
$logoOld,
$this->rcs,
$this->contact
$rcs,
$contact,
);
// WHEN
@ -137,16 +135,16 @@ describe('brand', function (): void {
// THEN
expect($brand->id())->toBe($id)
->and($brand->code)->toBe($this->code)
->and($brand->name)->toBe($this->name)
->and($brand->code)->toBe($code)
->and($brand->name)->toBe($name)
->and($brand->logo)->toBe($logoNew)
->and($brand->rcs)->toBe($this->rcs)
->and($brand->contact)->toBe($this->contact)
->and($brand->rcs)->toBe($rcs)
->and($brand->contact)->toBe($contact)
->and($brand->updatedAt)->not->toBeNull()
->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00');
});
it('updates a brand rcs and sets updatedAt timestamp', function (): void {
it('updates a brand rcs and sets updatedAt timestamp', function () use ($code, $name, $logo, $contact): void {
// GIVEN
CarbonImmutable::setTestNow('2026-06-15 14:30:00 UTC');
$id = BrandId::generate();
@ -154,11 +152,11 @@ describe('brand', function (): void {
$rcsNew = new Rcs('RCS NANTES 444 555 666');
$brand = Brand::hydrate(
$id,
$this->code,
$this->name,
$this->logo,
$code,
$name,
$logo,
$rcsOld,
$this->contact
$contact,
);
// WHEN
@ -166,16 +164,16 @@ describe('brand', function (): void {
// THEN
expect($brand->id())->toBe($id)
->and($brand->code)->toBe($this->code)
->and($brand->name)->toBe($this->name)
->and($brand->logo)->toBe($this->logo)
->and($brand->code)->toBe($code)
->and($brand->name)->toBe($name)
->and($brand->logo)->toBe($logo)
->and($brand->rcs)->toBe($rcsNew)
->and($brand->contact)->toBe($this->contact)
->and($brand->contact)->toBe($contact)
->and($brand->updatedAt)->not->toBeNull()
->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 14:30:00');
});
it('updates a brand contact and sets updatedAt timestamp', function (): void {
it('updates a brand contact and sets updatedAt timestamp', function () use ($code, $name, $logo, $rcs): void {
// GIVEN
CarbonImmutable::setTestNow('2026-06-15 16:45:00 UTC');
$id = BrandId::generate();
@ -183,11 +181,11 @@ describe('brand', function (): void {
$contactNew = new Email('contact-new@example.com');
$brand = Brand::hydrate(
$id,
$this->code,
$this->name,
$this->logo,
$this->rcs,
$contactOld
$code,
$name,
$logo,
$rcs,
$contactOld,
);
// WHEN
@ -195,26 +193,26 @@ describe('brand', function (): void {
// THEN
expect($brand->id())->toBe($id)
->and($brand->code)->toBe($this->code)
->and($brand->name)->toBe($this->name)
->and($brand->logo)->toBe($this->logo)
->and($brand->rcs)->toBe($this->rcs)
->and($brand->code)->toBe($code)
->and($brand->name)->toBe($name)
->and($brand->logo)->toBe($logo)
->and($brand->rcs)->toBe($rcs)
->and($brand->contact)->toBe($contactNew)
->and($brand->updatedAt)->not->toBeNull()
->and($brand->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 16:45:00');
});
it('returns the creation date extracted from the id', function (): void {
it('returns the creation date extracted from the id', function () use ($code, $name, $logo, $rcs, $contact): void {
// GIVEN
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
$id = new BrandId(Uuid::uuid7($expected));
$brand = Brand::hydrate(
$id,
$this->code,
$this->name,
$this->logo,
$this->rcs,
$this->contact
$code,
$name,
$logo,
$rcs,
$contact,
);
// WHEN

View file

@ -6,7 +6,6 @@ namespace Tests\Unit\Domain\Models\Entities;
use Carbon\CarbonImmutable;
use DateTime;
use Domain\Enums\BarcodeFormat;
use Domain\Models\Entities\CampaignRequest;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Campaign\CampaignName;
@ -15,15 +14,20 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestId;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestType;
use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
use Domain\Models\ValueObjects\Coupon\CouponValidity;
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponName;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\CouponName;
use Domain\Models\ValueObjects\GS1;
use Domain\Models\ValueObjects\IncoCode;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\Period;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
use Ramsey\Uuid\Uuid;
@ -35,77 +39,90 @@ use Shared\ValueObjects\Money;
covers(CampaignRequest::class);
describe('campaign request', function (): void {
beforeEach(function (): void {
$this->requestedCoupon = new RequestedCoupon(
new CouponName('Coupon A'),
new RequestedCouponQuantity(100),
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
new MoneyAmount(new Money(Currency::EUR, 300)),
new ProcessingCode('PC1'),
new CouponValidity(30, new DateTime('2026-02-01')),
[
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
],
[new IncoCode(35784), new IncoCode(15522)],
new PrintRules(maxPerCustomer: 3),
new GS1('01234567890128', BarcodeFormat::EAN13)
);
$this->code = new BrandCode('AAA01');
$this->name = new CampaignName('Campaign A');
$this->applicant = new Applicant('John', 'Doe', new Email('john.doe@example.com'));
$this->campaignType = CampaignRequestType::CLUB;
$this->requestedCoupon = $this->requestedCoupon;
$this->status = CampaignRequestStatus::CREATED;
$this->transformedStatus = CampaignRequestStatus::TRANSFORMED;
$this->rejectedStatus = CampaignRequestStatus::REJECTED;
$this->rejectionReason = new RejectionReason('Missing legal documents.');
});
$requestedCoupon = new RequestedCoupon(
couponName: new CouponName('Coupon A'),
quantity: new RequestedCouponQuantity(100),
diffusion: new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
moneyAmount: new MoneyAmount(new Money(Currency::EUR, 300)),
couponPicture: new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobileThumbnail.jpg')
),
couponProcessingCodes: new CouponProcessingCodeArray([
new CouponProcessingCode('ABC1234567890', Country::FRANCE),
]),
couponValidityInformations: new CouponValidityInformationArray([
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponLegalInformations: new CouponLegalInformationArray([
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX)
]),
couponProducts: new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]),
printRules: new PrintRules(maxPerCustomer: 3),
);
$code = new BrandCode('AAA01');
$name = new CampaignName('Campaign A');
$applicant = new Applicant('John', 'Doe', new Email('john.doe@example.com'));
$campaignType = CampaignRequestType::CLUB;
$status = CampaignRequestStatus::CREATED;
$transformedStatus = CampaignRequestStatus::TRANSFORMED;
$rejectedStatus = CampaignRequestStatus::REJECTED;
$rejectionReason = new RejectionReason('Missing legal documents.');
afterEach(function (): void {
CarbonImmutable::setTestNow();
});
it('hydrates a campaign request with all its fields', function (): void {
it('hydrates a campaign request with all its fields', function () use ($code, $name, $applicant, $campaignType, $requestedCoupon, $status): void {
// GIVEN
$id = CampaignRequestId::generate();
// WHEN
$campaignRequest = CampaignRequest::hydrate(
$id,
$this->code,
$this->name,
$this->applicant,
$this->campaignType,
$this->requestedCoupon,
$this->status,
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$status,
);
// THEN
expect($campaignRequest->id())->toBe($id)
->and($campaignRequest->brandCode)->toBe($this->code)
->and($campaignRequest->name)->toBe($this->name)
->and($campaignRequest->applicant)->toBe($this->applicant)
->and($campaignRequest->campaignType)->toBe($this->campaignType)
->and($campaignRequest->requestedCoupon)->toBe($this->requestedCoupon)
->and($campaignRequest->status)->toBe($this->status)
->and($campaignRequest->brandCode)->toBe($code)
->and($campaignRequest->name)->toBe($name)
->and($campaignRequest->applicant)->toBe($applicant)
->and($campaignRequest->campaignType)->toBe($campaignType)
->and($campaignRequest->requestedCoupon)->toBe($requestedCoupon)
->and($campaignRequest->status)->toBe($status)
->and($campaignRequest->updatedAt)->toBeNull()
->and($campaignRequest->updatedAt())->toBeNull();
});
it('hydrates a campaign request with an explicit updatedAt date', function (): void {
it('hydrates a campaign request with an explicit updatedAt date', function () use (
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$status,
): void {
// GIVEN
$updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC'));
// WHEN
$campaignRequest = CampaignRequest::hydrate(
CampaignRequestId::generate(),
$this->code,
$this->name,
$this->applicant,
$this->campaignType,
$this->requestedCoupon,
$this->status,
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$status,
null,
$updatedAt,
);
@ -115,34 +132,49 @@ describe('campaign request', function (): void {
->and($campaignRequest->updatedAt())->toBe($updatedAt);
});
it('hydrates a rejected campaign request with a rejection reason', function (): void {
it('hydrates a rejected campaign request with a rejection reason', function () use (
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$rejectedStatus,
$rejectionReason,
): void {
// GIVEN / WHEN
$campaignRequest = CampaignRequest::hydrate(
CampaignRequestId::generate(),
$this->code,
$this->name,
$this->applicant,
$this->campaignType,
$this->requestedCoupon,
$this->rejectedStatus,
$this->rejectionReason,
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$rejectedStatus,
$rejectionReason,
);
// THEN
expect($campaignRequest->status)->toBe($this->rejectedStatus)
->and($campaignRequest->rejectionReason)->toBe($this->rejectionReason);
expect($campaignRequest->status)->toBe($rejectedStatus)
->and($campaignRequest->rejectionReason)->toBe($rejectionReason);
});
it('rejects a rejected campaign request without a rejection reason', function (): void {
it('rejects a rejected campaign request without a rejection reason', function () use (
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$rejectedStatus,
): void {
// GIVEN / WHEN
$action = fn (): CampaignRequest => CampaignRequest::hydrate(
CampaignRequestId::generate(),
$this->code,
$this->name,
$this->applicant,
$this->campaignType,
$this->requestedCoupon,
$this->rejectedStatus,
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$rejectedStatus,
);
// THEN
@ -152,17 +184,25 @@ describe('campaign request', function (): void {
);
});
it('rejects a non-rejected campaign request with a rejection reason', function (): void {
it('rejects a non-rejected campaign request with a rejection reason', function () use (
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$status,
$rejectionReason,
): void {
// WHEN
$action = fn (): CampaignRequest => CampaignRequest::hydrate(
CampaignRequestId::generate(),
$this->code,
$this->name,
$this->applicant,
$this->campaignType,
$this->requestedCoupon,
$this->status,
$this->rejectionReason,
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$status,
$rejectionReason,
);
// THEN
@ -172,25 +212,33 @@ describe('campaign request', function (): void {
);
});
it('rejects a campaign request and sets the rejection reason and updatedAt', function (): void {
it('rejects a campaign request and sets the rejection reason and updatedAt', function () use (
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$status,
$rejectionReason,
): void {
// GIVEN
CarbonImmutable::setTestNow('2026-06-15 12:00:00 UTC');
$campaignRequest = CampaignRequest::hydrate(
CampaignRequestId::generate(),
$this->code,
$this->name,
$this->applicant,
$this->campaignType,
$this->requestedCoupon,
$this->status,
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$status,
);
// WHEN
$rejected = $campaignRequest->reject($this->rejectionReason);
$rejected = $campaignRequest->reject($rejectionReason);
// THEN
expect($rejected->status)->toBe(CampaignRequestStatus::REJECTED)
->and($rejected->rejectionReason)->toBe($this->rejectionReason)
->and($rejected->rejectionReason)->toBe($rejectionReason)
->and($rejected->id())->toBe($campaignRequest->id())
->and($rejected->brandCode)->toBe($campaignRequest->brandCode)
->and($rejected->name)->toBe($campaignRequest->name)
@ -201,35 +249,50 @@ describe('campaign request', function (): void {
->and($rejected->updatedAt()?->format('Y-m-d H:i:s'))->toBe('2026-06-15 12:00:00');
});
it('rejects rejecting a transformed campaign request', function (): void {
it('rejects rejecting a transformed campaign request', function () use (
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$transformedStatus,
$rejectionReason,
): void {
// GIVEN
$campaignRequest = CampaignRequest::hydrate(
CampaignRequestId::generate(),
$this->code,
$this->name,
$this->applicant,
$this->campaignType,
$this->requestedCoupon,
$this->transformedStatus,
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$transformedStatus,
);
// WHEN
$action = fn (): CampaignRequest => $campaignRequest->reject($this->rejectionReason);
$action = fn (): CampaignRequest => $campaignRequest->reject($rejectionReason);
// THEN
expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.');
});
it('rejects rejecting an already rejected campaign request', function (): void {
it('rejects rejecting an already rejected campaign request', function () use (
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$rejectedStatus,
): void {
// GIVEN
$campaignRequest = CampaignRequest::hydrate(
CampaignRequestId::generate(),
$this->code,
$this->name,
$this->applicant,
$this->campaignType,
$this->requestedCoupon,
$this->rejectedStatus,
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$rejectedStatus,
new RejectionReason('First reason.'),
);
@ -240,18 +303,18 @@ describe('campaign request', function (): void {
expect($action)->toThrow(\DomainException::class, 'Only a created campaign request can be rejected.');
});
it('returns the creation date extracted from the id', function (): void {
it('returns the creation date extracted from the id', function () use ($code, $name, $applicant, $campaignType, $requestedCoupon, $status): void {
// GIVEN
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
$id = new CampaignRequestId(Uuid::uuid7($expected));
$campaignRequest = CampaignRequest::hydrate(
$id,
$this->code,
$this->name,
$this->applicant,
$this->campaignType,
$this->requestedCoupon,
$this->status,
$code,
$name,
$applicant,
$campaignType,
$requestedCoupon,
$status,
);
// WHEN

View file

@ -5,92 +5,83 @@ declare(strict_types=1);
namespace Tests\Unit\Domain\Models\Entities;
use Carbon\CarbonImmutable;
use Shared\Enums\Country;
use Domain\Models\Entities\Campaign;
use Domain\Models\ValueObjects\Actor\ActorId;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Campaign\CampaignCode;
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
use Domain\Models\ValueObjects\Campaign\CampaignCouponArray;
use Domain\Models\ValueObjects\Campaign\CampaignId;
use Domain\Models\ValueObjects\Campaign\CampaignName;
use Domain\Models\ValueObjects\Coupon\CouponQuantity;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Ramsey\Uuid\Uuid;
covers(Campaign::class);
describe('campaign', function (): void {
beforeEach(function (): void {
$this->code = new CampaignCode('Camp100');
$this->name = new CampaignName('Name');
$this->publication = \boolval(0);
$this->printAllAtOnce = \boolval(1);
$this->coupons = [
new CampaignCoupon(
new ProcessingCode('PC1'),
new CouponQuantity(100),
Country::FRANCE,
new PrintRules(maxPerCustomer: 3)
)
];
$this->brandCode = new BrandCode('AAA01');
$this->actorId = ActorId::generate();
});
$code = new CampaignCode('Camp100');
$name = new CampaignName('Name');
$publication = \boolval(0);
$printAllAtOnce = \boolval(1);
$coupons = new CampaignCouponArray([
new CampaignCoupon(
new CouponCode('AAA01'),
new PrintRules(maxPerCustomer: 3)
)
]);
$brandCode = new BrandCode('AAA01');
$actorId = ActorId::generate();
afterEach(function (): void {
CarbonImmutable::setTestNow();
});
it('creates a campaign', function (): void {
it('creates a campaign', function () use (
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId,
): void {
// GIVEN
$id = CampaignId::generate();
// WHEN
$campaign = Campaign::hydrate(
$id,
$this->code,
$this->name,
$this->publication,
$this->printAllAtOnce,
$this->coupons,
$this->brandCode,
$this->actorId
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId
);
// THEN
expect($campaign->id())->toBe($id)
->and($campaign->code)->toBe($this->code)
->and($campaign->name)->toBe($this->name)
->and($campaign->publication)->toBe($this->publication)
->and($campaign->printAllAtOnce)->toBe($this->printAllAtOnce)
->and($campaign->coupons)->toBe($this->coupons)
->and($campaign->brandCode)->toBe($this->brandCode)
->and($campaign->code)->toBe($code)
->and($campaign->name)->toBe($name)
->and($campaign->publication)->toBe($publication)
->and($campaign->printAllAtOnce)->toBe($printAllAtOnce)
->and($campaign->coupons)->toBe($coupons)
->and($campaign->brandCode)->toBe($brandCode)
->and($campaign->updatedAt)->toBeNull()
->and($campaign->updatedAt())->toBeNull();
});
it('creates a campaign without coupon', function (): void {
// GIVEN
$id = CampaignId::generate();
// WHEN
$campaign = Campaign::hydrate(
$id,
$this->code,
$this->name,
$this->publication,
$this->printAllAtOnce,
[],
$this->brandCode,
$this->actorId
);
// THEN
expect($campaign->coupons)->toBe([]);
});
it('creates a coupon with an explicit updatedAt date', function (): void {
it('creates a coupon with an explicit updatedAt date', function () use (
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId,
): void {
// GIVEN
$id = CampaignId::generate();
$updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC'));
@ -98,13 +89,13 @@ describe('campaign', function (): void {
// WHEN
$campaign = Campaign::hydrate(
$id,
$this->code,
$this->name,
$this->publication,
$this->printAllAtOnce,
$this->coupons,
$this->brandCode,
$this->actorId,
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId,
$updatedAt
);
@ -113,68 +104,92 @@ describe('campaign', function (): void {
->and($campaign->updatedAt())->toBe($updatedAt);
});
it('creates a new campaign with a freshly generated id', function (): void {
it('creates a new campaign with a freshly generated id', function () use (
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId,
): void {
// GIVEN / WHEN
$campaign = Campaign::create(
$this->code,
$this->name,
$this->publication,
$this->printAllAtOnce,
$this->coupons,
$this->brandCode,
$this->actorId
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId
);
// THEN
expect($campaign->id())->toBeInstanceOf(CampaignId::class)
->and($campaign->code)->toBe($this->code)
->and($campaign->name)->toBe($this->name)
->and($campaign->publication)->toBe($this->publication)
->and($campaign->printAllAtOnce)->toBe($this->printAllAtOnce)
->and($campaign->coupons)->toBe($this->coupons)
->and($campaign->brandCode)->toBe($this->brandCode)
->and($campaign->code)->toBe($code)
->and($campaign->name)->toBe($name)
->and($campaign->publication)->toBe($publication)
->and($campaign->printAllAtOnce)->toBe($printAllAtOnce)
->and($campaign->coupons)->toBe($coupons)
->and($campaign->brandCode)->toBe($brandCode)
->and($campaign->updatedAt)->toBeNull()
->and($campaign->updatedAt())->toBeNull();
});
it('generates a different id for each created campaign', function (): void {
it('generates a different id for each created campaign', function () use (
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId,
): void {
// GIVEN / WHEN
$firstCampaign = Campaign::create(
$this->code,
$this->name,
$this->publication,
$this->printAllAtOnce,
$this->coupons,
$this->brandCode,
$this->actorId
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId
);
$secondCampaign = Campaign::create(
$this->code,
$this->name,
$this->publication,
$this->printAllAtOnce,
$this->coupons,
$this->brandCode,
$this->actorId
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId
);
// THEN
expect($firstCampaign->id()->equals($secondCampaign->id()))->toBeFalse();
});
it('returns the creation date extracted from the id', function (): void {
it('returns the creation date extracted from the id', function () use (
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId,
): void {
// GIVEN
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
$id = new CampaignId(Uuid::uuid7($expected));
$campaign = Campaign::hydrate(
$id,
$this->code,
$this->name,
$this->publication,
$this->printAllAtOnce,
$this->coupons,
$this->brandCode,
$this->actorId
$code,
$name,
$publication,
$printAllAtOnce,
$coupons,
$brandCode,
$actorId
);
// WHEN
@ -184,21 +199,20 @@ describe('campaign', function (): void {
expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v'));
});
it('rejects a campaign with a non-coupon element', function (): void {
it('rejects a campaign with a non-coupon element', function () use ($code, $name, $publication, $printAllAtOnce, $brandCode, $actorId): void {
// GIVEN
$id = CampaignId::generate();
$coupons = ['PC1'];
$id = CampaignId::generate();
// WHEN / THEN
expect(fn (): Campaign => Campaign::hydrate(
$id,
$this->code,
$this->name,
$this->publication,
$this->printAllAtOnce,
$coupons,
$this->brandCode,
$this->actorId
))->toThrow(\DomainException::class, 'Campaign coupons must be CampaignCoupon instances.');
$code,
$name,
$publication,
$printAllAtOnce,
new CampaignCouponArray(['PC1']),
$brandCode,
$actorId
))->toThrow(\DomainException::class, 'Coupons must be CampaignCoupon instances.');
});
});

View file

@ -6,7 +6,6 @@ namespace Tests\Unit\Domain\Models\Entities;
use Carbon\CarbonImmutable;
use DateTime;
use Domain\Enums\BarcodeFormat;
use Shared\Enums\Country;
use Domain\Models\Entities\Coupon;
use Domain\Models\ValueObjects\Actor\ActorId;
@ -15,17 +14,22 @@ use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
use Domain\Models\ValueObjects\Coupon\CouponId;
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponValidity;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponMovement;
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
use Domain\Models\ValueObjects\Coupon\CouponName;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
use Domain\Models\ValueObjects\Coupon\CouponQuantity;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\CouponName;
use Domain\Models\ValueObjects\CouponPicture;
use Domain\Models\ValueObjects\GS1;
use Domain\Models\ValueObjects\IncoCode;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\Period;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\ProcessingCode;
use Ramsey\Uuid\Uuid;
use Shared\Enums\Currency;
use Shared\ValueObjects\Money;
@ -33,274 +37,277 @@ use Shared\ValueObjects\Money;
covers(Coupon::class);
describe('coupon', function (): void {
beforeEach(function (): void {
$this->code = new CouponCode('COU01');
$this->name = new CouponName('Name');
$this->amount = new MoneyAmount(new Money(Currency::EUR, 1));
$this->picture = new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/inactive_thumbnail.jpg')
);
$this->diffusion = new CouponDiffusion(new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')));
$this->validity = new CouponValidity(30, new DateTime('2026-02-01'));
$this->couponValidityInformation = [
new CouponValidityInformation(
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
Country::FRANCE
)
];
$this->couponLegalInformation = [
new CouponLegalInformation(
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
Country::FRANCE
)
];
$this->processingCode = new ProcessingCode('5425003944130');
$this->brandCode = new BrandCode('AAA01');
$this->createdBy = ActorId::generate();
$this->products = [new IncoCode(35784), new IncoCode(15522)];
$this->availabilityCalendar = new CouponAvailabilityCalendar([new Period(new DateTime('2026-08-25'), new DateTime('2026-10-31'))]);
$this->gs1 = new GS1('110101010', BarcodeFormat::EAN13);
});
$code = new CouponCode('AAA01');
$name = new CouponName('Name');
$amount = new MoneyAmount(new Money(Currency::EUR, 1));
$picture = new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobileThumbnail.jpg'),
);
$diffusion = new CouponDiffusion(new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')));
$validityInformations = new CouponValidityInformationArray([
new CouponValidityInformation(
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
Country::FRANCE,
)
]);
$legalInformations = new CouponLegalInformationArray([
new CouponLegalInformation(
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
Country::FRANCE,
)
]);
$processingCode = new CouponProcessingCodeArray([
new CouponProcessingCode('5425003944130', Country::FRANCE),
]);
$brandCode = new BrandCode('AAA01');
$createdBy = ActorId::generate();
$products = new CouponProductArray([new CouponIncoCode(35784), new CouponIncoCode(15522)]);
$movements = new CouponMovementArray([new CouponMovement(100)]);
$quantity = new CouponQuantity(100);
$availabilityCalendar = new CouponAvailabilityCalendar([
new Period(
new DateTime('2026-08-25'),
new DateTime('2026-10-31'),
)]);
afterEach(function (): void {
CarbonImmutable::setTestNow();
});
it('creates a coupon', function (): void {
it('creates a coupon', function () use (
$code,
$name,
$amount,
$picture,
$diffusion,
$validityInformations,
$legalInformations,
$processingCode,
$brandCode,
$createdBy,
$products,
$movements,
$quantity,
$availabilityCalendar,
): void {
// GIVEN
$id = CouponId::generate();
// WHEN
$coupon = Coupon::hydrate(
$id,
$this->code,
$this->name,
$this->amount,
$this->picture,
$this->diffusion,
$this->validity,
$this->couponValidityInformation,
$this->couponLegalInformation,
$this->processingCode,
$this->brandCode,
$this->createdBy,
$this->products,
$this->availabilityCalendar,
$this->gs1,
id: $id,
code: $code,
name: $name,
amount: $amount,
picture: $picture,
diffusion: $diffusion,
validityInformations: $validityInformations,
legalInformations: $legalInformations,
processingCode: $processingCode,
brandCode: $brandCode,
createdBy: $createdBy,
products: $products,
movements: $movements,
quantity: $quantity,
availabilityCalendar: $availabilityCalendar,
);
// THEN
expect($coupon->id())->toBe($id)
->and($coupon->code)->toBe($this->code)
->and($coupon->name)->toBe($this->name)
->and($coupon->amount)->toBe($this->amount)
->and($coupon->picture)->toBe($this->picture)
->and($coupon->diffusion)->toBe($this->diffusion)
->and($coupon->validity)->toBe($this->validity)
->and($coupon->couponValidityInformation)->toBe($this->couponValidityInformation)
->and($coupon->couponLegalInformation)->toBe($this->couponLegalInformation)
->and($coupon->processingCode)->toBe($this->processingCode)
->and($coupon->brandCode)->toBe($this->brandCode)
->and($coupon->createdBy)->toBe($this->createdBy)
->and($coupon->products)->toBe($this->products)
->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar)
->and($coupon->gs1)->toBe($this->gs1)
->and($coupon->code)->toBe($code)
->and($coupon->name)->toBe($name)
->and($coupon->amount)->toBe($amount)
->and($coupon->picture)->toBe($picture)
->and($coupon->diffusion)->toBe($diffusion)
->and($coupon->validityInformations)->toBe($validityInformations)
->and($coupon->legalInformations)->toBe($legalInformations)
->and($coupon->processingCode)->toBe($processingCode)
->and($coupon->brandCode)->toBe($brandCode)
->and($coupon->createdBy)->toBe($createdBy)
->and($coupon->products)->toBe($products)
->and($coupon->availabilityCalendar)->toBe($availabilityCalendar)
->and($coupon->movements)->toBe($movements)
->and($coupon->updatedAt)->toBeNull()
->and($coupon->updatedAt())->toBeNull();
});
it('hydrates a coupon without gs1', function (): void {
it('hydrates a coupon without availability calendar', function () use (
$code,
$name,
$amount,
$picture,
$diffusion,
$validityInformations,
$legalInformations,
$processingCode,
$brandCode,
$createdBy,
$products,
$movements,
$quantity,
): void {
// GIVEN
$id = CouponId::generate();
// WHEN
$coupon = Coupon::hydrate(
id: $id,
code: $this->code,
name: $this->name,
amount: $this->amount,
picture: $this->picture,
diffusion: $this->diffusion,
validity: $this->validity,
couponValidityInformation: $this->couponValidityInformation,
couponLegalInformation: $this->couponLegalInformation,
processingCode: $this->processingCode,
brandCode: $this->brandCode,
createdBy: $this->createdBy,
products: $this->products,
availabilityCalendar: $this->availabilityCalendar
);
// THEN
expect($coupon->products)->toBe($this->products)
->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar)
->and($coupon->gs1)->toBeNull();
});
it('hydrates a coupon without availability calendar', function (): void {
// GIVEN
$id = CouponId::generate();
// WHEN
$coupon = Coupon::hydrate(
id: $id,
code: $this->code,
name: $this->name,
amount: $this->amount,
picture: $this->picture,
diffusion: $this->diffusion,
validity: $this->validity,
couponValidityInformation: $this->couponValidityInformation,
couponLegalInformation: $this->couponLegalInformation,
processingCode: $this->processingCode,
brandCode: $this->brandCode,
createdBy: $this->createdBy,
products: $this->products,
gs1: $this->gs1
code: $code,
name: $name,
amount: $amount,
picture: $picture,
diffusion: $diffusion,
validityInformations: $validityInformations,
legalInformations: $legalInformations,
processingCode: $processingCode,
brandCode: $brandCode,
createdBy: $createdBy,
products: $products,
movements: $movements,
quantity: $quantity,
);
// THEN
expect($coupon->availabilityCalendar)->toBeNull()
->and($coupon->products)->toBe($this->products)
->and($coupon->gs1)->toBe($this->gs1);
->and($coupon->products)->toBe($products);
});
it('hydrates a coupon without products', function (): void {
it('hydrates a coupon without products', function () use (
$code,
$name,
$amount,
$picture,
$diffusion,
$validityInformations,
$legalInformations,
$processingCode,
$brandCode,
$createdBy,
$movements,
$quantity,
$availabilityCalendar,
): void {
// GIVEN
$id = CouponId::generate();
// WHEN
$coupon = Coupon::hydrate(
id: $id,
code: $this->code,
name: $this->name,
amount: $this->amount,
picture: $this->picture,
diffusion: $this->diffusion,
validity: $this->validity,
couponValidityInformation: $this->couponValidityInformation,
couponLegalInformation: $this->couponLegalInformation,
processingCode: $this->processingCode,
brandCode: $this->brandCode,
createdBy: $this->createdBy,
availabilityCalendar: $this->availabilityCalendar,
gs1: $this->gs1
code: $code,
name: $name,
amount: $amount,
picture: $picture,
diffusion: $diffusion,
validityInformations: $validityInformations,
legalInformations: $legalInformations,
processingCode: $processingCode,
brandCode: $brandCode,
createdBy: $createdBy,
products: new CouponProductArray(),
movements: $movements,
quantity: $quantity,
availabilityCalendar: $availabilityCalendar,
);
// THEN
expect($coupon->products)->toBe([])
->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar)
->and($coupon->gs1)->toBe($this->gs1);
expect($coupon->products->value)->toBe([])
->and($coupon->availabilityCalendar)->toBe($availabilityCalendar);
});
it('hydrates a coupon with only required fields', function (): void {
it('throws when validity information contains an invalid item', function () use (
$code,
$name,
$amount,
$picture,
$diffusion,
$legalInformations,
$processingCode,
$brandCode,
$createdBy,
$products,
$movements,
$quantity,
$availabilityCalendar,
): void {
// GIVEN
$id = CouponId::generate();
// WHEN
$coupon = Coupon::hydrate(
// WHEN / THEN
expect(fn (): Coupon => Coupon::hydrate(
id: $id,
code: $this->code,
name: $this->name,
amount: $this->amount,
picture: $this->picture,
diffusion: $this->diffusion,
validity: $this->validity,
couponValidityInformation: $this->couponValidityInformation,
couponLegalInformation: $this->couponLegalInformation,
processingCode: $this->processingCode,
brandCode: $this->brandCode,
createdBy: $this->createdBy,
);
// THEN
expect($coupon->id())->toBe($id)
->and($coupon->products)->toBe([])
->and($coupon->availabilityCalendar)->toBeNull()
->and($coupon->gs1)->toBeNull()
->and($coupon->updatedAt)->toBeNull()
->and($coupon->updatedAt())->toBeNull();
code: $code,
name: $name,
amount: $amount,
picture: $picture,
diffusion: $diffusion,
validityInformations: new CouponValidityInformationArray(['Not a CouponValidityInformation']),
legalInformations: $legalInformations,
processingCode: $processingCode,
brandCode: $brandCode,
createdBy: $createdBy,
products: $products,
movements: $movements,
quantity: $quantity,
availabilityCalendar: $availabilityCalendar,
))->toThrow(\DomainException::class, 'Value of CouponValidityInformationArray must be CouponValidityInformation instances.');
});
it('accepts empty validity and legal information lists', function (): void {
// GIVEN
$id = CouponId::generate();
// WHEN
$coupon = Coupon::hydrate(
$id,
$this->code,
$this->name,
$this->amount,
$this->picture,
$this->diffusion,
$this->validity,
[],
[],
$this->processingCode,
$this->brandCode,
$this->createdBy,
$this->products,
$this->availabilityCalendar,
$this->gs1,
);
// THEN
expect($coupon->couponValidityInformation)->toBe([])
->and($coupon->couponLegalInformation)->toBe([]);
});
it('throws when validity information contains an invalid item', function (): void {
it('throws when legal information contains an invalid item', function () use (
$code,
$name,
$amount,
$picture,
$diffusion,
$validityInformations,
$processingCode,
$brandCode,
$createdBy,
$products,
$movements,
$quantity,
$availabilityCalendar,
): void {
// GIVEN
$id = CouponId::generate();
// WHEN / THEN
expect(fn () => Coupon::hydrate(
$id,
$this->code,
$this->name,
$this->amount,
$this->picture,
$this->diffusion,
$this->validity,
['Not a CouponValidityInformation'],
$this->couponLegalInformation,
$this->processingCode,
$this->brandCode,
$this->createdBy,
$this->products,
$this->availabilityCalendar,
$this->gs1,
))->toThrow(\DomainException::class, 'Coupon validity information must be CouponValidityInformation instances.');
expect(fn (): Coupon => Coupon::hydrate(
id: $id,
code: $code,
name: $name,
amount: $amount,
picture: $picture,
diffusion: $diffusion,
validityInformations: $validityInformations,
legalInformations: new CouponLegalInformationArray(['Not a CouponLegalInformation']),
processingCode: $processingCode,
brandCode: $brandCode,
createdBy: $createdBy,
products: $products,
movements: $movements,
quantity: $quantity,
availabilityCalendar: $availabilityCalendar,
))->toThrow(\DomainException::class, 'Value of CouponLegalInformationArray must be CouponLegalInformation instances.');
});
it('throws when legal information contains an invalid item', function (): void {
// GIVEN
$id = CouponId::generate();
// WHEN / THEN
expect(fn () => Coupon::hydrate(
$id,
$this->code,
$this->name,
$this->amount,
$this->picture,
$this->diffusion,
$this->validity,
$this->couponValidityInformation,
['Not a CouponLegalInformation'],
$this->processingCode,
$this->brandCode,
$this->createdBy,
$this->products,
$this->availabilityCalendar,
$this->gs1,
))->toThrow(\DomainException::class, 'Coupon legal information must be CouponLegalInformation instances.');
});
it('hydrates a coupon with an explicit updatedAt date', function (): void {
it('hydrates a coupon with an explicit updatedAt date', function () use (
$code,
$name,
$amount,
$picture,
$diffusion,
$validityInformations,
$legalInformations,
$processingCode,
$brandCode,
$createdBy,
$products,
$movements,
$quantity,
$availabilityCalendar,
): void {
// GIVEN
$id = CouponId::generate();
$updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC'));
@ -308,21 +315,21 @@ describe('coupon', function (): void {
// WHEN
$coupon = Coupon::hydrate(
$id,
$this->code,
$this->name,
$this->amount,
$this->picture,
$this->diffusion,
$this->validity,
$this->couponValidityInformation,
$this->couponLegalInformation,
$this->processingCode,
$this->brandCode,
$this->createdBy,
$this->products,
$this->availabilityCalendar,
$this->gs1,
$updatedAt
code: $code,
name: $name,
amount: $amount,
picture: $picture,
diffusion: $diffusion,
validityInformations: $validityInformations,
legalInformations: $legalInformations,
processingCode: $processingCode,
brandCode: $brandCode,
createdBy: $createdBy,
products: $products,
movements: $movements,
quantity: $quantity,
availabilityCalendar: $availabilityCalendar,
updatedAt: $updatedAt,
);
// THEN
@ -330,104 +337,149 @@ describe('coupon', function (): void {
->and($coupon->updatedAt())->toBe($updatedAt);
});
it('creates a new coupon with a freshly generated id', function (): void {
it('creates a new coupon with a freshly generated id', function () use (
$code,
$name,
$amount,
$picture,
$diffusion,
$validityInformations,
$legalInformations,
$processingCode,
$brandCode,
$createdBy,
$products,
$movements,
$quantity,
$availabilityCalendar,
): void {
// GIVEN / WHEN
$coupon = Coupon::create(
$this->code,
$this->name,
$this->amount,
$this->picture,
$this->diffusion,
$this->validity,
$this->couponValidityInformation,
$this->couponLegalInformation,
$this->processingCode,
$this->brandCode,
$this->createdBy,
$this->products,
$this->availabilityCalendar,
$this->gs1,
code: $code,
name: $name,
amount: $amount,
picture: $picture,
diffusion: $diffusion,
validityInformations: $validityInformations,
legalInformations: $legalInformations,
processingCode: $processingCode,
brandCode: $brandCode,
createdBy: $createdBy,
products: $products,
movements: $movements,
quantity: $quantity,
availabilityCalendar: $availabilityCalendar,
);
// THEN
expect($coupon->id())->toBeInstanceOf(CouponId::class)
->and($coupon->code)->toBe($this->code)
->and($coupon->name)->toBe($this->name)
->and($coupon->amount)->toBe($this->amount)
->and($coupon->picture)->toBe($this->picture)
->and($coupon->diffusion)->toBe($this->diffusion)
->and($coupon->validity)->toBe($this->validity)
->and($coupon->couponValidityInformation)->toBe($this->couponValidityInformation)
->and($coupon->couponLegalInformation)->toBe($this->couponLegalInformation)
->and($coupon->processingCode)->toBe($this->processingCode)
->and($coupon->brandCode)->toBe($this->brandCode)
->and($coupon->createdBy)->toBe($this->createdBy)
->and($coupon->products)->toBe($this->products)
->and($coupon->availabilityCalendar)->toBe($this->availabilityCalendar)
->and($coupon->gs1)->toBe($this->gs1)
->and($coupon->code)->toBe($code)
->and($coupon->name)->toBe($name)
->and($coupon->amount)->toBe($amount)
->and($coupon->picture)->toBe($picture)
->and($coupon->diffusion)->toBe($diffusion)
->and($coupon->validityInformations)->toBe($validityInformations)
->and($coupon->legalInformations)->toBe($legalInformations)
->and($coupon->processingCode)->toBe($processingCode)
->and($coupon->brandCode)->toBe($brandCode)
->and($coupon->createdBy)->toBe($createdBy)
->and($coupon->products)->toBe($products)
->and($coupon->movements)->toBe($movements)
->and($coupon->quantity)->toBe($quantity)
->and($coupon->availabilityCalendar)->toBe($availabilityCalendar)
->and($coupon->updatedAt)->toBeNull()
->and($coupon->updatedAt())->toBeNull();
});
it('generates a different id for each created coupon', function (): void {
it('generates a different id for each created coupon', function () use (
$code,
$name,
$amount,
$picture,
$diffusion,
$validityInformations,
$legalInformations,
$processingCode,
$brandCode,
$createdBy,
$products,
$movements,
$quantity,
$availabilityCalendar,
): void {
// GIVEN / WHEN
$firstCoupon = Coupon::create(
$this->code,
$this->name,
$this->amount,
$this->picture,
$this->diffusion,
$this->validity,
$this->couponValidityInformation,
$this->couponLegalInformation,
$this->processingCode,
$this->brandCode,
$this->createdBy,
$this->products,
$this->availabilityCalendar,
$this->gs1,
code: $code,
name: $name,
amount: $amount,
picture: $picture,
diffusion: $diffusion,
validityInformations: $validityInformations,
legalInformations: $legalInformations,
processingCode: $processingCode,
brandCode: $brandCode,
createdBy: $createdBy,
products: $products,
movements: $movements,
quantity: $quantity,
availabilityCalendar: $availabilityCalendar,
);
$secondCoupon = Coupon::create(
$this->code,
$this->name,
$this->amount,
$this->picture,
$this->diffusion,
$this->validity,
$this->couponValidityInformation,
$this->couponLegalInformation,
$this->processingCode,
$this->brandCode,
$this->createdBy,
$this->products,
$this->availabilityCalendar,
$this->gs1,
code: $code,
name: $name,
amount: $amount,
picture: $picture,
diffusion: $diffusion,
validityInformations: $validityInformations,
legalInformations: $legalInformations,
processingCode: $processingCode,
brandCode: $brandCode,
createdBy: $createdBy,
products: $products,
movements: $movements,
quantity: $quantity,
availabilityCalendar: $availabilityCalendar,
);
// THEN
expect($firstCoupon->id()->equals($secondCoupon->id()))->toBeFalse();
});
it('returns the creation date extracted from the id', function (): void {
it('returns the creation date extracted from the id', function () use (
$code,
$name,
$amount,
$picture,
$diffusion,
$validityInformations,
$legalInformations,
$processingCode,
$brandCode,
$createdBy,
$products,
$movements,
$quantity,
$availabilityCalendar,
): void {
// GIVEN
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
$id = new CouponId(Uuid::uuid7($expected));
$coupon = Coupon::hydrate(
$id,
$this->code,
$this->name,
$this->amount,
$this->picture,
$this->diffusion,
$this->validity,
$this->couponValidityInformation,
$this->couponLegalInformation,
$this->processingCode,
$this->brandCode,
$this->createdBy,
$this->products,
$this->availabilityCalendar,
$this->gs1,
code: $code,
name: $name,
amount: $amount,
picture: $picture,
diffusion: $diffusion,
validityInformations: $validityInformations,
legalInformations: $legalInformations,
processingCode: $processingCode,
brandCode: $brandCode,
createdBy: $createdBy,
products: $products,
movements: $movements,
quantity: $quantity,
availabilityCalendar: $availabilityCalendar,
);
// WHEN

View file

@ -7,7 +7,7 @@ namespace Tests\Unit\Domain\Models\Entities;
use Domain\Models\Entities\Stock;
use Domain\Models\ValueObjects\Actor\ActorId;
use Domain\Models\ValueObjects\Campaign\CampaignCode;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\Stock\StockAdjustment;
use Domain\Models\ValueObjects\Stock\StockAmount;
use Domain\Models\ValueObjects\Stock\StockId;
@ -16,40 +16,38 @@ use Ramsey\Uuid\Uuid;
covers(Stock::class);
describe('stock', function (): void {
beforeEach(function (): void {
$this->stockAmount = new StockAmount(10);
$this->campaignCode = new CampaignCode('AAA01');
$this->processingCode = new ProcessingCode('PC1');
$this->stockAdjustment = [new StockAdjustment(5, 'Inventory correction', ActorId::generate())];
});
$stockAmount = new StockAmount(10);
$campaignCode = new CampaignCode('AAA01');
$couponCode = new CouponCode('AAA01');
$stockAdjustment = [new StockAdjustment(5, 'Inventory correction', ActorId::generate())];
it('hydrates a stock', function (): void {
it('hydrates a stock', function () use ($stockAmount, $campaignCode, $couponCode, $stockAdjustment): void {
// GIVEN
$id = StockId::generate();
// WHEN
$stock = Stock::hydrate(
$id,
$this->stockAmount,
$this->campaignCode,
$this->processingCode,
$this->stockAdjustment
$stockAmount,
$campaignCode,
$couponCode,
$stockAdjustment,
);
// THEN
expect($stock->id())->toBe($id)
->and($stock->stockAmount)->toBe($this->stockAmount)
->and($stock->campaignCode)->toBe($this->campaignCode)
->and($stock->processingCode)->toBe($this->processingCode)
->and($stock->stockAdjustment)->toBe($this->stockAdjustment)
->and($stock->stockAmount)->toBe($stockAmount)
->and($stock->campaignCode)->toBe($campaignCode)
->and($stock->couponCode)->toBe($couponCode)
->and($stock->stockAdjustment)->toBe($stockAdjustment)
->and($stock->updatedAt)->toBeNull()
->and($stock->updatedAt())->toBeNull();
});
it('hydrates a stock with several stock adjustments', function (): void {
it('hydrates a stock with several stock adjustments', function () use ($stockAmount, $campaignCode, $couponCode): void {
// GIVEN
$id = StockId::generate();
$stockAdjustment = [
$id = StockId::generate();
$severalAdjustments = [
new StockAdjustment(5, 'Inventory correction', ActorId::generate()),
new StockAdjustment(10, 'Inventory correction', ActorId::generate()),
];
@ -57,70 +55,70 @@ describe('stock', function (): void {
// WHEN
$stock = Stock::hydrate(
$id,
$this->stockAmount,
$this->campaignCode,
$this->processingCode,
$stockAdjustment,
$stockAmount,
$campaignCode,
$couponCode,
$severalAdjustments,
);
// THEN
expect($stock->id())->toBe($id)
->and($stock->stockAmount)->toBe($this->stockAmount)
->and($stock->campaignCode)->toBe($this->campaignCode)
->and($stock->processingCode)->toBe($this->processingCode)
->and($stock->stockAdjustment)->toBe($stockAdjustment)
->and($stock->stockAmount)->toBe($stockAmount)
->and($stock->campaignCode)->toBe($campaignCode)
->and($stock->couponCode)->toBe($couponCode)
->and($stock->stockAdjustment)->toBe($severalAdjustments)
->and($stock->updatedAt)->toBeNull()
->and($stock->updatedAt())->toBeNull();
});
it('hydrates a stock without stock adjustement', function (): void {
it('hydrates a stock without stock adjustement', function () use ($stockAmount, $campaignCode, $couponCode): void {
// GIVEN
$id = StockId::generate();
// WHEN
$stock = Stock::hydrate(
$id,
$this->stockAmount,
$this->campaignCode,
$this->processingCode
$stockAmount,
$campaignCode,
$couponCode,
);
// THEN
expect($stock->id())->toBe($id)
->and($stock->stockAmount)->toBe($this->stockAmount)
->and($stock->campaignCode)->toBe($this->campaignCode)
->and($stock->processingCode)->toBe($this->processingCode)
->and($stock->stockAmount)->toBe($stockAmount)
->and($stock->campaignCode)->toBe($campaignCode)
->and($stock->couponCode)->toBe($couponCode)
->and($stock->stockAdjustment)->toBe([])
->and($stock->updatedAt)->toBeNull()
->and($stock->updatedAt())->toBeNull();
});
it('rejects a stock with a non-stock-adjustment element', function (): void {
it('rejects a stock with a non-stock-adjustment element', function () use ($stockAmount, $campaignCode, $couponCode): void {
// GIVEN
$id = StockId::generate();
$stockAdjustment = [new StockAmount(5)];
$id = StockId::generate();
$invalidAdjustment = [new StockAmount(5)];
// WHEN / THEN
expect(fn (): Stock => Stock::hydrate(
$id,
$this->stockAmount,
$this->campaignCode,
$this->processingCode,
$stockAdjustment,
$stockAmount,
$campaignCode,
$couponCode,
$invalidAdjustment,
))->toThrow(\DomainException::class, 'Stock adjustment must be StockAdjustment instances.');
});
it('hydrates a stock with an explicit updatedAt date', function (): void {
it('hydrates a stock with an explicit updatedAt date', function () use ($stockAmount, $campaignCode, $couponCode, $stockAdjustment): void {
// GIVEN
$updatedAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC'));
// WHEN
$stock = Stock::hydrate(
StockId::generate(),
$this->stockAmount,
$this->campaignCode,
$this->processingCode,
$this->stockAdjustment,
$stockAmount,
$campaignCode,
$couponCode,
$stockAdjustment,
$updatedAt,
);
@ -129,16 +127,16 @@ describe('stock', function (): void {
->and($stock->updatedAt())->toBe($updatedAt);
});
it('returns the creation date extracted from the id', function (): void {
it('returns the creation date extracted from the id', function () use ($stockAmount, $campaignCode, $couponCode, $stockAdjustment): void {
// GIVEN
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
$id = new StockId(Uuid::uuid7($expected));
$stock = Stock::hydrate(
$id,
$this->stockAmount,
$this->campaignCode,
$this->processingCode,
$this->stockAdjustment
$stockAmount,
$campaignCode,
$couponCode,
$stockAdjustment
);
// WHEN

View file

@ -9,25 +9,22 @@ use Domain\Models\ValueObjects\Actor\ActorDisplayName;
covers(ActorDisplayName::class);
describe('actor displayname', function (): void {
beforeEach(function (): void {
$this->value = 'Johnny';
$this->emptyValue = '';
});
$value = 'Johnny';
$emptyValue = '';
it('creates a valid actor displayname', function (): void {
it('creates a valid actor displayname', function () use ($value): void {
// GIVEN / WHEN
$name = new ActorDisplayName(
$this->value
$value
);
// THEN
expect($name->value)->toBe($this->value);
expect($name->value)->toBe($value);
});
it('rejects invalid actor displayname', function (): void {
it('rejects invalid actor displayname', function () use ($emptyValue): void {
// GIVEN / WHEN / THEN
expect(fn (): ActorDisplayName => new ActorDisplayName(
$this->emptyValue
))->toThrow(\DomainException::class);
expect(fn (): ActorDisplayName => new ActorDisplayName($emptyValue))
->toThrow(\DomainException::class);
});
});

View file

@ -9,26 +9,21 @@ use Domain\Models\ValueObjects\Brand\BrandName;
covers(BrandName::class);
describe('brand name', function (): void {
beforeEach(function (): void {
$this->value = 'Name';
$this->emptyValue = '';
});
$value = 'Name';
$emptyValue = '';
it('creates a valid brand name', function (): void {
it('creates a valid brand name', function () use ($value): void {
// GIVEN / WHEN
$name = new BrandName(
$this->value
);
$name = new BrandName($value);
// THEN
expect($name->value)->toBe($this->value);
expect($name->value)->toBe($value);
});
it('rejects invalid brand name', function (): void {
it('rejects invalid brand name', function () use ($emptyValue): void {
// GIVEN / WHEN / THEN
expect(fn (): BrandName => new BrandName(
$this->emptyValue
))->toThrow(\DomainException::class);
expect(fn (): BrandName => new BrandName($emptyValue))
->toThrow(\DomainException::class);
});
it('rejects a name that exceeds the maximum allowed length', function (): void {

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Brand;
use Domain\Models\ValueObjects\Brand\BrandSortCriteria;
use Domain\Models\ValueObjects\Brand\BrandSortField;
covers(BrandSortCriteria::class);
describe('brand sort criteria', function (): void {
it('sorts by name ascending by default', function (): void {
// GIVEN / WHEN
$criteria = new BrandSortCriteria();
// THEN
expect($criteria->field)->toBe(BrandSortField::Name)
->and($criteria->direction)->toBe(\SortDirection::Ascending);
});
it('keeps the given field and direction', function (): void {
// GIVEN
$field = BrandSortField::Name;
$direction = \SortDirection::Descending;
// WHEN
$criteria = new BrandSortCriteria($field, $direction);
// THEN
expect($criteria->field)->toBe($field)
->and($criteria->direction)->toBe($direction);
});
});

View file

@ -9,23 +9,20 @@ use Domain\Models\ValueObjects\Campaign\CampaignCode;
covers(CampaignCode::class);
describe('campaign code', function (): void {
beforeEach(function (): void {
$this->code = 'Name';
$this->emptyCode = '';
});
$code = 'Name';
$emptyCode = '';
it('creates a valid campaign code', function (): void {
it('creates a valid campaign code', function () use ($code): void {
// GIVEN / WHEN
$name = new CampaignCode($this->code);
$name = new CampaignCode($code);
// THEN
expect($name->code)->toBe($this->code);
expect($name->code)->toBe($code);
});
it('rejects invalid campaign code', function (): void {
it('rejects invalid campaign code', function () use ($emptyCode): void {
// GIVEN / WHEN / THEN
expect(fn (): CampaignCode => new CampaignCode(
$this->emptyCode
))->toThrow(\DomainException::class);
expect(fn (): CampaignCode => new CampaignCode($emptyCode))
->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Campaign;
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
use Domain\Models\ValueObjects\Campaign\CampaignCouponArray;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\PrintRules;
covers(CampaignCouponArray::class);
describe('campaign coupon array', function (): void {
$coupon = new CampaignCoupon(
new CouponCode('AAA01'),
new PrintRules(maxPerCustomer: 3),
);
it('creates an array with valid coupons', function () use ($coupon): void {
// GIVEN
$coupons = [$coupon, $coupon];
// WHEN
$array = new CampaignCouponArray($coupons);
// THEN
expect($array->value)->toBe($coupons);
});
it('rejects an empty array', function (): void {
// GIVEN / WHEN
$action = fn (): CampaignCouponArray => new CampaignCouponArray([]);
// THEN
expect($action)->toThrow(\DomainException::class, 'Coupons requires at least one CampaignCoupon.');
});
it('rejects a non-coupon element', function (): void {
// GIVEN / WHEN
$action = fn (): CampaignCouponArray => new CampaignCouponArray(['PC1']);
// THEN
expect($action)->toThrow(\DomainException::class, 'Coupons must be CampaignCoupon instances.');
});
it('rejects a non-coupon element mixed with valid coupons', function () use ($coupon): void {
// GIVEN
$coupons = [$coupon, 'PC1'];
// WHEN
$action = fn (): CampaignCouponArray => new CampaignCouponArray($coupons);
// THEN
expect($action)->toThrow(\DomainException::class, 'Coupons must be CampaignCoupon instances.');
});
});

View file

@ -5,78 +5,34 @@ declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Campaign;
use Domain\Models\ValueObjects\Campaign\CampaignCoupon;
use Domain\Models\ValueObjects\Coupon\CouponQuantity;
use Domain\Models\ValueObjects\Coupon\CouponCode;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Shared\Enums\Country;
covers(CampaignCoupon::class);
describe('campaign coupon', function (): void {
beforeEach(function (): void {
$this->processingCode = new ProcessingCode('PC1');
$this->quantity = new CouponQuantity(10);
$this->availableIn = Country::FRANCE;
$this->printRules = new PrintRules(maxPerCustomer: 3);
});
$couponCode = new CouponCode('AAA01');
$printRules = new PrintRules(maxPerCustomer: 3);
it('creates a valid campaign coupon', function (): void {
it('creates a valid campaign coupon', function () use ($couponCode, $printRules): void {
// GIVEN / WHEN
$campaignCoupon = new CampaignCoupon(
$this->processingCode,
$this->quantity,
$this->availableIn,
$this->printRules
couponCode: $couponCode,
printRules: $printRules,
);
// THEN
expect($campaignCoupon->processingCode)->toBe($this->processingCode)
->and($campaignCoupon->quantity)->toBe($this->quantity)
->and($campaignCoupon->availableIn)->toBe($this->availableIn)
->and($campaignCoupon->printRules)->toBe($this->printRules);
expect($campaignCoupon->couponCode)->toBe($couponCode)
->and($campaignCoupon->printRules)->toBe($printRules);
});
it('creates a valid campaign coupon whitout warning threshold quantity', function (): void {
it('creates a valid campaign coupon whitout print rules', function () use ($couponCode): void {
// GIVEN / WHEN
$campaignCoupon = new CampaignCoupon(
processingCode: $this->processingCode,
quantity: $this->quantity,
availableIn: $this->availableIn,
printRules: $this->printRules
couponCode: $couponCode,
);
// THEN
expect($campaignCoupon->processingCode)->toBe($this->processingCode)
->and($campaignCoupon->quantity)->toBe($this->quantity)
->and($campaignCoupon->availableIn)->toBe($this->availableIn)
->and($campaignCoupon->printRules)->toBe($this->printRules);
});
it('creates a valid campaign coupon whitout print rules', function (): void {
// GIVEN / WHEN
$campaignCoupon = new CampaignCoupon(
processingCode: $this->processingCode,
quantity: $this->quantity,
availableIn: $this->availableIn,
);
// THEN
expect($campaignCoupon->processingCode)->toBe($this->processingCode)
->and($campaignCoupon->quantity)->toBe($this->quantity)
->and($campaignCoupon->availableIn)->toBe($this->availableIn);
});
it('creates a valid campaign coupon whitout warning threshold quantity and print rules', function (): void {
// GIVEN / WHEN
$campaignCoupon = new CampaignCoupon(
processingCode: $this->processingCode,
quantity: $this->quantity,
availableIn: $this->availableIn
);
// THEN
expect($campaignCoupon->processingCode)->toBe($this->processingCode)
->and($campaignCoupon->quantity)->toBe($this->quantity)
->and($campaignCoupon->availableIn)->toBe($this->availableIn);
expect($campaignCoupon->couponCode)->toBe($couponCode);
});
});

View file

@ -9,23 +9,20 @@ use Domain\Models\ValueObjects\Campaign\CampaignName;
covers(CampaignName::class);
describe('campaign name', function (): void {
beforeEach(function (): void {
$this->value = 'Name';
$this->emptyValue = '';
});
$value = 'Name';
$emptyValue = '';
it('creates a valid campaign name', function (): void {
it('creates a valid campaign name', function () use ($value): void {
// GIVEN / WHEN
$name = new CampaignName($this->value);
$name = new CampaignName($value);
// THEN
expect($name->value)->toBe($this->value);
expect($name->value)->toBe($value);
});
it('rejects invalid campaign name', function (): void {
it('rejects invalid campaign name', function () use ($emptyValue): void {
// GIVEN / WHEN / THEN
expect(fn (): CampaignName => new CampaignName(
$this->emptyValue
))->toThrow(\DomainException::class);
expect(fn (): CampaignName => new CampaignName($emptyValue))
->toThrow(\DomainException::class);
});
});

View file

@ -10,33 +10,31 @@ use Shared\ValueObjects\Email;
covers(Applicant::class);
describe('applicant', function (): void {
beforeEach(function (): void {
$this->firstname = 'John';
$this->lastname = 'Doe';
$this->email = new Email('john.doe@example.com');
});
$firstname = 'John';
$lastname = 'Doe';
$email = new Email('john.doe@example.com');
it('creates a valid applicant', function (): void {
it('creates a valid applicant', function () use ($firstname, $lastname, $email): void {
// GIVEN / WHEN
$applicant = new Applicant(
$this->firstname,
$this->lastname,
$this->email
$firstname,
$lastname,
$email,
);
// THEN
expect($applicant)->toBeInstanceOf(Applicant::class)
->and($applicant->firstname)->toBe($this->firstname)
->and($applicant->lastname)->toBe($this->lastname)
->and($applicant->email)->toBe($this->email);
->and($applicant->firstname)->toBe($firstname)
->and($applicant->lastname)->toBe($lastname)
->and($applicant->email)->toBe($email);
});
it('rejects an empty or whitespace-only firstname', function (string $firstname): void {
it('rejects an empty or whitespace-only firstname', function (string $invalidFirstname) use ($lastname, $email): void {
// WHEN
$action = fn (): Applicant => new Applicant(
$firstname,
$this->lastname,
$this->email
$invalidFirstname,
$lastname,
$email,
);
// THEN
@ -46,42 +44,42 @@ describe('applicant', function (): void {
'spaces' => ' ',
]);
it('rejects a firstname that exceeds the maximum allowed length', function (): void {
it('rejects a firstname that exceeds the maximum allowed length', function () use ($lastname, $email): void {
// GIVEN
$firstname = str_repeat('a', 201);
$tooLongFirstname = str_repeat('a', 201);
// WHEN
$action = fn (): Applicant => new Applicant(
$firstname,
$this->lastname,
$this->email
$tooLongFirstname,
$lastname,
$email,
);
// THEN
expect($action)->toThrow(\DomainException::class, 'Applicant firstname exceeds the maximum allowed length.');
});
it('accepts a firstname of exactly the maximum length', function (): void {
it('accepts a firstname of exactly the maximum length', function () use ($lastname, $email): void {
// GIVEN
$firstname = str_repeat('a', 200);
$maxLengthFirstname = str_repeat('a', 200);
// WHEN
$applicant = new Applicant(
$firstname,
$this->lastname,
$this->email
$maxLengthFirstname,
$lastname,
$email,
);
// THEN
expect(mb_strlen($applicant->firstname))->toBe(200);
});
it('rejects an empty or whitespace-only lastname', function (string $lastname): void {
it('rejects an empty or whitespace-only lastname', function (string $invalidLastname) use ($firstname, $email): void {
// WHEN
$action = fn (): Applicant => new Applicant(
$this->firstname,
$lastname,
$this->email
$firstname,
$invalidLastname,
$email,
);
// THEN
@ -91,30 +89,30 @@ describe('applicant', function (): void {
'spaces' => ' ',
]);
it('rejects a lastname that exceeds the maximum allowed length', function (): void {
it('rejects a lastname that exceeds the maximum allowed length', function () use ($firstname, $email): void {
// GIVEN
$lastname = str_repeat('a', 201);
$tooLongLastname = str_repeat('a', 201);
// WHEN
$action = fn (): Applicant => new Applicant(
$this->firstname,
$lastname,
$this->email
$firstname,
$tooLongLastname,
$email,
);
// THEN
expect($action)->toThrow(\DomainException::class, 'Applicant lastname exceeds the maximum allowed length.');
});
it('accepts a lastname of exactly the maximum length', function (): void {
it('accepts a lastname of exactly the maximum length', function () use ($firstname, $email): void {
// GIVEN
$lastname = str_repeat('a', 200);
$maxLengthLastname = str_repeat('a', 200);
// WHEN
$applicant = new Applicant(
$this->firstname,
$lastname,
$this->email
$firstname,
$maxLengthLastname,
$email,
);
// THEN

View file

@ -11,43 +11,41 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestStatus;
covers(CampaignRequestFilters::class);
describe('campaign request filters', function (): void {
beforeEach(function (): void {
$this->brandCode = new BrandCode('AAA01');
$this->status = CampaignRequestStatus::CREATED;
});
$brandCode = new BrandCode('AAA01');
$status = CampaignRequestStatus::CREATED;
it('creates filters with both fields set', function (): void {
it('creates filters with both fields set', function () use ($brandCode, $status): void {
// GIVEN / WHEN
$filters = new CampaignRequestFilters(
brandCode: $this->brandCode,
status: $this->status
brandCode: $brandCode,
status: $status,
);
// THEN
expect($filters->brandCode)->toBe($this->brandCode)
->and($filters->status)->toBe($this->status);
expect($filters->brandCode)->toBe($brandCode)
->and($filters->status)->toBe($status);
});
it('creates filters with only brandCode set', function (): void {
it('creates filters with only brandCode set', function () use ($brandCode): void {
// GIVEN / WHEN
$filters = new CampaignRequestFilters(
brandCode: $this->brandCode
brandCode: $brandCode,
);
// THEN
expect($filters->brandCode)->toBe($this->brandCode)
expect($filters->brandCode)->toBe($brandCode)
->and($filters->status)->toBeNull();
});
it('creates filters with only status set', function (): void {
it('creates filters with only status set', function () use ($status): void {
// GIVEN / WHEN
$filters = new CampaignRequestFilters(
status: $this->status
status: $status,
);
// THEN
expect($filters->brandCode)->toBeNull()
->and($filters->status)->toBe($this->status);
->and($filters->status)->toBe($status);
});
it('rejects filters without any field set', function (): void {

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\CampaignRequest;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortClause;
use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField;
covers(CampaignRequestSortClause::class);
describe('campaign request sort clause', function (): void {
it('keeps the given field and direction', function (): void {
// GIVEN
$field = CampaignRequestSortField::cases()[0];
$direction = \SortDirection::Descending;
// WHEN
$clause = new CampaignRequestSortClause($field, $direction);
// THEN
expect($clause->field)->toBe($field)
->and($clause->direction)->toBe($direction);
});
});

View file

@ -11,48 +11,42 @@ use Domain\Models\ValueObjects\CampaignRequest\CampaignRequestSortField;
covers(CampaignRequestSortCriteria::class);
describe('campaign request sort criteria', function (): void {
beforeEach(function (): void {
$this->singleClauses = [
new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending),
];
$this->clauses = [
new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending),
new CampaignRequestSortClause(field: CampaignRequestSortField::Status, direction: \SortDirection::Descending),
];
$this->invalidClauses = [CampaignRequestSortField::BrandCode];
$this->emptyClauses = [];
});
$singleClauses = [
new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending),
];
$clauses = [
new CampaignRequestSortClause(field: CampaignRequestSortField::BrandCode, direction: \SortDirection::Ascending),
new CampaignRequestSortClause(field: CampaignRequestSortField::Status, direction: \SortDirection::Descending),
];
$invalidClauses = [CampaignRequestSortField::BrandCode];
$emptyClauses = [];
it('creates a valid campaign request sort criteria with one clause', function (): void {
it('creates a valid campaign request sort criteria with one clause', function () use ($singleClauses): void {
// GIVEN / WHEN
$campaignRequestSortCriteria = new CampaignRequestSortCriteria(
$this->singleClauses
);
$campaignRequestSortCriteria = new CampaignRequestSortCriteria($singleClauses);
// THEN
expect($campaignRequestSortCriteria->clauses)->toBe($this->singleClauses);
expect($campaignRequestSortCriteria->clauses)->toBe($singleClauses);
});
it('creates a valid campaign request sort criteria with several clauses', function (): void {
it('creates a valid campaign request sort criteria with several clauses', function () use ($clauses): void {
// GIVEN / WHEN
$campaignRequestSortCriteria = new CampaignRequestSortCriteria(
$this->clauses
);
$campaignRequestSortCriteria = new CampaignRequestSortCriteria($clauses);
// THEN
expect($campaignRequestSortCriteria->clauses)->toBe($this->clauses);
expect($campaignRequestSortCriteria->clauses)->toBe($clauses);
});
it('rejects a campaign request sort criteria without clause', function (): void {
it('rejects a campaign request sort criteria without clause', function () use ($emptyClauses): void {
// GIVEN / WHEN / THEN
expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($this->emptyClauses))
expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($emptyClauses))
->toThrow(\DomainException::class, 'Campaign Request Sort Criteria requires at least one clause.');
});
it('rejects a campaign request sort criteria with a non-clause element', function (): void {
it('rejects a campaign request sort criteria with a non-clause element', function () use ($invalidClauses): void {
// GIVEN / WHEN / THEN
expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($this->invalidClauses))
expect(fn (): CampaignRequestSortCriteria => new CampaignRequestSortCriteria($invalidClauses))
->toThrow(\DomainException::class, 'CampaignRequestSortCriteria clauses must be CampaignRequestSortClause instances.');
});
});

View file

@ -9,22 +9,20 @@ use Domain\Models\ValueObjects\CampaignRequest\RejectionReason;
covers(RejectionReason::class);
describe('rejection reason', function (): void {
beforeEach(function (): void {
$this->value = 'Missing legal documents.';
});
$value = 'Missing legal documents.';
it('creates a valid rejection reason', function (): void {
it('creates a valid rejection reason', function () use ($value): void {
// GIVEN / WHEN
$reason = new RejectionReason($this->value);
$reason = new RejectionReason($value);
// THEN
expect($reason)->toBeInstanceOf(RejectionReason::class)
->and($reason->value)->toBe($this->value);
->and($reason->value)->toBe($value);
});
it('rejects an empty or whitespace-only reason', function (string $value): void {
it('rejects an empty or whitespace-only reason', function (string $invalidValue): void {
// WHEN
$action = fn (): RejectionReason => new RejectionReason($value);
$action = fn (): RejectionReason => new RejectionReason($invalidValue);
// THEN
expect($action)->toThrow(\DomainException::class, 'Rejection reason cannot be empty.');

View file

@ -11,49 +11,41 @@ use DateTime;
covers(CouponAvailabilityCalendar::class);
describe('coupon availability calendar', function (): void {
beforeEach(function (): void {
$this->singlePeriods = [
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
];
$this->periods = [
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
new Period(new DateTime('2026-03-01'), new DateTime('2026-04-01')),
];
$this->invalidPeriods = ['2026-08-25', '2026-10-31'];
$this->emptyPeriods = [];
});
$singlePeriods = [
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
];
$periods = [
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
new Period(new DateTime('2026-03-01'), new DateTime('2026-04-01')),
];
$invalidPeriods = ['2026-08-25', '2026-10-31'];
$emptyPeriods = [];
it('creates a valid coupon availability calendar with one period', function (): void {
it('creates a valid coupon availability calendar with one period', function () use ($singlePeriods): void {
// GIVEN / WHEN
$couponAvailabilityCalendar = new CouponAvailabilityCalendar(
$this->singlePeriods
);
$couponAvailabilityCalendar = new CouponAvailabilityCalendar($singlePeriods);
// THEN
expect($couponAvailabilityCalendar->periods)->toBe($this->singlePeriods);
expect($couponAvailabilityCalendar->periods)->toBe($singlePeriods);
});
it('creates a valid coupon availability calendar with several periods', function (): void {
it('creates a valid coupon availability calendar with several periods', function () use ($periods): void {
// GIVEN / WHEN
$couponAvailabilityCalendar = new CouponAvailabilityCalendar(
$this->periods
);
$couponAvailabilityCalendar = new CouponAvailabilityCalendar($periods);
// THEN
expect($couponAvailabilityCalendar->periods)->toBe($this->periods);
expect($couponAvailabilityCalendar->periods)->toBe($periods);
});
it('rejects a coupon availability calendar without period', function (): void {
it('rejects a coupon availability calendar without period', function () use ($emptyPeriods): void {
// GIVEN / WHEN / THEN
expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar(
$this->emptyPeriods
))->toThrow(\DomainException::class);
expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar($emptyPeriods))
->toThrow(\DomainException::class);
});
it('rejects a coupon availability calendar with a non-period element', function (): void {
it('rejects a coupon availability calendar with a non-period element', function () use ($invalidPeriods): void {
// GIVEN / WHEN / THEN
expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar(
$this->invalidPeriods
))->toThrow(\DomainException::class, 'CouponAvailabilityCalendar periods must be Period instances.');
expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar($invalidPeriods))
->toThrow(\DomainException::class, 'CouponAvailabilityCalendar periods must be Period instances.');
});
});

View file

@ -11,15 +11,13 @@ use DateTime;
covers(CouponDiffusion::class);
describe('coupon diffusion', function (): void {
beforeEach(function (): void {
$this->period = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'));
});
$period = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'));
it('creates a valid coupon diffusion with one period', function (): void {
it('creates a valid coupon diffusion with one period', function () use ($period): void {
// GIVEN / WHEN
$couponDiffusion = new CouponDiffusion($this->period);
$couponDiffusion = new CouponDiffusion($period);
// THEN
expect($couponDiffusion->period)->toBe($this->period);
expect($couponDiffusion->period)->toBe($period);
});
});

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
covers(CouponIncoCode::class);
describe('inco code', function (): void {
$value = 1010100101;
$zeroValue = 0;
it('creates a valid inco code', function () use ($value): void {
// GIVEN / WHEN
$incoCode = new CouponIncoCode($value);
// THEN
expect($incoCode->value)->toBe($value);
});
it('rejects invalid inco code', function () use ($zeroValue): void {
// GIVEN / WHEN / THEN
expect(fn (): CouponIncoCode => new CouponIncoCode($zeroValue))
->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Shared\Enums\Country;
covers(CouponLegalInformationArray::class);
describe('coupon legal information array', function (): void {
$franceInformation = new CouponLegalInformation('Mentions légales France.', Country::FRANCE);
$beneluxInformation = new CouponLegalInformation('Legal information Benelux.', Country::BENELUX);
it('creates an array containing a France legal information', function () use ($franceInformation): void {
// GIVEN
$informations = [$franceInformation];
// WHEN
$array = new CouponLegalInformationArray($informations);
// THEN
expect($array->value)->toBe($informations);
});
it('accepts additional countries next to France', function () use ($franceInformation, $beneluxInformation): void {
// GIVEN
$informations = [$beneluxInformation, $franceInformation];
// WHEN
$array = new CouponLegalInformationArray($informations);
// THEN
expect($array->value)->toBe($informations);
});
it('rejects an empty array', function (): void {
// GIVEN / WHEN
$action = fn (): CouponLegalInformationArray => new CouponLegalInformationArray([]);
// THEN
expect($action)->toThrow(
\DomainException::class,
'CouponLegalInformations requires at least one CouponLegalInformation.',
);
});
it('rejects a non legal information element', function (): void {
// GIVEN / WHEN
$action = fn (): CouponLegalInformationArray => new CouponLegalInformationArray(['legal']);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Value of CouponLegalInformationArray must be CouponLegalInformation instances.',
);
});
it('rejects a non legal information element mixed with valid ones', function () use ($franceInformation): void {
// GIVEN
$informations = [$franceInformation, 'legal'];
// WHEN
$action = fn (): CouponLegalInformationArray => new CouponLegalInformationArray($informations);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Value of CouponLegalInformationArray must be CouponLegalInformation instances.',
);
});
it('rejects an array without France legal information', function () use ($beneluxInformation): void {
// GIVEN
$informations = [$beneluxInformation];
// WHEN
$action = fn (): CouponLegalInformationArray => new CouponLegalInformationArray($informations);
// THEN
expect($action)->toThrow(
\DomainException::class,
'CouponLegalInformations requires a CouponLegalInformation for France.',
);
});
});

View file

@ -10,29 +10,39 @@ use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
covers(CouponLegalInformation::class);
describe('coupon legal information', function (): void {
beforeEach(function (): void {
$this->legalText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.';
$this->emptyLegalText = '';
$this->forLocale = Country::FRANCE;
});
$legalText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.';
$emptyLegalText = '';
$forLocale = Country::FRANCE;
it('creates a valid coupon legal information', function (): void {
it('creates a valid coupon legal information FRANCE', function () use ($legalText, $forLocale): void {
// GIVEN / WHEN
$couponLegalInformation = new CouponLegalInformation(
$this->legalText,
$this->forLocale
$legalText,
$forLocale,
);
// THEN
expect($couponLegalInformation->legalText)->toBe($this->legalText)
->and($couponLegalInformation->forLocale)->toBe($this->forLocale);
expect($couponLegalInformation->legalText)->toBe($legalText)
->and($couponLegalInformation->forLocale)->toBe($forLocale);
});
it('rejects invalid coupon legal information', function (): void {
it('creates a valid coupon legal information BENELUX', function () use ($legalText): void {
// GIVEN / WHEN
$couponLegalInformation = new CouponLegalInformation(
$legalText,
Country::BENELUX,
);
// THEN
expect($couponLegalInformation->legalText)->toBe($legalText)
->and($couponLegalInformation->forLocale)->toBe(Country::BENELUX);
});
it('rejects invalid coupon legal information', function () use ($emptyLegalText, $forLocale): void {
// GIVEN / WHEN / THEN
expect(fn (): CouponLegalInformation => new CouponLegalInformation(
$this->emptyLegalText,
$this->forLocale
$emptyLegalText,
$forLocale,
))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponMovement;
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
covers(CouponMovementArray::class);
describe('coupon movement array', function (): void {
$couponMovement = new CouponMovement(100);
it('creates an array with valid coupon movements', function () use ($couponMovement): void {
// GIVEN
$couponMovements = [$couponMovement, $couponMovement];
// WHEN
$array = new CouponMovementArray($couponMovements);
// THEN
expect($array->couponMovements)->toBe($couponMovements);
});
it('rejects an empty array', function (): void {
// GIVEN / WHEN
$action = fn (): CouponMovementArray => new CouponMovementArray([]);
// THEN
expect($action)->toThrow(
\DomainException::class,
'CouponMovementArray requires at least one CouponMovement.',
);
});
it('rejects a non coupon movement element', function (): void {
// GIVEN / WHEN
$action = fn (): CouponMovementArray => new CouponMovementArray(['movement']);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Value of CouponMovementArray must be CouponMovement instances.',
);
});
it('rejects a non coupon movement element mixed with valid ones', function () use ($couponMovement): void {
// GIVEN
$couponMovements = [$couponMovement, 'movement'];
// WHEN
$action = fn (): CouponMovementArray => new CouponMovementArray($couponMovements);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Value of CouponMovementArray must be CouponMovement instances.',
);
});
});

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponMovement;
covers(CouponMovement::class);
describe('coupon movement', function (): void {
it('keeps the given quantity and creation date', function (): void {
// GIVEN
$quantity = 10;
$createdAt = new \DateTimeImmutable('2026-01-01 12:00:00');
// WHEN
$movement = new CouponMovement($quantity, $createdAt);
// THEN
expect($movement->quantity)->toBe($quantity)
->and($movement->createdAt)->toBe($createdAt);
});
it('defaults the creation date to now', function (): void {
// GIVEN
$before = new \DateTimeImmutable();
// WHEN
$movement = new CouponMovement(5);
// THEN
$after = new \DateTimeImmutable();
expect($movement->createdAt)->toBeInstanceOf(\DateTimeImmutable::class)
->and($movement->createdAt >= $before)->toBeTrue()
->and($movement->createdAt <= $after)->toBeTrue();
});
it('accepts a negative quantity', function (): void {
// GIVEN / WHEN
$movement = new CouponMovement(-3);
// THEN
expect($movement->quantity)->toBe(-3);
});
});

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\Coupon\CouponName;
covers(CouponName::class);
describe('coupon name', function (): void {
$value = 'Name';
$emptyValue = '';
it('creates a valid coupon name', function () use ($value): void {
// GIVEN / WHEN
$name = new CouponName($value);
// THEN
expect($name->value)->toBe($value);
});
it('rejects invalid coupon name', function () use ($emptyValue): void {
// GIVEN / WHEN / THEN
expect(fn (): CouponName => new CouponName($emptyValue))
->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
use Shared\Enums\Country;
covers(CouponProcessingCode::class);
describe('processing code', function (): void {
$value = 'ABC1234567890';
$emptyValue = '';
it('creates a valid processing code FRANCE', function () use ($value): void {
// GIVEN / WHEN
$processingCode = new CouponProcessingCode(
$value,
Country::FRANCE,
);
// THEN
expect($processingCode->value)->toBe($value);
});
it('creates a valid processing code BENELUX', function () use ($value): void {
// GIVEN / WHEN
$processingCode = new CouponProcessingCode(
$value,
Country::BENELUX,
);
// THEN
expect($processingCode->value)->toBe($value);
});
it('rejects invalid processing code', function () use ($emptyValue): void {
// GIVEN / WHEN / THEN
expect(fn (): CouponProcessingCode => new CouponProcessingCode(
$emptyValue,
Country::FRANCE,
))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
use Shared\Enums\Country;
covers(CouponProcessingCodeArray::class);
describe('coupon processing code array', function (): void {
$franceCode = new CouponProcessingCode('ABC1234567890', Country::FRANCE);
$beneluxCode = new CouponProcessingCode('XYZ0987654321', Country::BENELUX);
it('creates an array containing a France processing code', function () use ($franceCode): void {
// GIVEN
$processingCodes = [$franceCode];
// WHEN
$array = new CouponProcessingCodeArray($processingCodes);
// THEN
expect($array->value)->toBe($processingCodes);
});
it('accepts additional countries next to France', function () use ($franceCode, $beneluxCode): void {
// GIVEN
$processingCodes = [$beneluxCode, $franceCode];
// WHEN
$array = new CouponProcessingCodeArray($processingCodes);
// THEN
expect($array->value)->toBe($processingCodes);
});
it('rejects an empty array', function (): void {
// GIVEN / WHEN
$action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray([]);
// THEN
expect($action)->toThrow(
\DomainException::class,
'ProcessingCodes requires at least one CouponProcessingCode.',
);
});
it('rejects a non processing code element', function (): void {
// GIVEN / WHEN
$action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray(['ABC1234567890']);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Value of CouponProcessingCodeArray must be CouponProcessingCode instances.',
);
});
it('rejects a non processing code element mixed with valid ones', function () use ($franceCode): void {
// GIVEN
$processingCodes = [$franceCode, 'ABC1234567890'];
// WHEN
$action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray($processingCodes);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Value of CouponProcessingCodeArray must be CouponProcessingCode instances.',
);
});
it('rejects an array without France processing code', function () use ($beneluxCode): void {
// GIVEN
$processingCodes = [$beneluxCode];
// WHEN
$action = fn (): CouponProcessingCodeArray => new CouponProcessingCodeArray($processingCodes);
// THEN
expect($action)->toThrow(
\DomainException::class,
'CouponProcessingCodes requires a CouponProcessingCode for France.',
);
});
});

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
use Shared\Enums\Country;
covers(CouponProcessingCode::class);
describe('coupon processing code', function (): void {
$value = 'ABC1234567890';
it('creates a valid processing code', function () use ($value): void {
// GIVEN / WHEN
$code = new CouponProcessingCode($value, Country::FRANCE);
// THEN
expect($code->value)->toBe($value)
->and($code->availableIn)->toBe(Country::FRANCE);
});
it('accepts lowercase and uppercase letters and digits', function (): void {
// GIVEN / WHEN
$code = new CouponProcessingCode('aBc1234567xYz', Country::FRANCE);
// THEN
expect($code->value)->toBe('aBc1234567xYz');
});
it('rejects an invalid value', function (string $invalidValue): void {
// GIVEN / WHEN
$action = fn (): CouponProcessingCode => new CouponProcessingCode($invalidValue, Country::FRANCE);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Processing Code must be exactly 13 alphanumeric characters, e.g. ABC1234567890.',
);
})->with([
'empty' => '',
'too short' => 'ABC123456789',
'too long' => 'ABC12345678901',
'special char' => 'ABC123456789!',
'with space' => 'ABC 123456789',
'trailing break' => "ABC1234567890\n",
]);
it('is equal to another processing code with the same value', function () use ($value): void {
// GIVEN
$code = new CouponProcessingCode($value, Country::FRANCE);
$other = new CouponProcessingCode($value, Country::FRANCE);
// WHEN / THEN
expect($code->equals($other))->toBeTrue();
});
it('ignores the country when comparing', function () use ($value): void {
// GIVEN
$france = new CouponProcessingCode($value, Country::FRANCE);
$benelux = new CouponProcessingCode($value, Country::BENELUX);
// WHEN / THEN
expect($france->equals($benelux))->toBeTrue();
});
it('is not equal to a processing code with a different value', function () use ($value): void {
// GIVEN
$code = new CouponProcessingCode($value, Country::FRANCE);
$other = new CouponProcessingCode('XYZ0987654321', Country::FRANCE);
// WHEN / THEN
expect($code->equals($other))->toBeFalse();
});
it('is not equal to a value that is not a processing code', function () use ($value): void {
// GIVEN
$code = new CouponProcessingCode($value, Country::FRANCE);
// WHEN / THEN
expect($code->equals($value))->toBeFalse()
->and($code->equals(null))->toBeFalse();
});
it('is converted to its value as a string', function () use ($value): void {
// GIVEN
$code = new CouponProcessingCode($value, Country::FRANCE);
// WHEN / THEN
expect((string) $code)->toBe($value);
});
});

View file

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
covers(CouponProductArray::class);
describe('coupon product array', function (): void {
$firstProduct = new CouponIncoCode(35784);
$secondProduct = new CouponIncoCode(15522);
it('is empty by default', function (): void {
// GIVEN / WHEN
$array = new CouponProductArray();
// THEN
expect($array->value)->toBe([]);
});
it('accepts an empty array explicitly', function (): void {
// GIVEN / WHEN
$array = new CouponProductArray([]);
// THEN
expect($array->value)->toBe([]);
});
it('creates an array with valid products', function () use ($firstProduct, $secondProduct): void {
// GIVEN
$products = [$firstProduct, $secondProduct];
// WHEN
$array = new CouponProductArray($products);
// THEN
expect($array->value)->toBe($products);
});
it('rejects a non product element', function (): void {
// GIVEN / WHEN
$action = fn (): CouponProductArray => new CouponProductArray([35784]);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Value of CouponProductArray must be CouponIncoCode instances.',
);
});
it('rejects a non product element mixed with valid ones', function () use ($firstProduct): void {
// GIVEN
$products = [$firstProduct, 35784];
// WHEN
$action = fn (): CouponProductArray => new CouponProductArray($products);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Value of CouponProductArray must be CouponIncoCode instances.',
);
});
});

View file

@ -9,38 +9,30 @@ use Domain\Models\ValueObjects\Coupon\CouponQuantity;
covers(CouponQuantity::class);
describe('coupon quantity', function (): void {
beforeEach(function (): void {
$this->quantity = 10;
$this->zeroQuantity = 0;
$this->negativeQuantity = -1;
});
$quantity = 10;
$zeroQuantity = 0;
$negativeQuantity = -1;
it('creates a valid coupon quantity', function (): void {
it('creates a valid coupon quantity', function () use ($quantity): void {
// GIVEN / WHEN
$couponQuantity = new CouponQuantity(
$this->quantity
);
$couponQuantity = new CouponQuantity($quantity);
// THEN
expect($couponQuantity)->toBeInstanceOf(CouponQuantity::class)
->and($couponQuantity->quantity)->toBe($this->quantity);
->and($couponQuantity->value)->toBe($quantity);
});
it('rejects a quantity of zero', function (): void {
it('rejects a quantity of zero', function () use ($zeroQuantity): void {
// WHEN
$action = fn (): CouponQuantity => new CouponQuantity(
$this->zeroQuantity
);
$action = fn (): CouponQuantity => new CouponQuantity($zeroQuantity);
// THEN
expect($action)->toThrow(\DomainException::class, 'CouponQuantity quantity must be positive.');
});
it('rejects a negative quantity', function (): void {
it('rejects a negative quantity', function () use ($negativeQuantity): void {
// WHEN
$action = fn (): CouponQuantity => new CouponQuantity(
$this->negativeQuantity
);
$action = fn (): CouponQuantity => new CouponQuantity($negativeQuantity);
// THEN
expect($action)->toThrow(\DomainException::class, 'CouponQuantity quantity must be positive.');

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Shared\Enums\Country;
covers(CouponValidityInformationArray::class);
describe('coupon validity information array', function (): void {
$franceInformation = new CouponValidityInformation('Valable en France.', Country::FRANCE);
$beneluxInformation = new CouponValidityInformation('Valid in Benelux.', Country::BENELUX);
it('creates an array containing a France validity information', function () use ($franceInformation): void {
// GIVEN
$informations = [$franceInformation];
// WHEN
$array = new CouponValidityInformationArray($informations);
// THEN
expect($array->value)->toBe($informations);
});
it('accepts additional countries next to France', function () use ($franceInformation, $beneluxInformation): void {
// GIVEN
$informations = [$beneluxInformation, $franceInformation];
// WHEN
$array = new CouponValidityInformationArray($informations);
// THEN
expect($array->value)->toBe($informations);
});
it('rejects an empty array', function (): void {
// GIVEN / WHEN
$action = fn (): CouponValidityInformationArray => new CouponValidityInformationArray([]);
// THEN
expect($action)->toThrow(
\DomainException::class,
'CouponValidityInformations requires at least one CouponValidityInformation.',
);
});
it('rejects a non validity information element', function (): void {
// GIVEN / WHEN
$action = fn (): CouponValidityInformationArray => new CouponValidityInformationArray(['validity']);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Value of CouponValidityInformationArray must be CouponValidityInformation instances.',
);
});
it('rejects a non validity information element mixed with valid ones', function () use ($franceInformation): void {
// GIVEN
$informations = [$franceInformation, 'validity'];
// WHEN
$action = fn (): CouponValidityInformationArray => new CouponValidityInformationArray($informations);
// THEN
expect($action)->toThrow(
\DomainException::class,
'Value of CouponValidityInformationArray must be CouponValidityInformation instances.',
);
});
it('rejects an array without France validity information', function () use ($beneluxInformation): void {
// GIVEN
$informations = [$beneluxInformation];
// WHEN
$action = fn (): CouponValidityInformationArray => new CouponValidityInformationArray($informations);
// THEN
expect($action)->toThrow(
\DomainException::class,
'CouponValidityInformations requires a CouponValidityInformation for France.',
);
});
});

View file

@ -10,29 +10,27 @@ use Shared\Enums\Country;
covers(CouponValidityInformation::class);
describe('coupon validity information', function (): void {
beforeEach(function (): void {
$this->validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.';
$this->emptyValidityText = '';
$this->forLocale = Country::FRANCE;
});
$validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.';
$emptyValidityText = '';
$forLocale = Country::FRANCE;
it('creates a valid coupon validity information', function (): void {
it('creates a valid coupon validity information', function () use ($validityText, $forLocale): void {
// GIVEN / WHEN
$couponValidityInformation = new CouponValidityInformation(
$this->validityText,
$this->forLocale
$validityText,
$forLocale,
);
// THEN
expect($couponValidityInformation->validityText)->toBe($this->validityText)
->and($couponValidityInformation->forLocale)->toBe($this->forLocale);
expect($couponValidityInformation->validityText)->toBe($validityText)
->and($couponValidityInformation->forLocale)->toBe($forLocale);
});
it('rejects invalid coupon validity information', function (): void {
it('rejects invalid coupon validity information', function () use ($emptyValidityText, $forLocale): void {
// GIVEN / WHEN / THEN
expect(fn (): CouponValidityInformation => new CouponValidityInformation(
$this->emptyValidityText,
$this->forLocale
$emptyValidityText,
$forLocale,
))->toThrow(\DomainException::class);
});
});

View file

@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\Coupon\CouponValidity;
use DateTime;
covers(CouponValidity::class);
describe('coupon validity', function (): void {
beforeEach(function (): void {
$this->days = 30;
$this->zeroDays = 0;
$this->until = new DateTime('2026-11-30');
});
it('creates a valid coupon validity', function (): void {
// GIVEN / WHEN
$couponValidity = new CouponValidity(
$this->days,
$this->until
);
// THEN
expect($couponValidity->days)->toBe($this->days)
->and($couponValidity->until)->toBe($this->until);
});
it('rejects invalid coupon validity', function (): void {
// GIVEN / WHEN / THEN
expect(fn (): CouponValidity => new CouponValidity(
$this->zeroDays,
$this->until
))->toThrow(\DomainException::class);
});
});

View file

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\CouponName;
covers(CouponName::class);
describe('coupon name', function (): void {
beforeEach(function (): void {
$this->value = 'Name';
$this->emptyValue = '';
});
it('creates a valid coupon name', function (): void {
// GIVEN / WHEN
$name = new CouponName(
$this->value
);
// THEN
expect($name->value)->toBe($this->value);
});
it('rejects invalid coupon name', function (): void {
// GIVEN / WHEN / THEN
expect(fn (): CouponName => new CouponName(
$this->emptyValue
))->toThrow(\DomainException::class);
});
});

View file

@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Enums\BarcodeFormat;
use Domain\Models\ValueObjects\GS1;
covers(GS1::class);
describe('gs1', function (): void {
beforeEach(function (): void {
$this->value = '1010100101';
$this->emptyValue = '';
$this->format = BarcodeFormat::EAN13;
});
it('creates a valid gs1', function (): void {
// WHEN
$GS1 = new GS1(
$this->value,
$this->format
);
// THEN
expect($GS1->value)->toBe($this->value)
->and($GS1->format)->toBe($this->format);
});
it('rejects invalid gs1', function (): void {
// GIVEN / WHEN / THEN
expect(fn (): GS1 => new GS1(
$this->emptyValue,
$this->format
))->toThrow(\DomainException::class);
});
});

View file

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\IncoCode;
covers(IncoCode::class);
describe('inco code', function (): void {
beforeEach(function (): void {
$this->value = 1010100101;
$this->zeroValue = 0;
});
it('creates a valid inco code', function (): void {
// GIVEN / WHEN
$incoCode = new IncoCode(
$this->value
);
// THEN
expect($incoCode->value)->toBe($this->value);
});
it('rejects invalid inco code', function (): void {
// GIVEN / WHEN / THEN
expect(fn (): IncoCode => new IncoCode(
$this->zeroValue
))->toThrow(\DomainException::class);
});
});

View file

@ -11,36 +11,26 @@ use Shared\ValueObjects\Money;
covers(MoneyAmount::class);
describe('money amount', function (): void {
beforeEach(function (): void {
$this->money = new Money(Currency::EUR, 300);
$this->secondMoney = new Money(Currency::EUR, 200);
$this->minimumMoney = new Money(Currency::EUR, 1);
$this->zeroMoney = new Money(Currency::EUR, 0);
$this->aboveLimitMoney = new Money(Currency::EUR, 301);
});
$money = new Money(Currency::EUR, 300);
$secondMoney = new Money(Currency::EUR, 200);
$minimumMoney = new Money(Currency::EUR, 1);
$zeroMoney = new Money(Currency::EUR, 0);
$aboveLimitMoney = new Money(Currency::EUR, 301);
it('wraps a Money value', function (): void {
it('wraps a Money value', function () use ($money): void {
// GIVEN / WHEN
$amount = new MoneyAmount(
$this->money
);
$amount = new MoneyAmount($money);
// THEN
expect($amount)->toBeInstanceOf(MoneyAmount::class)
->and($amount->value)->toBe($this->money);
->and($amount->value)->toBe($money);
});
it('implements equals by delegating to the wrapped Money', function (): void {
it('implements equals by delegating to the wrapped Money', function () use ($money, $secondMoney): void {
// GIVEN / WHEN
$amount1 = new MoneyAmount(
$this->money
);
$amount2 = new MoneyAmount(
$this->money
);
$amount3 = new MoneyAmount(
$this->secondMoney
);
$amount1 = new MoneyAmount($money);
$amount2 = new MoneyAmount($money);
$amount3 = new MoneyAmount($secondMoney);
// THEN
expect($amount1->equals($amount2))->toBeTrue()
@ -48,41 +38,39 @@ describe('money amount', function (): void {
->and($amount1->equals(null))->toBeFalse();
});
it('converts to string by delegating to the wrapped Money', function (): void {
it('converts to string by delegating to the wrapped Money', function () use ($money): void {
// GIVEN / WHEN
$amount = new MoneyAmount(
$this->money
);
$amount = new MoneyAmount($money);
// THEN
expect((string) $amount)->toBe((string) $amount->value);
});
it('accepts the minimum amount of 0.01€', function (): void {
it('accepts the minimum amount of 0.01€', function () use ($minimumMoney): void {
// GIVEN / WHEN
$amount = new MoneyAmount($this->minimumMoney);
$amount = new MoneyAmount($minimumMoney);
// THEN
expect($amount->value)->toBe($this->minimumMoney);
expect($amount->value)->toBe($minimumMoney);
});
it('accepts the maximum amount of 3.00€', function (): void {
it('accepts the maximum amount of 3.00€', function () use ($money): void {
// GIVEN / WHEN
$amount = new MoneyAmount($this->money);
$amount = new MoneyAmount($money);
// THEN
expect($amount->value)->toBe($this->money);
expect($amount->value)->toBe($money);
});
it('throws when the amount is zero', function (): void {
it('throws when the amount is zero', function () use ($zeroMoney): void {
// GIVEN / WHEN / THEN
expect(fn () => new MoneyAmount($this->zeroMoney))
expect(fn (): MoneyAmount => new MoneyAmount($zeroMoney))
->toThrow(\DomainException::class, 'Money amount must be between 0.01€ and 3.00€');
});
it('throws when the amount is above 3.00€', function (): void {
it('throws when the amount is above 3.00€', function () use ($aboveLimitMoney): void {
// GIVEN / WHEN / THEN
expect(fn () => new MoneyAmount($this->aboveLimitMoney))
expect(fn (): MoneyAmount => new MoneyAmount($aboveLimitMoney))
->toThrow(\DomainException::class, 'Money amount must be between 0.01€ and 3.00€');
});
});

View file

@ -9,48 +9,46 @@ use Domain\Models\ValueObjects\PrintRules;
covers(PrintRules::class);
describe('print rules', function (): void {
beforeEach(function (): void {
$this->maxPerCustomer = 3;
$this->minMaxPerCustomer = 1;
$this->zeroMaxPerCustomer = 0;
$this->cooldownDays = 30;
$this->minCooldownDays = 1;
$this->zeroCooldownDays = 0;
});
$maxPerCustomer = 3;
$minMaxPerCustomer = 1;
$zeroMaxPerCustomer = 0;
$cooldownDays = 30;
$minCooldownDays = 1;
$zeroCooldownDays = 0;
it('creates print rules with both fields set', function (): void {
it('creates print rules with both fields set', function () use ($maxPerCustomer, $cooldownDays): void {
// GIVEN / WHEN
$printRules = new PrintRules(
maxPerCustomer: $this->maxPerCustomer,
cooldownDays: $this->cooldownDays
maxPerCustomer: $maxPerCustomer,
cooldownDays: $cooldownDays
);
// THEN
expect($printRules)->toBeInstanceOf(PrintRules::class)
->and($printRules->maxPerCustomer)->toBe($this->maxPerCustomer)
->and($printRules->cooldownDays)->toBe($this->cooldownDays);
->and($printRules->maxPerCustomer)->toBe($maxPerCustomer)
->and($printRules->cooldownDays)->toBe($cooldownDays);
});
it('creates print rules with only maxPerCustomer set', function (): void {
it('creates print rules with only maxPerCustomer set', function () use ($maxPerCustomer): void {
// GIVEN / WHEN
$printRules = new PrintRules(
maxPerCustomer: $this->maxPerCustomer
maxPerCustomer: $maxPerCustomer
);
// THEN
expect($printRules->maxPerCustomer)->toBe($this->maxPerCustomer)
expect($printRules->maxPerCustomer)->toBe($maxPerCustomer)
->and($printRules->cooldownDays)->toBeNull();
});
it('creates print rules with only cooldownDays set', function (): void {
it('creates print rules with only cooldownDays set', function () use ($cooldownDays): void {
// GIVEN / WHEN
$printRules = new PrintRules(
cooldownDays: $this->cooldownDays
cooldownDays: $cooldownDays
);
// THEN
expect($printRules->maxPerCustomer)->toBeNull()
->and($printRules->cooldownDays)->toBe($this->cooldownDays);
->and($printRules->cooldownDays)->toBe($cooldownDays);
});
it('rejects print rules without any field set', function (): void {
@ -59,29 +57,29 @@ describe('print rules', function (): void {
->toThrow(\InvalidArgumentException::class, 'PrintRules require at least a max per customer or a cooldown days.');
});
it('accepts the minimum value of 1 for both fields', function (): void {
it('accepts the minimum value of 1 for both fields', function () use ($minMaxPerCustomer, $minCooldownDays): void {
// GIVEN / WHEN
$printRules = new PrintRules(
maxPerCustomer: $this->minMaxPerCustomer,
cooldownDays: $this->minCooldownDays
maxPerCustomer: $minMaxPerCustomer,
cooldownDays: $minCooldownDays
);
// THEN
expect($printRules->maxPerCustomer)->toBe($this->minMaxPerCustomer)
->and($printRules->cooldownDays)->toBe($this->minCooldownDays);
expect($printRules->maxPerCustomer)->toBe($minMaxPerCustomer)
->and($printRules->cooldownDays)->toBe($minCooldownDays);
});
it('rejects a maxPerCustomer equal to zero', function (): void {
it('rejects a maxPerCustomer equal to zero', function () use ($zeroMaxPerCustomer): void {
// GIVEN / WHEN / THEN
expect(fn (): PrintRules => new PrintRules(
maxPerCustomer: $this->zeroMaxPerCustomer
maxPerCustomer: $zeroMaxPerCustomer
))->toThrow(\InvalidArgumentException::class, 'Max per customer must be greater than 0.');
});
it('rejects a cooldownDays equal to zero', function (): void {
it('rejects a cooldownDays equal to zero', function () use ($zeroCooldownDays): void {
// GIVEN / WHEN / THEN
expect(fn (): PrintRules => new PrintRules(
cooldownDays: $this->zeroCooldownDays
cooldownDays: $zeroCooldownDays
))->toThrow(\InvalidArgumentException::class, 'Cooldown days must be greater than 0.');
});
});

View file

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\ProcessingCode;
covers(ProcessingCode::class);
describe('processing code', function (): void {
beforeEach(function (): void {
$this->value = '1010100101';
$this->emptyValue = '';
});
it('creates a valid processing code', function (): void {
// GIVEN / WHEN
$processingCode = new ProcessingCode(
$this->value
);
// THEN
expect($processingCode->value)->toBe($this->value);
});
it('rejects invalid processing code', function (): void {
// GIVEN / WHEN / THEN
expect(fn (): ProcessingCode => new ProcessingCode(
$this->emptyValue
))->toThrow(\DomainException::class);
});
});

View file

@ -61,7 +61,7 @@ describe('raw picture', function (): void {
it('measures size in raw bytes rather than characters', function (): void {
// GIVEN
$halfLimit = (int) (RawPicturePath::MAX_SIZE_IN_BYTES / 2) + 1;
$halfLimit = RawPicturePath::MAX_SIZE_IN_BYTES / 2 + 1;
$content = str_repeat('é', $halfLimit);
// WHEN

View file

@ -10,22 +10,17 @@ use DateTime;
covers(Period::class);
describe('period', function (): void {
beforeEach(function (): void {
$this->start = new DateTime('2026-01-01');
$this->end = new DateTime('2026-01-31');
});
$start = new DateTime('2026-01-01');
$end = new DateTime('2026-01-31');
it('creates a valid period', function (): void {
it('creates a valid period', function () use ($start, $end): void {
// GIVEN / WHEN
$period = new Period(
$this->start,
$this->end
);
$period = new Period($start, $end);
// THEN
expect($period)->toBeInstanceOf(Period::class)
->and($period->start)->toBe($this->start)
->and($period->end)->toBe($this->end);
->and($period->start)->toBe($start)
->and($period->end)->toBe($end);
});
it('accepts a period whose end equals its start', function (): void {
@ -43,7 +38,7 @@ describe('period', function (): void {
it('throws when the end is before the start', function (): void {
// GIVEN / WHEN / THEN
expect(fn () => new Period(new DateTime('2026-02-01'), new DateTime('2026-01-01')))
expect(fn (): Period => new Period(new DateTime('2026-02-01'), new DateTime('2026-01-01')))
->toThrow(\DomainException::class, 'Period end date cannot be before its start date.');
});
});

View file

@ -9,38 +9,30 @@ use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
covers(RequestedCouponQuantity::class);
describe('quantity', function (): void {
beforeEach(function (): void {
$this->value = 10;
$this->zeroValue = 0;
$this->negativeValue = -1;
});
$value = 10;
$zeroValue = 0;
$negativeValue = -1;
it('creates a valid quantity', function (): void {
it('creates a valid quantity', function () use ($value): void {
// GIVEN / WHEN
$quantity = new RequestedCouponQuantity(
$this->value
);
$quantity = new RequestedCouponQuantity($value);
// THEN
expect($quantity)->toBeInstanceOf(RequestedCouponQuantity::class)
->and($quantity->value)->toBe($this->value);
->and($quantity->value)->toBe($value);
});
it('rejects a quantity of zero', function (): void {
it('rejects a quantity of zero', function () use ($zeroValue): void {
// GIVEN / WHEN
$action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity(
$this->zeroValue
);
$action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity($zeroValue);
// THEN
expect($action)->toThrow(\DomainException::class, 'Quantity should be more than 0.');
});
it('rejects a negative quantity', function (): void {
it('rejects a negative quantity', function () use ($negativeValue): void {
// GIVEN / WHEN
$action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity(
$this->negativeValue
);
$action = fn (): RequestedCouponQuantity => new RequestedCouponQuantity($negativeValue);
// THEN
expect($action)->toThrow(\DomainException::class, 'Quantity should be more than 0.');

View file

@ -4,18 +4,21 @@ declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\RequestedCoupon;
use Carbon\CarbonImmutable;
use DateTime;
use Domain\Enums\BarcodeFormat;
use Domain\Models\ValueObjects\Coupon\CouponValidity;
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
use Domain\Models\ValueObjects\Coupon\CouponName;
use Domain\Models\ValueObjects\Coupon\CouponPicture;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCode;
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
use Domain\Models\ValueObjects\CouponName;
use Domain\Models\ValueObjects\GS1;
use Domain\Models\ValueObjects\IncoCode;
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\Period;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\PrintRules;
use Domain\Models\ValueObjects\ProcessingCode;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCoupon;
use Domain\Models\ValueObjects\RequestedCoupon\RequestedCouponQuantity;
use Shared\Enums\Country;
@ -25,210 +28,265 @@ use Shared\ValueObjects\Money;
covers(RequestedCoupon::class);
describe('requested coupon', function (): void {
beforeEach(function (): void {
$this->name = new CouponName('Coupon A');
$this->quantity = new RequestedCouponQuantity(100);
$this->diffusion = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'));
$this->moneyAmount = new MoneyAmount(new Money(Currency::EUR, 300));
$this->processingCode = new ProcessingCode('PC1');
$this->couponValidity = new CouponValidity(30, new DateTime('2026-02-01'));
$this->franceValidityInformation = new CouponValidityInformation(
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
Country::FRANCE
);
$this->beneluxValidityInformation = new CouponValidityInformation(
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
Country::BENELUX
);
$this->couponValidityInformation = [
$this->franceValidityInformation,
$this->beneluxValidityInformation,
];
$this->products = [new IncoCode(35784), new IncoCode(15522)];
$this->printRules = new PrintRules(maxPerCustomer: 3);
$this->gs1 = new GS1('01234567890128', BarcodeFormat::EAN13);
});
$couponName = new CouponName('Coupon A');
$quantity = new RequestedCouponQuantity(100);
$diffusion = new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01'));
$moneyAmount = new MoneyAmount(new Money(Currency::EUR, 300));
$couponPicture = new CouponPicture(
new PicturePath('files/product.jpg'),
new PicturePath('files/thumbnail.jpg'),
new PicturePath('files/mobileThumbnail.jpg')
);
afterEach(function (): void {
CarbonImmutable::setTestNow();
});
$franceProcessingCode = new CouponProcessingCode('ABC1234567890', Country::FRANCE);
$beneluxProcessingCode = new CouponProcessingCode('XYZ9876543210', Country::BENELUX);
$couponProcessingCodes = new CouponProcessingCodeArray([
$franceProcessingCode,
$beneluxProcessingCode,
]);
it('hydrates a requested coupon with all its fields', function (): void {
$franceValidityInformation = new CouponValidityInformation(
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
Country::FRANCE
);
$beneluxValidityInformation = new CouponValidityInformation(
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
Country::BENELUX
);
$couponValidityInformations = new CouponValidityInformationArray([
$franceValidityInformation,
$beneluxValidityInformation,
]);
$franceLegalInformation = new CouponLegalInformation(
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
Country::FRANCE
);
$beneluxLegalInformation = new CouponLegalInformation(
'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
Country::BENELUX
);
$couponLegalInformations = new CouponLegalInformationArray([
$franceLegalInformation,
$beneluxLegalInformation,
]);
$couponProducts = new CouponProductArray([
new CouponIncoCode(35784),
new CouponIncoCode(15522),
]);
$printRules = new PrintRules(maxPerCustomer: 3);
it('hydrates a requested coupon with all its fields', function () use (
$couponName,
$quantity,
$diffusion,
$moneyAmount,
$couponPicture,
$couponProcessingCodes,
$couponValidityInformations,
$couponLegalInformations,
$couponProducts,
$printRules,
): void {
// GIVEN / WHEN
$coupon = new RequestedCoupon(
$this->name,
$this->quantity,
$this->diffusion,
$this->moneyAmount,
$this->processingCode,
$this->couponValidity,
$this->couponValidityInformation,
$this->products,
$this->printRules,
$this->gs1,
couponName: $couponName,
quantity: $quantity,
diffusion: $diffusion,
moneyAmount: $moneyAmount,
couponPicture: $couponPicture,
couponProcessingCodes: $couponProcessingCodes,
couponValidityInformations: $couponValidityInformations,
couponLegalInformations: $couponLegalInformations,
couponProducts: $couponProducts,
printRules: $printRules,
);
// THEN
expect($coupon->name)->toBe($this->name)
->and($coupon->quantity)->toBe($this->quantity)
->and($coupon->diffusion)->toBe($this->diffusion)
->and($coupon->moneyAmount)->toBe($this->moneyAmount)
->and($coupon->processingCode)->toBe($this->processingCode)
->and($coupon->couponValidity)->toBe($this->couponValidity)
->and($coupon->couponValidityInformation)->toBe($this->couponValidityInformation)
->and($coupon->products)->toBe($this->products)
->and($coupon->printRules)->toBe($this->printRules)
->and($coupon->gs1)->toBe($this->gs1);
expect($coupon->couponName)->toBe($couponName)
->and($coupon->quantity)->toBe($quantity)
->and($coupon->diffusion)->toBe($diffusion)
->and($coupon->moneyAmount)->toBe($moneyAmount)
->and($coupon->couponPicture)->toBe($couponPicture)
->and($coupon->couponProcessingCodes)->toBe($couponProcessingCodes)
->and($coupon->couponValidityInformations)->toBe($couponValidityInformations)
->and($coupon->couponLegalInformations)->toBe($couponLegalInformations)
->and($coupon->couponProducts)->toBe($couponProducts)
->and($coupon->printRules)->toBe($printRules);
});
it('hydrates a requested coupon without products', function (): void {
it('hydrates a requested coupon without products', function () use (
$couponName,
$quantity,
$diffusion,
$moneyAmount,
$couponPicture,
$couponProcessingCodes,
$couponValidityInformations,
$couponLegalInformations,
$printRules,
): void {
// GIVEN / WHEN
$coupon = new RequestedCoupon(
name: $this->name,
quantity: $this->quantity,
diffusion: $this->diffusion,
moneyAmount: $this->moneyAmount,
processingCode: $this->processingCode,
couponValidity: $this->couponValidity,
couponValidityInformation: $this->couponValidityInformation,
printRules: $this->printRules,
gs1: $this->gs1
couponName: $couponName,
quantity: $quantity,
diffusion: $diffusion,
moneyAmount: $moneyAmount,
couponPicture: $couponPicture,
couponProcessingCodes: $couponProcessingCodes,
couponValidityInformations: $couponValidityInformations,
couponLegalInformations: $couponLegalInformations,
couponProducts: new CouponProductArray(),
printRules: $printRules,
);
// THEN
expect($coupon->products)->toBe([])
->and($coupon->printRules)->toBe($this->printRules)
->and($coupon->gs1)->toBe($this->gs1);
expect($coupon->couponProducts->value)->toBe([])
->and($coupon->printRules)->toBe($printRules);
});
it('hydrates a requested coupon without print rules', function (): void {
it('hydrates a requested coupon without print rules', function () use (
$couponName,
$quantity,
$diffusion,
$moneyAmount,
$couponPicture,
$couponProcessingCodes,
$couponValidityInformations,
$couponLegalInformations,
$couponProducts,
): void {
// GIVEN / WHEN
$coupon = new RequestedCoupon(
name: $this->name,
quantity: $this->quantity,
diffusion: $this->diffusion,
moneyAmount: $this->moneyAmount,
processingCode: $this->processingCode,
couponValidity: $this->couponValidity,
couponValidityInformation: $this->couponValidityInformation,
products: $this->products,
gs1: $this->gs1
couponName: $couponName,
quantity: $quantity,
diffusion: $diffusion,
moneyAmount: $moneyAmount,
couponPicture: $couponPicture,
couponProcessingCodes: $couponProcessingCodes,
couponValidityInformations: $couponValidityInformations,
couponLegalInformations: $couponLegalInformations,
couponProducts: $couponProducts,
);
// THEN
expect($coupon->printRules)->toBeNull()
->and($coupon->products)->toBe($this->products)
->and($coupon->gs1)->toBe($this->gs1);
->and($coupon->couponProducts)->toBe($couponProducts);
});
it('hydrates a requested coupon without gs1', function (): void {
it('accepts a france only coupon', function () use (
$couponName,
$quantity,
$diffusion,
$moneyAmount,
$couponPicture,
$franceProcessingCode,
$franceValidityInformation,
$franceLegalInformation,
$couponProducts,
): void {
// GIVEN / WHEN
$coupon = new RequestedCoupon(
name: $this->name,
quantity: $this->quantity,
diffusion: $this->diffusion,
moneyAmount: $this->moneyAmount,
processingCode: $this->processingCode,
couponValidity: $this->couponValidity,
couponValidityInformation: $this->couponValidityInformation,
products: $this->products,
printRules: $this->printRules
couponName: $couponName,
quantity: $quantity,
diffusion: $diffusion,
moneyAmount: $moneyAmount,
couponPicture: $couponPicture,
couponProcessingCodes: new CouponProcessingCodeArray([
$franceProcessingCode,
]),
couponValidityInformations: new CouponValidityInformationArray([
$franceValidityInformation,
]),
couponLegalInformations: new CouponLegalInformationArray([
$franceLegalInformation,
]),
couponProducts: $couponProducts,
);
// THEN
expect($coupon->gs1)->toBeNull()
->and($coupon->products)->toBe($this->products)
->and($coupon->printRules)->toBe($this->printRules);
expect($coupon)->toBeInstanceOf(RequestedCoupon::class);
});
it('hydrates a requested coupon with only required fields', function (): void {
// GIVEN / WHEN
$coupon = new RequestedCoupon(
name: $this->name,
quantity: $this->quantity,
diffusion: $this->diffusion,
moneyAmount: $this->moneyAmount,
processingCode: $this->processingCode,
couponValidity: $this->couponValidity,
couponValidityInformation: $this->couponValidityInformation,
);
// THEN
expect($coupon->products)->toBe([])
->and($coupon->printRules)->toBeNull()
->and($coupon->gs1)->toBeNull();
});
it('throws when there is no validity information for france', function (): void {
it('throws when there is no processing code for france', function () use (
$couponName,
$quantity,
$diffusion,
$moneyAmount,
$couponPicture,
$beneluxProcessingCode,
$couponValidityInformations,
$couponLegalInformations,
$couponProducts,
): void {
// GIVEN / WHEN / THEN
expect(fn () => new RequestedCoupon(
name: $this->name,
quantity: $this->quantity,
diffusion: $this->diffusion,
moneyAmount: $this->moneyAmount,
processingCode: $this->processingCode,
couponValidity: $this->couponValidity,
couponValidityInformation: [],
))->toThrow(\DomainException::class, 'A coupon requires validity information for France.');
expect(fn (): RequestedCoupon => new RequestedCoupon(
couponName: $couponName,
quantity: $quantity,
diffusion: $diffusion,
moneyAmount: $moneyAmount,
couponPicture: $couponPicture,
couponProcessingCodes: new CouponProcessingCodeArray([
$beneluxProcessingCode,
]),
couponValidityInformations: $couponValidityInformations,
couponLegalInformations: $couponLegalInformations,
couponProducts: $couponProducts,
))->toThrow(\DomainException::class, 'CouponProcessingCodes requires a CouponProcessingCode for France.');
});
it('throws when validity information contains an invalid item', function (): void {
it('throws when a benelux processing code has no benelux validity information', function () use (
$couponName,
$quantity,
$diffusion,
$moneyAmount,
$couponPicture,
$couponProcessingCodes,
$franceValidityInformation,
$couponLegalInformations,
$couponProducts,
): void {
// GIVEN / WHEN / THEN
expect(fn () => new RequestedCoupon(
$this->name,
$this->quantity,
$this->diffusion,
$this->moneyAmount,
$this->processingCode,
$this->couponValidity,
['Not a CouponValidityInformation'],
$this->products,
$this->printRules,
$this->gs1
))->toThrow(\DomainException::class, 'Coupon validity information must be CouponValidityInformation instances.');
expect(fn (): RequestedCoupon => new RequestedCoupon(
couponName: $couponName,
quantity: $quantity,
diffusion: $diffusion,
moneyAmount: $moneyAmount,
couponPicture: $couponPicture,
couponProcessingCodes: $couponProcessingCodes,
couponValidityInformations: new CouponValidityInformationArray([
$franceValidityInformation,
]),
couponLegalInformations: $couponLegalInformations,
couponProducts: $couponProducts,
))->toThrow(\DomainException::class, 'A coupon requires validity information for BENELUX.');
});
it('throws when a gs1 coupon has no benelux validity information', function (): void {
it('throws when a benelux processing code has no benelux legal information', function () use (
$couponName,
$quantity,
$diffusion,
$moneyAmount,
$couponPicture,
$couponProcessingCodes,
$couponValidityInformations,
$franceLegalInformation,
$couponProducts,
): void {
// GIVEN / WHEN / THEN
expect(fn () => new RequestedCoupon(
name: $this->name,
quantity: $this->quantity,
diffusion: $this->diffusion,
moneyAmount: $this->moneyAmount,
processingCode: $this->processingCode,
couponValidity: $this->couponValidity,
couponValidityInformation: [$this->franceValidityInformation],
gs1: $this->gs1,
))->toThrow(\DomainException::class, 'A GS1 coupon requires validity information for Benelux.');
});
it('throws when a gs1 coupon has only benelux validity information', function (): void {
// GIVEN / WHEN / THEN
expect(fn () => new RequestedCoupon(
name: $this->name,
quantity: $this->quantity,
diffusion: $this->diffusion,
moneyAmount: $this->moneyAmount,
processingCode: $this->processingCode,
couponValidity: $this->couponValidity,
couponValidityInformation: [$this->beneluxValidityInformation],
gs1: $this->gs1,
))->toThrow(\DomainException::class, 'A coupon requires validity information for France.');
});
it('accepts a coupon without gs1 and without benelux validity information', function (): void {
// GIVEN / WHEN
$coupon = new RequestedCoupon(
name: $this->name,
quantity: $this->quantity,
diffusion: $this->diffusion,
moneyAmount: $this->moneyAmount,
processingCode: $this->processingCode,
couponValidity: $this->couponValidity,
couponValidityInformation: [$this->franceValidityInformation],
);
// THEN
expect($coupon->gs1)->toBeNull();
expect(fn (): RequestedCoupon => new RequestedCoupon(
couponName: $couponName,
quantity: $quantity,
diffusion: $diffusion,
moneyAmount: $moneyAmount,
couponPicture: $couponPicture,
couponProcessingCodes: $couponProcessingCodes,
couponValidityInformations: $couponValidityInformations,
couponLegalInformations: new CouponLegalInformationArray([
$franceLegalInformation,
]),
couponProducts: $couponProducts,
))->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.');
});
});

View file

@ -10,73 +10,71 @@ use Domain\Models\ValueObjects\Stock\StockAdjustment;
covers(StockAdjustment::class);
describe('stock adjustment', function (): void {
beforeEach(function (): void {
$this->quantity = 10;
$this->zeroQuantity = 0;
$this->negativeQuantity = -5;
$this->reason = 'Inventory correction';
});
$quantity = 10;
$zeroQuantity = 0;
$negativeQuantity = -5;
$reason = 'Inventory correction';
it('creates a valid stock adjustment', function (): void {
it('creates a valid stock adjustment', function () use ($quantity, $reason): void {
// GIVEN
$actorId = ActorId::generate();
// WHEN
$adjustment = new StockAdjustment(
$this->quantity,
$this->reason,
$actorId
$quantity,
$reason,
$actorId,
);
// THEN
expect($adjustment)->toBeInstanceOf(StockAdjustment::class)
->and($adjustment->quantity)->toBe($this->quantity)
->and($adjustment->reason)->toBe($this->reason)
->and($adjustment->quantity)->toBe($quantity)
->and($adjustment->reason)->toBe($reason)
->and($adjustment->actorId)->toBe($actorId)
->and($adjustment->createdAt)->toBeNull();
});
it('creates a stock adjustment with an explicit createdAt date', function (): void {
it('creates a stock adjustment with an explicit createdAt date', function () use ($quantity, $reason): void {
// GIVEN
$actorId = ActorId::generate();
$createdAt = new \DateTimeImmutable('2026-06-15 12:00:00', new \DateTimeZone('UTC'));
// WHEN
$adjustment = new StockAdjustment(
$this->quantity,
$this->reason,
$quantity,
$reason,
$actorId,
$createdAt
$createdAt,
);
// THEN
expect($adjustment->createdAt)->toBe($createdAt);
});
it('rejects a quantity of zero', function (): void {
it('rejects a quantity of zero', function () use ($zeroQuantity, $reason): void {
// GIVEN
$actorId = ActorId::generate();
// WHEN
$action = fn (): StockAdjustment => new StockAdjustment(
$this->zeroQuantity,
$this->reason,
$actorId
$zeroQuantity,
$reason,
$actorId,
);
// THEN
expect($action)->toThrow(\DomainException::class, 'Stock Amount should be more than 0.');
});
it('rejects a negative quantity', function (): void {
it('rejects a negative quantity', function () use ($negativeQuantity, $reason): void {
// GIVEN
$actorId = ActorId::generate();
// WHEN
$action = fn (): StockAdjustment => new StockAdjustment(
$this->negativeQuantity,
$this->reason,
$actorId
$negativeQuantity,
$reason,
$actorId,
);
// THEN

View file

@ -9,38 +9,30 @@ use Domain\Models\ValueObjects\Stock\StockAmount;
covers(StockAmount::class);
describe('stock amount', function (): void {
beforeEach(function (): void {
$this->value = 10;
$this->zeroValue = 0;
$this->negativeValue = -1;
});
$value = 10;
$zeroValue = 0;
$negativeValue = -1;
it('creates a valid stock amount', function (): void {
it('creates a valid stock amount', function () use ($value): void {
// GIVEN / WHEN
$amount = new StockAmount(
$this->value
);
$amount = new StockAmount($value);
// THEN
expect($amount)->toBeInstanceOf(StockAmount::class)
->and($amount->value)->toBe($this->value);
->and($amount->value)->toBe($value);
});
it('accepts a zero value', function (): void {
it('accepts a zero value', function () use ($zeroValue): void {
// GIVEN / WHEN
$amount = new StockAmount(
$this->zeroValue
);
$amount = new StockAmount($zeroValue);
// THEN
expect($amount->value)->toBe($this->zeroValue);
expect($amount->value)->toBe($zeroValue);
});
it('rejects a negative value', function (): void {
it('rejects a negative value', function () use ($negativeValue): void {
// GIVEN / WHEN
$action = fn (): StockAmount => new StockAmount(
$this->negativeValue
);
$action = fn (): StockAmount => new StockAmount($negativeValue);
// THEN
expect($action)->toThrow(\DomainException::class, 'Stock Amount should be positive.');

View file

@ -4,9 +4,10 @@ declare(strict_types=1);
namespace Tests\Unit\Shared\Enums;
use Shared\DDD\Exceptions\UnknownCountryIsoCodeException;
use Shared\Enums\Country;
covers(Country::class);
covers(Country::class, UnknownCountryIsoCodeException::class);
describe('country', function (): void {
it('has the expected cases with their backing values', function (): void {
@ -19,4 +20,12 @@ describe('country', function (): void {
// THEN
expect(Country::FRANCE->name)->toBe('FRANCE');
});
it('is a domain exception', function (): void {
// GIVEN / WHEN
$exception = UnknownCountryIsoCodeException::fromIsoCode('XX');
// THEN
expect($exception)->toBeInstanceOf(\DomainException::class);
});
});

View file

@ -250,7 +250,7 @@ describe('dates helper', function (): void {
$invalidInput = 'not-a-valid-date';
// WHEN / THEN
expect(fn () => DatesHelper::parseDateTime($invalidInput))
expect(fn (): \DateTimeInterface => DatesHelper::parseDateTime($invalidInput))
->toThrow(InvalidDateFormatException::class);
});
@ -259,7 +259,7 @@ describe('dates helper', function (): void {
$emptyInput = ' ';
// WHEN / THEN
expect(fn () => DatesHelper::parseDateTime($emptyInput))
expect(fn (): \DateTimeInterface => DatesHelper::parseDateTime($emptyInput))
->toThrow(InvalidDateFormatException::class);
});
});

View file

@ -11,7 +11,7 @@ use Ramsey\Uuid\Uuid;
use Shared\Helpers\Exceptions\InvalidUuidException;
use Shared\Helpers\UuidHelper;
covers(UuidHelper::class);
covers(UuidHelper::class, InvalidUuidException::class);
describe('Uuid Helper Unit Tests', function (): void {
it('prevents direct instantiation but covers constructor', function (): void {
@ -23,7 +23,7 @@ describe('Uuid Helper Unit Tests', function (): void {
$constructor->invoke($reflection->newInstanceWithoutConstructor());
// THEN
expect(fn () => $reflection->newInstance())
expect(fn (): UuidHelper => $reflection->newInstance())
->toThrow(\ReflectionException::class);
});
@ -83,6 +83,7 @@ describe('Uuid Helper Unit Tests', function (): void {
public function unserialize(string $data): void
{
// Keep empty.
}
public function __serialize(): array
@ -92,6 +93,7 @@ describe('Uuid Helper Unit Tests', function (): void {
public function __unserialize(array $data): void
{
// Keep empty.
}
};
@ -127,7 +129,15 @@ describe('Uuid Helper Unit Tests', function (): void {
$uuidV4 = Uuid::uuid4();
// WHEN & THEN
expect(fn () => UuidHelper::toDateTime($uuidV4))
expect(fn (): \DateTimeImmutable => UuidHelper::toDateTime($uuidV4))
->toThrow(InvalidUuidException::class, 'Cannot extract datetime from non-V7 UUID');
});
it('is an invalid argument exception', function (): void {
// GIVEN / WHEN
$exception = InvalidUuidException::forValue('foo');
// THEN
expect($exception)->toBeInstanceOf(\InvalidArgumentException::class);
});
});

View file

@ -46,7 +46,7 @@ describe('PageNumber Value Object Unit Tests', function (): void {
it('rejects page number less than or equal to zero', function (int $invalidValue): void {
// WHEN / THEN
expect(fn () => new PageNumber($invalidValue))
expect(fn (): PageNumber => new PageNumber($invalidValue))
->toThrow(\InvalidArgumentException::class, 'Page number must be greater than 0');
})->with([
'zero' => [0],

View file

@ -54,7 +54,7 @@ describe('PageSize Value Object Unit Tests', function (): void {
it('rejects page size less than or equal to zero', function (int $invalidValue): void {
// WHEN / THEN
expect(fn () => new PageSize($invalidValue))
expect(fn (): PageSize => new PageSize($invalidValue))
->toThrow(\InvalidArgumentException::class, 'Page size must be greater than 0');
})->with([
'zero' => [0],
@ -64,7 +64,7 @@ describe('PageSize Value Object Unit Tests', function (): void {
it('rejects page size exceeding maximum page size', function (): void {
// WHEN / THEN
expect(fn () => new PageSize(PageSize::MAX_PAGE_SIZE + 1))
expect(fn (): PageSize => new PageSize(PageSize::MAX_PAGE_SIZE + 1))
->toThrow(\InvalidArgumentException::class, 'Page size must be less than or equal to 10000');
});