Merge branch 'coupon-list' into 'coupon'
feat: ✨ add ListCoupons Use Case and tests
See merge request ldc-web/projets/mesbr!58
This commit is contained in:
commit
c3a3d1cb83
9 changed files with 386 additions and 1 deletions
|
|
@ -73,8 +73,8 @@ final readonly class CreateCoupon implements UseCase, Transactional
|
|||
products: $coupon->products,
|
||||
movements: $coupon->movements,
|
||||
quantity: $coupon->quantity,
|
||||
availabilityCalendar: $coupon->availabilityCalendar,
|
||||
createdAt: $coupon->createdAt(),
|
||||
availabilityCalendar: $coupon->availabilityCalendar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
src/Application/UseCases/Coupon/List/CouponListItem.php
Normal file
25
src/Application/UseCases/Coupon/List/CouponListItem.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\List;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponName;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponPicture;
|
||||
|
||||
final readonly class CouponListItem
|
||||
{
|
||||
public function __construct(
|
||||
public CouponCode $code,
|
||||
public CouponName $name,
|
||||
public BrandCode $brandCode,
|
||||
public CouponPicture $picture,
|
||||
public CouponDiffusion $diffusion,
|
||||
public ?CouponAvailabilityCalendar $availabilityCalendar = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
20
src/Application/UseCases/Coupon/List/Input.php
Normal file
20
src/Application/UseCases/Coupon/List/Input.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\List;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponSortCriteria;
|
||||
use Shared\DDD\UseCases\Input as UseCaseInput;
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
|
||||
final readonly class Input implements UseCaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public Pagination $pagination,
|
||||
public ?CouponSortCriteria $sort = null,
|
||||
public ?BrandCode $brandCode = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
53
src/Application/UseCases/Coupon/List/ListCoupons.php
Normal file
53
src/Application/UseCases/Coupon/List/ListCoupons.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\List;
|
||||
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\DDD\UseCases\UseCase;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements UseCase<\Application\UseCases\Coupon\List\Input, \Application\UseCases\Coupon\List\Output> */
|
||||
final readonly class ListCoupons implements UseCase
|
||||
{
|
||||
public function __construct(
|
||||
private CouponRepository $repository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$result = $this->repository->findAll(
|
||||
pagination: $input->pagination,
|
||||
sortCriteria: $input->sort,
|
||||
brandCode: $input->brandCode,
|
||||
);
|
||||
|
||||
$items = array_map(
|
||||
static fn (Coupon $coupon): CouponListItem => new CouponListItem(
|
||||
code: $coupon->code,
|
||||
name: $coupon->name,
|
||||
brandCode: $coupon->brandCode,
|
||||
picture: $coupon->picture,
|
||||
diffusion: $coupon->diffusion,
|
||||
availabilityCalendar: $coupon->availabilityCalendar,
|
||||
),
|
||||
$result->getItems(),
|
||||
);
|
||||
|
||||
return new Output(
|
||||
coupons: new Paginated(
|
||||
pagination: $result->getPagination(),
|
||||
items: $items,
|
||||
total: $result->getTotal(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/Application/UseCases/Coupon/List/Output.php
Normal file
17
src/Application/UseCases/Coupon/List/Output.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\UseCases\Coupon\List;
|
||||
|
||||
use Shared\DDD\UseCases\Output as UseCaseOutput;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
|
||||
final readonly class Output implements UseCaseOutput
|
||||
{
|
||||
/** @param Paginated<CouponListItem> $coupons */
|
||||
public function __construct(
|
||||
public Paginated $coupons,
|
||||
) {
|
||||
}
|
||||
}
|
||||
14
src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php
Normal file
14
src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
final readonly class CouponSortCriteria
|
||||
{
|
||||
public function __construct(
|
||||
public CouponSortField $field = CouponSortField::CreatedAt,
|
||||
public \SortDirection $direction = \SortDirection::Ascending,
|
||||
) {
|
||||
}
|
||||
}
|
||||
12
src/Domain/Models/ValueObjects/Coupon/CouponSortField.php
Normal file
12
src/Domain/Models/ValueObjects/Coupon/CouponSortField.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Coupon;
|
||||
|
||||
enum CouponSortField: string
|
||||
{
|
||||
case BrandCode = 'brand_code';
|
||||
case Diffusion = 'diffusion';
|
||||
case CreatedAt = 'created_at';
|
||||
}
|
||||
|
|
@ -5,8 +5,12 @@ declare(strict_types=1);
|
|||
namespace Domain\Ports\Repositories;
|
||||
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponSortCriteria;
|
||||
use Shared\DDD\Ports\Repository;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
|
||||
/**
|
||||
* @extends \Shared\DDD\Ports\Repository<\Domain\Models\ValueObjects\Coupon\CouponId, \Domain\Models\Entities\Coupon>
|
||||
|
|
@ -17,5 +21,14 @@ interface CouponRepository extends Repository
|
|||
|
||||
public function findByCode(CouponCode $code): ?Coupon;
|
||||
|
||||
/**
|
||||
* @return Paginated<Coupon>
|
||||
*/
|
||||
public function findAll(
|
||||
Pagination $pagination,
|
||||
?CouponSortCriteria $sortCriteria = null,
|
||||
?BrandCode $brandCode = null,
|
||||
): Paginated;
|
||||
|
||||
public function save(Coupon $brand): void;
|
||||
}
|
||||
|
|
|
|||
231
tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php
Normal file
231
tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\UseCases\Coupon\List;
|
||||
|
||||
use Application\UseCases\Coupon\List\CouponListItem;
|
||||
use Application\UseCases\Coupon\List\Input;
|
||||
use Application\UseCases\Coupon\List\ListCoupons;
|
||||
use Application\UseCases\Coupon\List\Output;
|
||||
use DateTime;
|
||||
use Domain\Models\Entities\Coupon;
|
||||
use Domain\Models\ValueObjects\Actor\ActorId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponId;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponIncoCode;
|
||||
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\CouponProcessingCode;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProcessingCodeArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponProductArray;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponQuantity;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponSortCriteria;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponSortField;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformation;
|
||||
use Domain\Models\ValueObjects\Coupon\CouponValidityInformationArray;
|
||||
use Domain\Models\ValueObjects\MoneyAmount;
|
||||
use Domain\Models\ValueObjects\Period;
|
||||
use Domain\Models\ValueObjects\PicturePath;
|
||||
use Domain\Ports\Repositories\CouponRepository;
|
||||
use Shared\DDD\UseCases\Input as BaseInput;
|
||||
use Shared\Enums\Country;
|
||||
use Shared\Enums\Currency;
|
||||
use Shared\ValueObjects\Money;
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
|
||||
covers(ListCoupons::class, Input::class, Output::class, CouponListItem::class);
|
||||
|
||||
describe('list coupons use case', function (): void {
|
||||
$coupon1 = Coupon::hydrate(
|
||||
id: CouponId::generate(),
|
||||
code: new CouponCode('AAA01'),
|
||||
name: new CouponName('Coupon 1'),
|
||||
amount: new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
picture: new CouponPicture(
|
||||
new PicturePath('files/product-1.jpg'),
|
||||
new PicturePath('files/thumbnail-1.jpg'),
|
||||
new PicturePath('files/mobileThumbnail-1.jpg')
|
||||
),
|
||||
diffusion: new CouponDiffusion(new Period(
|
||||
new DateTime('2026-01-01'),
|
||||
new DateTime('2026-02-01'),
|
||||
)),
|
||||
validityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet.', Country::FRANCE),
|
||||
]),
|
||||
legalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet.', Country::FRANCE),
|
||||
]),
|
||||
processingCode: new CouponProcessingCodeArray([
|
||||
new CouponProcessingCode('ABC1234567890', Country::FRANCE),
|
||||
]),
|
||||
brandCode: new BrandCode('AAA01'),
|
||||
createdBy: ActorId::generate(),
|
||||
products: new CouponProductArray([new CouponIncoCode(35784)]),
|
||||
movements: new CouponMovementArray([new CouponMovement(100)]),
|
||||
quantity: new CouponQuantity(100),
|
||||
availabilityCalendar: new CouponAvailabilityCalendar([
|
||||
new Period(new DateTime('2026-01-01'), new DateTime('2026-02-01')),
|
||||
]),
|
||||
);
|
||||
$coupon2 = Coupon::hydrate(
|
||||
id: CouponId::generate(),
|
||||
code: new CouponCode('BBB02'),
|
||||
name: new CouponName('Coupon 2'),
|
||||
amount: new MoneyAmount(new Money(Currency::EUR, 300)),
|
||||
picture: new CouponPicture(
|
||||
new PicturePath('files/product-2.jpg'),
|
||||
new PicturePath('files/thumbnail-2.jpg'),
|
||||
new PicturePath('files/mobileThumbnail-2.jpg')
|
||||
),
|
||||
diffusion: new CouponDiffusion(new Period(
|
||||
new DateTime('2027-01-01'),
|
||||
new DateTime('2027-02-01'),
|
||||
)),
|
||||
validityInformations: new CouponValidityInformationArray([
|
||||
new CouponValidityInformation('Lorem ipsum dolor sit amet.', Country::FRANCE),
|
||||
]),
|
||||
legalInformations: new CouponLegalInformationArray([
|
||||
new CouponLegalInformation('Lorem ipsum dolor sit amet.', Country::FRANCE),
|
||||
]),
|
||||
processingCode: new CouponProcessingCodeArray([
|
||||
new CouponProcessingCode('DEF0123456789', Country::FRANCE),
|
||||
]),
|
||||
brandCode: new BrandCode('BBB02'),
|
||||
createdBy: ActorId::generate(),
|
||||
products: new CouponProductArray([new CouponIncoCode(35784)]),
|
||||
movements: new CouponMovementArray([new CouponMovement(100)]),
|
||||
quantity: new CouponQuantity(100),
|
||||
);
|
||||
$pagination = Pagination::firstPage();
|
||||
$brandCode = new BrandCode('AAA01');
|
||||
|
||||
it('maps repository coupons to list items, preserving pagination and sort', function () use (
|
||||
$coupon1,
|
||||
$coupon2,
|
||||
$pagination,
|
||||
$brandCode,
|
||||
): void {
|
||||
// GIVEN
|
||||
$paginated = new Paginated($pagination, [$coupon1, $coupon2], 2);
|
||||
$sort = new CouponSortCriteria();
|
||||
$input = new Input(pagination: $pagination, sort: $sort, brandCode: $brandCode);
|
||||
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($pagination, CouponSortCriteria::class, $brandCode)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCoupons($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class);
|
||||
|
||||
$items = $result->coupons->getItems();
|
||||
expect($items)->toHaveCount(2)
|
||||
->and($items[0])->toBeInstanceOf(CouponListItem::class)
|
||||
->and($items[0]->code)->toBe($coupon1->code)
|
||||
->and($items[0]->name)->toBe($coupon1->name)
|
||||
->and($items[0]->brandCode)->toBe($coupon1->brandCode)
|
||||
->and($items[0]->picture)->toBe($coupon1->picture)
|
||||
->and($items[0]->diffusion)->toBe($coupon1->diffusion)
|
||||
->and($items[0]->availabilityCalendar)->toBe($coupon1->availabilityCalendar)
|
||||
->and($items[1]->code)->toBe($coupon2->code)
|
||||
->and($items[1]->brandCode)->toBe($coupon2->brandCode)
|
||||
->and($items[1]->availabilityCalendar)->toBeNull()
|
||||
->and($result->coupons->getTotal())->toBe(2)
|
||||
->and($result->coupons->getPagination())->toBe($pagination);
|
||||
});
|
||||
|
||||
it('maps repository coupons to list items, preserving specific pagination and sort', function () use (
|
||||
$coupon1,
|
||||
$coupon2,
|
||||
$pagination,
|
||||
): void {
|
||||
// GIVEN
|
||||
$paginated = new Paginated($pagination, [$coupon2, $coupon1], 2);
|
||||
$sort = new CouponSortCriteria(field: CouponSortField::BrandCode, direction: \SortDirection::Descending);
|
||||
$input = new Input(pagination: $pagination, sort: $sort);
|
||||
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($pagination, CouponSortCriteria::class, null)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCoupons($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class);
|
||||
|
||||
$items = $result->coupons->getItems();
|
||||
expect($items)->toHaveCount(2)
|
||||
->and($items[1])->toBeInstanceOf(CouponListItem::class)
|
||||
->and($items[1]->code)->toBe($coupon1->code)
|
||||
->and($items[1]->name)->toBe($coupon1->name)
|
||||
->and($items[1]->brandCode)->toBe($coupon1->brandCode)
|
||||
->and($items[1]->picture)->toBe($coupon1->picture)
|
||||
->and($items[1]->diffusion)->toBe($coupon1->diffusion)
|
||||
->and($items[1]->availabilityCalendar)->toBe($coupon1->availabilityCalendar)
|
||||
->and($items[0]->code)->toBe($coupon2->code)
|
||||
->and($items[0]->brandCode)->toBe($coupon2->brandCode)
|
||||
->and($items[0]->availabilityCalendar)->toBeNull()
|
||||
->and($result->coupons->getTotal())->toBe(2)
|
||||
->and($result->coupons->getPagination())->toBe($pagination);
|
||||
});
|
||||
|
||||
it('returns an empty list without error when there are no coupons', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage();
|
||||
$paginated = new Paginated($pagination, [], 0);
|
||||
$input = new Input(pagination: $pagination);
|
||||
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$repository->shouldReceive('findAll')
|
||||
->once()
|
||||
->with($pagination, null, null)
|
||||
->andReturn($paginated);
|
||||
|
||||
$useCase = new ListCoupons($repository);
|
||||
|
||||
// WHEN
|
||||
$result = $useCase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result->coupons->getItems())->toBe([])
|
||||
->and($result->coupons->getTotal())->toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list coupons use case assertions', function (): void {
|
||||
it('rejects an input of the wrong type', function (): void {
|
||||
// GIVEN
|
||||
$repository = \Mockery::mock(CouponRepository::class);
|
||||
$useCase = new ListCoupons($repository);
|
||||
$invalidInput = new class () implements BaseInput
|
||||
{
|
||||
};
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Output => $useCase->execute($invalidInput);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(\InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue