From 5c6c1b12226efc4c31518f16884a9fa70641738a Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 12 Oct 2026 17:26:14 +0200 Subject: [PATCH] feat: :sparkles: add ListCoupons Use Case and tests --- .../UseCases/Coupon/Create/CreateCoupon.php | 2 +- .../UseCases/Coupon/List/CouponListItem.php | 25 ++ .../UseCases/Coupon/List/Input.php | 20 ++ .../UseCases/Coupon/List/ListCoupons.php | 53 ++++ .../UseCases/Coupon/List/Output.php | 17 ++ .../Coupon/CouponSortCriteria.php | 14 ++ .../ValueObjects/Coupon/CouponSortField.php | 12 + .../Ports/Repositories/CouponRepository.php | 13 + .../UseCases/Coupon/List/ListCouponTest.php | 231 ++++++++++++++++++ 9 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 src/Application/UseCases/Coupon/List/CouponListItem.php create mode 100644 src/Application/UseCases/Coupon/List/Input.php create mode 100644 src/Application/UseCases/Coupon/List/ListCoupons.php create mode 100644 src/Application/UseCases/Coupon/List/Output.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php create mode 100644 src/Domain/Models/ValueObjects/Coupon/CouponSortField.php create mode 100644 tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php diff --git a/src/Application/UseCases/Coupon/Create/CreateCoupon.php b/src/Application/UseCases/Coupon/Create/CreateCoupon.php index 32409a1..287275e 100644 --- a/src/Application/UseCases/Coupon/Create/CreateCoupon.php +++ b/src/Application/UseCases/Coupon/Create/CreateCoupon.php @@ -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, ); } } diff --git a/src/Application/UseCases/Coupon/List/CouponListItem.php b/src/Application/UseCases/Coupon/List/CouponListItem.php new file mode 100644 index 0000000..2439e48 --- /dev/null +++ b/src/Application/UseCases/Coupon/List/CouponListItem.php @@ -0,0 +1,25 @@ + */ +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(), + ), + ); + } +} diff --git a/src/Application/UseCases/Coupon/List/Output.php b/src/Application/UseCases/Coupon/List/Output.php new file mode 100644 index 0000000..27dc8db --- /dev/null +++ b/src/Application/UseCases/Coupon/List/Output.php @@ -0,0 +1,17 @@ + $coupons */ + public function __construct( + public Paginated $coupons, + ) { + } +} diff --git a/src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php b/src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php new file mode 100644 index 0000000..8385df0 --- /dev/null +++ b/src/Domain/Models/ValueObjects/Coupon/CouponSortCriteria.php @@ -0,0 +1,14 @@ + @@ -17,5 +21,14 @@ interface CouponRepository extends Repository public function findByCode(CouponCode $code): ?Coupon; + /** + * @return Paginated + */ + public function findAll( + Pagination $pagination, + ?CouponSortCriteria $sortCriteria = null, + ?BrandCode $brandCode = null, + ): Paginated; + public function save(Coupon $brand): void; } diff --git a/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php b/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php new file mode 100644 index 0000000..a3106e6 --- /dev/null +++ b/tests/Unit/Application/UseCases/Coupon/List/ListCouponTest.php @@ -0,0 +1,231 @@ +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); + }); +});