344 lines
15 KiB
PHP
344 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Application\UseCases\Coupon\Create;
|
|
|
|
use Application\UseCases\Coupon\Create\CreateCoupon;
|
|
use Application\UseCases\Coupon\Create\Input;
|
|
use Application\UseCases\Coupon\Create\Output;
|
|
use DateTime;
|
|
use Domain\Models\Entities\Coupon;
|
|
use Domain\Models\ValueObjects\Actor\ActorId;
|
|
use Domain\Models\ValueObjects\Brand\BrandCode;
|
|
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
|
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
|
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
|
use Domain\Models\ValueObjects\Coupon\CouponId;
|
|
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
|
|
use Domain\Models\ValueObjects\Coupon\CouponLegalInformationArray;
|
|
use Domain\Models\ValueObjects\Coupon\CouponMovement;
|
|
use Domain\Models\ValueObjects\Coupon\CouponMovementArray;
|
|
use Domain\Models\ValueObjects\Coupon\CouponName;
|
|
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
|
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
|
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
|
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
|
use Domain\Models\ValueObjects\MoneyAmount;
|
|
use Domain\Models\ValueObjects\Period;
|
|
use Domain\Models\ValueObjects\PicturePath;
|
|
use Domain\Models\ValueObjects\ProcessingCode;
|
|
use Domain\Models\ValueObjects\ProcessingCodeArray;
|
|
use Domain\Ports\Repositories\BrandRepository;
|
|
use Domain\Ports\Repositories\CouponRepository;
|
|
use Shared\DDD\UseCases\Input as BaseInput;
|
|
use Shared\Enums\Country;
|
|
use Shared\Enums\Currency;
|
|
use Shared\ValueObjects\Money;
|
|
|
|
covers(CreateCoupon::class, Input::class, Output::class);
|
|
|
|
describe('create coupon use case', function (): void {
|
|
$makeInput = fn (array $overrides = []): Input => new Input(...array_merge([
|
|
'code' => new CouponCode('COU01'),
|
|
'name' => new CouponName('Coupon name'),
|
|
'amount' => new MoneyAmount(new Money(Currency::EUR, 100)),
|
|
'picture' => new CouponPicture(
|
|
new PicturePath('files/product.jpg'),
|
|
new PicturePath('files/thumbnail.jpg'),
|
|
new PicturePath('files/mobileThumbnail.jpg'),
|
|
),
|
|
'diffusion' => new CouponDiffusion(
|
|
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
|
),
|
|
'validityInformations' => new CouponValidityInformationArray([
|
|
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
|
]),
|
|
'legalInformations' => new CouponLegalInformationArray([
|
|
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
|
]),
|
|
'processingCode' => new ProcessingCodeArray([
|
|
new ProcessingCode('5425003944130', Country::FRANCE),
|
|
]),
|
|
'brandCode' => new BrandCode('TST89'),
|
|
'createdBy' => ActorId::generate(),
|
|
'products' => new CouponProductArray(),
|
|
'movements' => new CouponMovementArray([
|
|
new CouponMovement(100),
|
|
]),
|
|
'availabilityCalendar' => null,
|
|
], $overrides));
|
|
|
|
it('creates a coupon when the brand exists and the code is unique', function () use ($makeInput): void {
|
|
// GIVEN
|
|
$input = $makeInput();
|
|
|
|
$brandRepository = \Mockery::mock(BrandRepository::class);
|
|
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true);
|
|
|
|
$couponRepository = \Mockery::mock(CouponRepository::class);
|
|
$couponRepository->shouldReceive('existsByCode')->once()->with($input->code)->andReturn(false);
|
|
$couponRepository->shouldReceive('save')->once()->with(\Mockery::type(Coupon::class));
|
|
|
|
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
|
|
|
// WHEN
|
|
$result = $useCase->execute($input);
|
|
|
|
// THEN
|
|
expect($result)->toBeInstanceOf(Output::class)
|
|
->and($result->id)->toBeInstanceOf(CouponId::class)
|
|
->and($result->code)->toEqual($input->code)
|
|
->and($result->name)->toEqual($input->name)
|
|
->and($result->amount)->toEqual($input->amount)
|
|
->and($result->picture)->toEqual($input->picture)
|
|
->and($result->diffusion)->toEqual($input->diffusion)
|
|
->and($result->validityInformations)->toEqual($input->validityInformations)
|
|
->and($result->legalInformations)->toEqual($input->legalInformations)
|
|
->and($result->processingCode)->toEqual($input->processingCode)
|
|
->and($result->brandCode)->toEqual($input->brandCode)
|
|
->and($result->createdBy)->toEqual($input->createdBy)
|
|
->and($result->products)->toEqual($input->products)
|
|
->and($result->movements)->toEqual($input->movements)
|
|
->and($result->availabilityCalendar)->toBeNull()
|
|
->and($result->createdAt)->toBeInstanceOf(\DateTimeInterface::class);
|
|
});
|
|
|
|
it('throws when the brand does not exist', function () use ($makeInput): void {
|
|
// GIVEN
|
|
$input = $makeInput();
|
|
|
|
$brandRepository = \Mockery::mock(BrandRepository::class);
|
|
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(false);
|
|
|
|
$couponRepository = \Mockery::mock(CouponRepository::class);
|
|
$couponRepository->shouldNotReceive('existsByCode');
|
|
$couponRepository->shouldNotReceive('save');
|
|
|
|
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
|
|
|
// WHEN
|
|
$action = fn (): Output => $useCase->execute($input);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(\DomainException::class, 'Brand code "TST89" was not found.');
|
|
});
|
|
|
|
it('throws when the coupon code already exists', function () use ($makeInput): void {
|
|
// GIVEN
|
|
$input = $makeInput();
|
|
|
|
$brandRepository = \Mockery::mock(BrandRepository::class);
|
|
$brandRepository->shouldReceive('existsByCode')->once()->with($input->brandCode)->andReturn(true);
|
|
|
|
$couponRepository = \Mockery::mock(CouponRepository::class);
|
|
$couponRepository->shouldReceive('existsByCode')->once()->with($input->code)->andReturn(true);
|
|
$couponRepository->shouldNotReceive('save');
|
|
|
|
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
|
|
|
// WHEN
|
|
$action = fn (): Output => $useCase->execute($input);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(\DomainException::class, 'Coupon code "COU01" already exists.');
|
|
});
|
|
|
|
it('creates a coupon with an availability calendar', function () use ($makeInput): void {
|
|
// GIVEN
|
|
$calendar = new CouponAvailabilityCalendar([
|
|
new Period(new DateTime('2026-01-05'), new DateTime('2026-01-15')),
|
|
new Period(new DateTime('2026-01-20'), new DateTime('2026-01-25')),
|
|
]);
|
|
$input = $makeInput(['availabilityCalendar' => $calendar]);
|
|
|
|
$brandRepository = \Mockery::mock(BrandRepository::class);
|
|
$brandRepository->shouldReceive('existsByCode')->once()->andReturn(true);
|
|
|
|
$couponRepository = \Mockery::mock(CouponRepository::class);
|
|
$couponRepository->shouldReceive('existsByCode')->once()->andReturn(false);
|
|
$couponRepository->shouldReceive('save')->once()->with(\Mockery::on(
|
|
fn (Coupon $saved): bool => $saved->availabilityCalendar === $calendar,
|
|
));
|
|
|
|
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
|
|
|
// WHEN
|
|
$result = $useCase->execute($input);
|
|
|
|
// THEN
|
|
expect($result->availabilityCalendar)->toBe($calendar);
|
|
});
|
|
|
|
it('creates a GS1 coupon with France and Benelux', function () use ($makeInput): void {
|
|
// GIVEN
|
|
$processingCode = new ProcessingCodeArray([
|
|
new ProcessingCode('5425003944130', Country::FRANCE),
|
|
new ProcessingCode('5425003944147', Country::BENELUX),
|
|
]);
|
|
$validityInformations = new CouponValidityInformationArray([
|
|
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
|
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
|
]);
|
|
$legalInformations = new CouponLegalInformationArray([
|
|
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
|
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
|
]);
|
|
$input = $makeInput([
|
|
'processingCode' => $processingCode,
|
|
'validityInformations' => $validityInformations,
|
|
'legalInformations' => $legalInformations,
|
|
]);
|
|
|
|
$brandRepository = \Mockery::mock(BrandRepository::class);
|
|
$brandRepository->shouldReceive('existsByCode')->once()->andReturn(true);
|
|
|
|
$couponRepository = \Mockery::mock(CouponRepository::class);
|
|
$couponRepository->shouldReceive('existsByCode')->once()->andReturn(false);
|
|
$couponRepository->shouldReceive('save')->once()->with(\Mockery::type(Coupon::class));
|
|
|
|
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
|
|
|
// WHEN
|
|
$result = $useCase->execute($input);
|
|
|
|
// THEN
|
|
expect($result->processingCode)->toBe($processingCode)
|
|
->and($result->validityInformations)->toBe($validityInformations)
|
|
->and($result->legalInformations)->toBe($legalInformations);
|
|
});
|
|
|
|
it('rejects a coupon that targets only benelux', function () use ($makeInput): void {
|
|
// GIVEN / WHEN
|
|
$action = fn (): Input => $makeInput([
|
|
'processingCode' => new ProcessingCodeArray([
|
|
new ProcessingCode('5425003944147', Country::BENELUX),
|
|
]),
|
|
'validityInformations' => new CouponValidityInformationArray([
|
|
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
|
]),
|
|
'legalInformations' => new CouponLegalInformationArray([
|
|
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
|
]),
|
|
]);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(\DomainException::class);
|
|
});
|
|
|
|
it('rejects a benelux processing code without benelux legal information', function () use ($makeInput): void {
|
|
// GIVEN
|
|
$input = $makeInput([
|
|
'processingCode' => new ProcessingCodeArray([
|
|
new ProcessingCode('5425003944130', Country::FRANCE),
|
|
new ProcessingCode('5425003944147', Country::BENELUX),
|
|
]),
|
|
'validityInformations' => new CouponValidityInformationArray([
|
|
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
|
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
|
]),
|
|
]);
|
|
|
|
$brandRepository = \Mockery::mock(BrandRepository::class);
|
|
$brandRepository->shouldReceive('existsByCode')->andReturn(true);
|
|
|
|
$couponRepository = \Mockery::mock(CouponRepository::class);
|
|
$couponRepository->shouldReceive('existsByCode')->andReturn(false);
|
|
$couponRepository->shouldNotReceive('save');
|
|
|
|
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
|
|
|
// WHEN
|
|
$action = fn (): Output => $useCase->execute($input);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(\DomainException::class, 'A coupon requires legal information for BENELUX.');
|
|
});
|
|
|
|
it('rejects a benelux processing code without benelux validity information', function () use ($makeInput): void {
|
|
// GIVEN
|
|
$input = $makeInput([
|
|
'processingCode' => new ProcessingCodeArray([
|
|
new ProcessingCode('5425003944130', Country::FRANCE),
|
|
new ProcessingCode('5425003944147', Country::BENELUX),
|
|
]),
|
|
'legalInformations' => new CouponLegalInformationArray([
|
|
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
|
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::BENELUX),
|
|
]),
|
|
]);
|
|
|
|
$brandRepository = \Mockery::mock(BrandRepository::class);
|
|
$brandRepository->shouldReceive('existsByCode')->andReturn(true);
|
|
|
|
$couponRepository = \Mockery::mock(CouponRepository::class);
|
|
$couponRepository->shouldReceive('existsByCode')->andReturn(false);
|
|
$couponRepository->shouldNotReceive('save');
|
|
|
|
$useCase = new CreateCoupon($couponRepository, $brandRepository);
|
|
|
|
// WHEN
|
|
$action = fn (): Output => $useCase->execute($input);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(\DomainException::class, 'A coupon requires validity information for BENELUX.');
|
|
});
|
|
|
|
it('rejects two processing codes for the same country', function () use ($makeInput): void {
|
|
// GIVEN / WHEN
|
|
$action = fn (): Input => $makeInput([
|
|
'processingCode' => new ProcessingCodeArray([
|
|
new ProcessingCode('5425003944130', Country::FRANCE),
|
|
new ProcessingCode('5425003944147', Country::FRANCE),
|
|
]),
|
|
]);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(\DomainException::class);
|
|
});
|
|
|
|
it('rejects two legal informations for the same country', function () use ($makeInput): void {
|
|
// GIVEN / WHEN
|
|
$action = fn (): Input => $makeInput([
|
|
'legalInformations' => new CouponLegalInformationArray([
|
|
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
|
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
|
]),
|
|
]);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(\DomainException::class);
|
|
});
|
|
|
|
it('rejects two validity informations for the same country', function () use ($makeInput): void {
|
|
// GIVEN / WHEN
|
|
$action = fn (): Input => $makeInput([
|
|
'validityInformations' => new CouponValidityInformationArray([
|
|
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
|
new CouponValidityInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FRANCE),
|
|
]),
|
|
]);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(\DomainException::class);
|
|
});
|
|
});
|
|
|
|
describe('create coupon use case assertions', function (): void {
|
|
it('rejects an input of the wrong type', function (): void {
|
|
// GIVEN
|
|
$useCase = new CreateCoupon(
|
|
\Mockery::mock(CouponRepository::class),
|
|
\Mockery::mock(BrandRepository::class),
|
|
);
|
|
$invalidInput = new class () implements BaseInput
|
|
{
|
|
};
|
|
|
|
// WHEN
|
|
$action = fn (): Output => $useCase->execute($invalidInput);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(\InvalidArgumentException::class);
|
|
});
|
|
});
|