feat(brand): add Brand use cases, API v1 endpoints, console command, OpenAPI spec and Bruno collection
This commit is contained in:
parent
20ec692160
commit
1f8f03e6d8
37 changed files with 1734 additions and 0 deletions
20
documentation/Bruno/Arcadia Api/Api/V1/Brands/Get Brand.yml
Normal file
20
documentation/Bruno/Arcadia Api/Api/V1/Brands/Get Brand.yml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
info:
|
||||
name: Get Brand
|
||||
type: http
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{ENDPOINT}}/api/{{APIV1}}/brands/:identifier"
|
||||
params:
|
||||
- name: identifier
|
||||
value: "{{BRAND_IDENTIFIER}}"
|
||||
type: path
|
||||
description: "Brand UUID v7 or public code (e.g. LGL01)"
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
info:
|
||||
name: List Brands
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: "{{ENDPOINT}}/api/{{APIV1}}/brands?page=1&pageSize=25"
|
||||
params:
|
||||
- name: page
|
||||
value: "1"
|
||||
type: query
|
||||
description: 1-based page index
|
||||
- name: pageSize
|
||||
value: "25"
|
||||
type: query
|
||||
description: Number of items per page (max 10000)
|
||||
- name: offset
|
||||
value: "0"
|
||||
type: query
|
||||
description: Offset for offset-based pagination
|
||||
disabled: true
|
||||
- name: limit
|
||||
value: "25"
|
||||
type: query
|
||||
description: Limit for offset-based pagination
|
||||
disabled: true
|
||||
auth: inherit
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
7
documentation/Bruno/Arcadia Api/Api/V1/Brands/folder.yml
Normal file
7
documentation/Bruno/Arcadia Api/Api/V1/Brands/folder.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
info:
|
||||
name: Brands
|
||||
type: folder
|
||||
seq: 2
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
|
|
@ -12,6 +12,9 @@ variables:
|
|||
- name: INCOSHEET_ID
|
||||
value: "1"
|
||||
description: Arcadia ID of an IncoSheet
|
||||
- name: BRAND_IDENTIFIER
|
||||
value: LGL01
|
||||
description: Brand public code (e.g. LGL01) or UUID
|
||||
- name: CLIENT_IDENTIFIER
|
||||
value: arcadia_id_00000000000000000000000000000001
|
||||
description: API Client Identifier for HMAC Authentication
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ security:
|
|||
tags:
|
||||
- name: Employees
|
||||
description: Gestion et consultation du référentiel RH des employés.
|
||||
- name: Brands
|
||||
description: Consultation et référentiel des marques.
|
||||
- name: IncoSheets
|
||||
description: Consultation et recherche des fiches produits INCO.
|
||||
- name: IncoComposition
|
||||
|
|
@ -149,6 +151,78 @@ paths:
|
|||
"503":
|
||||
$ref: "#/components/responses/ServiceUnavailable"
|
||||
|
||||
/brands:
|
||||
get:
|
||||
tags:
|
||||
- Brands
|
||||
summary: Liste de toutes les marques
|
||||
description: |
|
||||
Retourne la liste de toutes les marques connues du système Arcadia.
|
||||
Les marques non encore enregistrées localement sont automatiquement initialisées
|
||||
et pourvues d'un code public unique (trigramme + 2 chiffres).
|
||||
operationId: listBrands
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PageSize"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
description: Liste des marques récupérée avec succès.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BrandListResponse"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"422":
|
||||
$ref: "#/components/responses/UnprocessableEntity"
|
||||
"429":
|
||||
$ref: "#/components/responses/TooManyRequests"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalServerError"
|
||||
"503":
|
||||
$ref: "#/components/responses/ServiceUnavailable"
|
||||
|
||||
/brands/{identifier}:
|
||||
get:
|
||||
tags:
|
||||
- Brands
|
||||
summary: Détail d'une marque par son identifiant UUID ou code public
|
||||
description: |
|
||||
Retourne la fiche d'une marque spécifiée soit par son identifiant unique UUID v7,
|
||||
soit par son code public (ex: LGL01).
|
||||
operationId: getBrand
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/BrandIdentifierPath"
|
||||
responses:
|
||||
"200":
|
||||
description: Fiche marque trouvée.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BrandItemResponse"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"422":
|
||||
$ref: "#/components/responses/UnprocessableEntity"
|
||||
"429":
|
||||
$ref: "#/components/responses/TooManyRequests"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalServerError"
|
||||
"503":
|
||||
$ref: "#/components/responses/ServiceUnavailable"
|
||||
|
||||
/inco-sheets/search:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -276,6 +350,15 @@ components:
|
|||
description: Signature HMAC-SHA256 calculée avec la clé secrète partagée.
|
||||
|
||||
parameters:
|
||||
BrandIdentifierPath:
|
||||
name: identifier
|
||||
in: path
|
||||
required: true
|
||||
description: Identifiant UUID v7 ou code public (ex. LGL01) de la marque.
|
||||
schema:
|
||||
type: string
|
||||
example: "LGL01"
|
||||
|
||||
EmployeeIdPath:
|
||||
name: employeeId
|
||||
in: path
|
||||
|
|
@ -542,6 +625,64 @@ components:
|
|||
$ref: "#/components/schemas/ProblemDetails"
|
||||
|
||||
schemas:
|
||||
Brand:
|
||||
type: object
|
||||
description: Représentation canonique d'une marque dans Arcadia API.
|
||||
required:
|
||||
- id
|
||||
- code
|
||||
- name
|
||||
- arcadiaId
|
||||
- createdAt
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Identifiant UUID v7 unique de la marque.
|
||||
example: "01918a3c-1b77-7000-8000-000000000001"
|
||||
code:
|
||||
type: string
|
||||
pattern: '^[A-Z]{3}\d{2}$'
|
||||
description: Code public unique trigramme + 2 chiffres.
|
||||
example: "LGL01"
|
||||
name:
|
||||
type: string
|
||||
description: Nom / libellé de la marque.
|
||||
example: "Le Gaulois"
|
||||
arcadiaId:
|
||||
type: integer
|
||||
description: Identifiant numérique amont Arcadia.
|
||||
example: 1
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Date et heure de création (ISO 8601).
|
||||
example: "2024-01-15T08:00:00Z"
|
||||
|
||||
BrandListResponse:
|
||||
type: object
|
||||
description: Enveloppe de réponse pour la liste paginée des marques.
|
||||
required:
|
||||
- brands
|
||||
- pagination
|
||||
properties:
|
||||
brands:
|
||||
type: array
|
||||
description: Collection des marques de la page.
|
||||
items:
|
||||
$ref: "#/components/schemas/Brand"
|
||||
pagination:
|
||||
$ref: "#/components/schemas/PaginationMeta"
|
||||
|
||||
BrandItemResponse:
|
||||
type: object
|
||||
description: Enveloppe de réponse pour le détail d'une marque.
|
||||
required:
|
||||
- brand
|
||||
properties:
|
||||
brand:
|
||||
$ref: "#/components/schemas/Brand"
|
||||
|
||||
Employee:
|
||||
type: object
|
||||
description: Représentation canonique d'un employé dans le métamodèle Arcadia.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Presentation\Api\V1\Http\Controllers\Brands\BrandShowController;
|
||||
use Presentation\Api\V1\Http\Controllers\Brands\BrandsIndexController;
|
||||
use Presentation\Api\V1\Http\Controllers\Employees\EmployeeShowController;
|
||||
use Presentation\Api\V1\Http\Controllers\Employees\EmployeesIndexController;
|
||||
use Presentation\Api\V1\Http\Controllers\IncoCompositions\IncoCompositionShowController;
|
||||
|
|
@ -42,6 +44,12 @@ Route::middleware('auth.hmac')->group(function (): void {
|
|||
Route::get('/{employeeId}', EmployeeShowController::class)->name('employees.show');
|
||||
});
|
||||
|
||||
// * BRANDS
|
||||
Route::prefix('brands')->group(function (): void {
|
||||
Route::get('/', BrandsIndexController::class)->name('brands.index');
|
||||
Route::get('/{identifier}', BrandShowController::class)->name('brands.show');
|
||||
});
|
||||
|
||||
// * INCO SHEETS
|
||||
Route::prefix('inco-sheets')->group(function (): void {
|
||||
Route::get('/', IncoSheetsIndexController::class)->name('inco-sheets.index');
|
||||
|
|
|
|||
67
src/Application/Arcadia/Usecase/Brands/Find/FindBrand.php
Normal file
67
src/Application/Arcadia/Usecase/Brands/Find/FindBrand.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Arcadia\Usecase\Brands\Find;
|
||||
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Shared\DDD\Usecase\Input as BaseInput;
|
||||
use Shared\DDD\Usecase\Usecase;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements Usecase<\Application\Arcadia\Usecase\Brands\Find\Input, \Application\Arcadia\Usecase\Brands\Find\Output> */
|
||||
final readonly class FindBrand implements Usecase
|
||||
{
|
||||
public function __construct(
|
||||
private BrandRepository $repository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
return match (true) {
|
||||
$input->identifier instanceof BrandId => $this->findById($input->identifier),
|
||||
$input->identifier instanceof BrandCode => $this->findByCode($input->identifier),
|
||||
};
|
||||
}
|
||||
|
||||
private function findById(BrandId $id): Output
|
||||
{
|
||||
$brand = $this->repository->find($id);
|
||||
|
||||
if ($brand === null) {
|
||||
throw BrandNotFoundException::forBrand($id);
|
||||
}
|
||||
|
||||
return new Output(
|
||||
id: $brand->id(),
|
||||
name: $brand->name,
|
||||
code: $brand->code,
|
||||
arcadiaId: $brand->arcadiaId,
|
||||
createdAt: $brand->createdAt(),
|
||||
);
|
||||
}
|
||||
|
||||
private function findByCode(BrandCode $code): Output
|
||||
{
|
||||
$brand = $this->repository->findByCode($code);
|
||||
|
||||
if ($brand === null) {
|
||||
throw BrandNotFoundException::forBrandCode($code);
|
||||
}
|
||||
|
||||
return new Output(
|
||||
id: $brand->id(),
|
||||
name: $brand->name,
|
||||
code: $brand->code,
|
||||
arcadiaId: $brand->arcadiaId,
|
||||
createdAt: $brand->createdAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
27
src/Application/Arcadia/Usecase/Brands/Find/Input.php
Normal file
27
src/Application/Arcadia/Usecase/Brands/Find/Input.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Arcadia\Usecase\Brands\Find;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Shared\DDD\Usecase\Input as UsecaseInput;
|
||||
|
||||
final readonly class Input implements UsecaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public BrandId | BrandCode $identifier,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function byId(BrandId $id): self
|
||||
{
|
||||
return new self($id);
|
||||
}
|
||||
|
||||
public static function byCode(BrandCode $code): self
|
||||
{
|
||||
return new self($code);
|
||||
}
|
||||
}
|
||||
23
src/Application/Arcadia/Usecase/Brands/Find/Output.php
Normal file
23
src/Application/Arcadia/Usecase/Brands/Find/Output.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Arcadia\Usecase\Brands\Find;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Shared\DDD\Usecase\Output as UsecaseOutput;
|
||||
|
||||
final readonly class Output implements UsecaseOutput
|
||||
{
|
||||
public function __construct(
|
||||
public BrandId $id,
|
||||
public BrandName $name,
|
||||
public BrandCode $code,
|
||||
public ArcadiaId $arcadiaId,
|
||||
public \DateTimeInterface $createdAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
34
src/Application/Arcadia/Usecase/Brands/FindAll/BrandItem.php
Normal file
34
src/Application/Arcadia/Usecase/Brands/FindAll/BrandItem.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Arcadia\Usecase\Brands\FindAll;
|
||||
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
|
||||
final readonly class BrandItem
|
||||
{
|
||||
public function __construct(
|
||||
public BrandId $id,
|
||||
public BrandName $name,
|
||||
public BrandCode $code,
|
||||
public ArcadiaId $arcadiaId,
|
||||
public \DateTimeInterface $createdAt,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromDomain(Brand $brand): self
|
||||
{
|
||||
return new self(
|
||||
id: $brand->id(),
|
||||
name: $brand->name,
|
||||
code: $brand->code,
|
||||
arcadiaId: $brand->arcadiaId,
|
||||
createdAt: $brand->createdAt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Arcadia\Usecase\Brands\FindAll;
|
||||
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Services\BrandResolver;
|
||||
use Shared\DDD\Usecase\Input as BaseInput;
|
||||
use Shared\DDD\Usecase\Usecase;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements Usecase<\Application\Arcadia\Usecase\Brands\FindAll\Input, \Application\Arcadia\Usecase\Brands\FindAll\Output> */
|
||||
final readonly class FindAllBrands implements Usecase
|
||||
{
|
||||
public function __construct(
|
||||
private BrandResolver $brandResolver,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function execute(BaseInput $input): Output
|
||||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$brands = $this->brandResolver->resolveAll();
|
||||
|
||||
$brandItems = array_map(
|
||||
static fn (Brand $brand): BrandItem => BrandItem::fromDomain($brand),
|
||||
$brands
|
||||
);
|
||||
|
||||
$total = count($brandItems);
|
||||
$offset = $input->pagination->from();
|
||||
$limit = $input->pagination->pageSize();
|
||||
$paged = array_values(array_slice($brandItems, $offset, $limit));
|
||||
|
||||
return new Output(
|
||||
brands: $paged,
|
||||
total: $total,
|
||||
page: $input->pagination->pageNumber(),
|
||||
perPage: $input->pagination->pageSize(),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/Application/Arcadia/Usecase/Brands/FindAll/Input.php
Normal file
16
src/Application/Arcadia/Usecase/Brands/FindAll/Input.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Arcadia\Usecase\Brands\FindAll;
|
||||
|
||||
use Shared\DDD\Usecase\Input as UsecaseInput;
|
||||
use Shared\Pagination;
|
||||
|
||||
final readonly class Input implements UsecaseInput
|
||||
{
|
||||
public function __construct(
|
||||
public Pagination $pagination,
|
||||
) {
|
||||
}
|
||||
}
|
||||
21
src/Application/Arcadia/Usecase/Brands/FindAll/Output.php
Normal file
21
src/Application/Arcadia/Usecase/Brands/FindAll/Output.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Arcadia\Usecase\Brands\FindAll;
|
||||
|
||||
use Shared\DDD\Usecase\Output as UsecaseOutput;
|
||||
|
||||
final readonly class Output implements UsecaseOutput
|
||||
{
|
||||
/**
|
||||
* @param list<BrandItem> $brands
|
||||
*/
|
||||
public function __construct(
|
||||
public array $brands,
|
||||
public int $total,
|
||||
public int $page,
|
||||
public int $perPage,
|
||||
) {
|
||||
}
|
||||
}
|
||||
51
src/Presentation/Api/V1/Dto/Brands/BrandDto.php
Normal file
51
src/Presentation/Api/V1/Dto/Brands/BrandDto.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Dto\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\BrandItem;
|
||||
use Presentation\Api\V1\Contracts\ApiResponseDto;
|
||||
use Presentation\Api\V1\Presenters\Brands\BrandShowView;
|
||||
|
||||
final readonly class BrandDto implements ApiResponseDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $code,
|
||||
public string $name,
|
||||
public int $arcadiaId,
|
||||
public string $createdAt,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromView(BrandShowView $view): self
|
||||
{
|
||||
return new self(
|
||||
id: $view->id->value,
|
||||
code: $view->code->value,
|
||||
name: $view->name->value,
|
||||
arcadiaId: $view->arcadiaId->value,
|
||||
createdAt: $view->createdAt->format(\DateTimeInterface::ATOM),
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromBrandItem(BrandItem $item): self
|
||||
{
|
||||
return new self(
|
||||
id: $item->id->value,
|
||||
code: $item->code->value,
|
||||
name: $item->name->value,
|
||||
arcadiaId: $item->arcadiaId->value,
|
||||
createdAt: $item->createdAt->format(\DateTimeInterface::ATOM),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
#[\Override]
|
||||
public function toArray(): array
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
return get_object_vars($this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Http\Controllers\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\Find\FindBrand;
|
||||
use Application\Arcadia\Usecase\Brands\Find\Input;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Http\Requests\Brands\BrandShowRequest;
|
||||
use Presentation\Api\V1\Http\Responses\Brands\BrandShowResponse;
|
||||
use Presentation\Api\V1\Presenters\Brands\BrandShowPresenter;
|
||||
use Shared\DDD\Usecase\Builder\UsecaseBuilder;
|
||||
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
|
||||
|
||||
final readonly class BrandShowController
|
||||
{
|
||||
public function __construct(
|
||||
private FindBrand $usecase,
|
||||
private UsecaseExecutor $executor,
|
||||
private BrandShowPresenter $presenter,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(BrandShowRequest $request): ApiResponse
|
||||
{
|
||||
$input = $this->inputFrom($request);
|
||||
|
||||
$response = UsecaseBuilder::using($this->executor)
|
||||
->do($this->usecase)
|
||||
->from($input)
|
||||
->with($this->presenter)
|
||||
->run();
|
||||
|
||||
return new BrandShowResponse($response);
|
||||
}
|
||||
|
||||
private function inputFrom(BrandShowRequest $request): Input
|
||||
{
|
||||
return new Input(
|
||||
identifier: $request->identifier(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Http\Controllers\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\FindAllBrands;
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\Input;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Http\Requests\Brands\BrandsIndexRequest;
|
||||
use Presentation\Api\V1\Http\Responses\Brands\BrandsIndexResponse;
|
||||
use Presentation\Api\V1\Presenters\Brands\BrandsIndexPresenter;
|
||||
use Shared\DDD\Usecase\Builder\UsecaseBuilder;
|
||||
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
|
||||
|
||||
final readonly class BrandsIndexController
|
||||
{
|
||||
public function __construct(
|
||||
private FindAllBrands $usecase,
|
||||
private UsecaseExecutor $executor,
|
||||
private BrandsIndexPresenter $presenter,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(BrandsIndexRequest $request): ApiResponse
|
||||
{
|
||||
$input = $this->inputFrom($request);
|
||||
|
||||
$response = UsecaseBuilder::using($this->executor)
|
||||
->do($this->usecase)
|
||||
->from($input)
|
||||
->with($this->presenter)
|
||||
->run();
|
||||
|
||||
return new BrandsIndexResponse($response, $request->toUri());
|
||||
}
|
||||
|
||||
private function inputFrom(BrandsIndexRequest $request): Input
|
||||
{
|
||||
return new Input(
|
||||
pagination: $request->pagination(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Http\Requests\Brands;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
final class BrandShowRequest extends FormRequest
|
||||
{
|
||||
/** @return array<string, array<int, mixed>> */
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'identifier' => [
|
||||
'required',
|
||||
'string',
|
||||
'regex:/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[A-Z]{3}\d{2})$/i',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'identifier' => $this->route('identifier'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function identifier(): BrandId | BrandCode
|
||||
{
|
||||
$val = $this->string('identifier')->value();
|
||||
|
||||
if (Uuid::isValid($val)) {
|
||||
return new BrandId(Uuid::fromString($val));
|
||||
}
|
||||
|
||||
return new BrandCode(strtoupper($val));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Http\Requests\Brands;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Shared\Pagination;
|
||||
|
||||
/**
|
||||
* @method \Shared\Pagination pagination(int $defaultPageSize = 25)
|
||||
* @method \Uri\Rfc3986\Uri toUri()
|
||||
*/
|
||||
final class BrandsIndexRequest extends FormRequest
|
||||
{
|
||||
/** @return array<string, array<int, mixed>> */
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'offset' => ['sometimes', 'integer', 'min:0'],
|
||||
'limit' => ['sometimes', 'integer', 'min:1', 'max:' . Pagination::MAX_PAGE_SIZE],
|
||||
'page' => ['sometimes', 'integer', 'min:1'],
|
||||
'pageSize' => ['sometimes', 'integer', 'min:1', 'max:' . Pagination::MAX_PAGE_SIZE],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Http\Responses\Brands;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Dto\Brands\BrandDto;
|
||||
use Presentation\Api\V1\Presenters\Brands\BrandShowView;
|
||||
|
||||
final readonly class BrandShowResponse implements ApiResponse
|
||||
{
|
||||
public function __construct(
|
||||
private BrandShowView $view,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function toResponse($request): JsonResponse
|
||||
{
|
||||
return $this->present();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function present(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'brand' => BrandDto::fromView($this->view)->toArray(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Http\Responses\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\BrandItem;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Presentation\Api\V1\Contracts\ApiResponse;
|
||||
use Presentation\Api\V1\Dto\Brands\BrandDto;
|
||||
use Presentation\Api\V1\Dto\PaginationDto;
|
||||
use Presentation\Api\V1\Presenters\Brands\BrandsIndexView;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
final readonly class BrandsIndexResponse implements ApiResponse
|
||||
{
|
||||
public function __construct(
|
||||
private BrandsIndexView $view,
|
||||
private Uri $baseUri,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function toResponse($request): JsonResponse
|
||||
{
|
||||
return $this->present();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function present(): JsonResponse
|
||||
{
|
||||
$pagination = PaginationDto::create(
|
||||
page: $this->view->page,
|
||||
pageSize: $this->view->perPage,
|
||||
totalItems: $this->view->total,
|
||||
baseUri: $this->baseUri,
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'brands' => array_map(
|
||||
static fn (BrandItem $item): array => BrandDto::fromBrandItem($item)->toArray(),
|
||||
$this->view->brands
|
||||
),
|
||||
'pagination' => $pagination,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Presenters\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\Find\Output;
|
||||
use Shared\DDD\Usecase\Output as UsecaseOutput;
|
||||
use Shared\DDD\Usecase\Presenter;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements Presenter<Output, BrandShowView> */
|
||||
final readonly class BrandShowPresenter implements Presenter
|
||||
{
|
||||
#[\Override]
|
||||
public function present(UsecaseOutput $output): object
|
||||
{
|
||||
Assert::isInstanceOf($output, Output::class);
|
||||
|
||||
return new BrandShowView(
|
||||
id: $output->id,
|
||||
name: $output->name,
|
||||
code: $output->code,
|
||||
arcadiaId: $output->arcadiaId,
|
||||
createdAt: $output->createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
22
src/Presentation/Api/V1/Presenters/Brands/BrandShowView.php
Normal file
22
src/Presentation/Api/V1/Presenters/Brands/BrandShowView.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Presenters\Brands;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
|
||||
final readonly class BrandShowView
|
||||
{
|
||||
public function __construct(
|
||||
public BrandId $id,
|
||||
public BrandName $name,
|
||||
public BrandCode $code,
|
||||
public ArcadiaId $arcadiaId,
|
||||
public \DateTimeInterface $createdAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Presenters\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\Output;
|
||||
use Shared\DDD\Usecase\Output as UsecaseOutput;
|
||||
use Shared\DDD\Usecase\Presenter;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements Presenter<Output, BrandsIndexView> */
|
||||
final readonly class BrandsIndexPresenter implements Presenter
|
||||
{
|
||||
#[\Override]
|
||||
public function present(UsecaseOutput $output): object
|
||||
{
|
||||
Assert::isInstanceOf($output, Output::class);
|
||||
|
||||
return new BrandsIndexView(
|
||||
brands: $output->brands,
|
||||
total: $output->total,
|
||||
page: $output->page,
|
||||
perPage: $output->perPage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Api\V1\Presenters\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\BrandItem;
|
||||
|
||||
final readonly class BrandsIndexView
|
||||
{
|
||||
/**
|
||||
* @param list<BrandItem> $brands
|
||||
*/
|
||||
public function __construct(
|
||||
public array $brands,
|
||||
public int $total,
|
||||
public int $page,
|
||||
public int $perPage,
|
||||
) {
|
||||
}
|
||||
}
|
||||
76
src/Presentation/Console/Commands/ListBrandsCommand.php
Normal file
76
src/Presentation/Console/Commands/ListBrandsCommand.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Console\Commands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\FindAllBrands;
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\Input;
|
||||
use Illuminate\Console\Command;
|
||||
use Presentation\Console\Presenters\Brands\BrandRow;
|
||||
use Presentation\Console\Presenters\Brands\ListBrandsPresenter;
|
||||
use Presentation\Console\Presenters\Brands\ListBrandsView;
|
||||
use Shared\DDD\Usecase\Builder\UsecaseBuilder;
|
||||
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
|
||||
use Shared\Pagination;
|
||||
|
||||
final class ListBrandsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'arcadia:brand:list
|
||||
{--page=1 : The page number}
|
||||
{--per-page=25 : The number of brands per page}';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'List all Arcadia brands';
|
||||
|
||||
public function handle(
|
||||
FindAllBrands $usecase,
|
||||
UsecaseExecutor $executor,
|
||||
ListBrandsPresenter $presenter,
|
||||
): int {
|
||||
$view = UsecaseBuilder::using($executor)
|
||||
->do($usecase)
|
||||
->from($this->getInput())
|
||||
->with($presenter)
|
||||
->run();
|
||||
|
||||
return $this->renderView($view);
|
||||
}
|
||||
|
||||
private function getInput(): Input
|
||||
{
|
||||
return new Input($this->getPaginationFromOptions());
|
||||
}
|
||||
|
||||
private function renderView(ListBrandsView $view): int
|
||||
{
|
||||
if ($view->isEmpty()) {
|
||||
$this->info('No brands found.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$tableRows = array_map(
|
||||
static fn (BrandRow $row): array => array_values(get_object_vars($row)),
|
||||
$view->rows
|
||||
);
|
||||
|
||||
$this->table($view->headers, $tableRows);
|
||||
$this->info("Showing page {$view->page}/{$view->totalPages} (Total: {$view->total} brand(s))");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function getPaginationFromOptions(): Pagination
|
||||
{
|
||||
return Pagination::ofPage(
|
||||
max(1, (int) $this->option('page')),
|
||||
clamp(1, (int) $this->option('per-page'), Pagination::MAX_PAGE_SIZE)
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/Presentation/Console/Presenters/Brands/BrandRow.php
Normal file
17
src/Presentation/Console/Presenters/Brands/BrandRow.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Console\Presenters\Brands;
|
||||
|
||||
final readonly class BrandRow
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $code,
|
||||
public string $name,
|
||||
public int $arcadiaId,
|
||||
public string $createdAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Console\Presenters\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\BrandItem;
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\Output;
|
||||
use Shared\DDD\Usecase\Output as UsecaseOutput;
|
||||
use Shared\DDD\Usecase\Presenter;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/** @implements Presenter<Output, ListBrandsView> */
|
||||
final readonly class ListBrandsPresenter implements Presenter
|
||||
{
|
||||
#[\Override]
|
||||
public function present(UsecaseOutput $output): object
|
||||
{
|
||||
Assert::isInstanceOf($output, Output::class);
|
||||
|
||||
/** @var list<BrandRow> $rows */
|
||||
$rows = array_values(array_map(
|
||||
static fn (BrandItem $brand): BrandRow => new BrandRow(
|
||||
id: $brand->id->value,
|
||||
code: $brand->code->value,
|
||||
name: $brand->name->value,
|
||||
arcadiaId: $brand->arcadiaId->value,
|
||||
createdAt: $brand->createdAt->format('Y-m-d H:i:s'),
|
||||
),
|
||||
$output->brands
|
||||
));
|
||||
|
||||
$totalPages = $output->perPage > 0 ? (int) ceil($output->total / $output->perPage) : 0;
|
||||
|
||||
return new ListBrandsView(
|
||||
headers: ['ID (UUID)', 'Code', 'Name', 'Arcadia ID', 'Created At'],
|
||||
rows: $rows,
|
||||
total: $output->total,
|
||||
page: $output->page,
|
||||
perPage: $output->perPage,
|
||||
totalPages: $totalPages,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Presentation\Console\Presenters\Brands;
|
||||
|
||||
final readonly class ListBrandsView
|
||||
{
|
||||
/**
|
||||
* @param list<string> $headers
|
||||
* @param list<BrandRow> $rows
|
||||
*/
|
||||
public function __construct(
|
||||
public array $headers,
|
||||
public array $rows,
|
||||
public int $total,
|
||||
public int $page,
|
||||
public int $perPage,
|
||||
public int $totalPages,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->rows);
|
||||
}
|
||||
}
|
||||
76
tests/Feature/Console/Commands/ListBrandsCommandTest.php
Normal file
76
tests/Feature/Console/Commands/ListBrandsCommandTest.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Console\Commands;
|
||||
|
||||
use Infra\Database\Models\ArcadiaBrand;
|
||||
use Infra\Database\Models\Brand;
|
||||
use Presentation\Console\Commands\ListBrandsCommand;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(ListBrandsCommand::class);
|
||||
|
||||
describe('ListBrandsCommand', function (): void {
|
||||
beforeEach(function (): void {
|
||||
Brand::query()->delete();
|
||||
ArcadiaBrand::query()->delete();
|
||||
});
|
||||
|
||||
it('outputs no brands found when arcadia database is empty', function (): void {
|
||||
// GIVEN
|
||||
/** @var \Tests\TestCase $this */
|
||||
|
||||
// WHEN
|
||||
$this->artisan('arcadia:brand:list')
|
||||
// THEN
|
||||
->expectsOutput('No brands found.')
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
it('outputs table of brands and synchronizes missing ones', function (): void {
|
||||
// GIVEN
|
||||
/** @var \Tests\TestCase $this */
|
||||
ArcadiaBrand::factory()->create([
|
||||
'code' => 1,
|
||||
'libelle' => 'Le Gaulois',
|
||||
]);
|
||||
ArcadiaBrand::factory()->create([
|
||||
'code' => 2,
|
||||
'libelle' => 'Maître CoQ',
|
||||
]);
|
||||
|
||||
$existingUuid = Uuid::uuid7()->toString();
|
||||
Brand::factory()->create([
|
||||
'id' => $existingUuid,
|
||||
'arcadia_id' => 1,
|
||||
'uwil_public_id' => 'LGL01',
|
||||
]);
|
||||
|
||||
// WHEN
|
||||
$this->artisan('arcadia:brand:list')
|
||||
// THEN
|
||||
->expectsOutput('Showing page 1/1 (Total: 2 brand(s))')
|
||||
->assertSuccessful();
|
||||
|
||||
// Check that brand 2 was auto-created in database
|
||||
expect(Brand::query()->where('arcadia_id', 2)->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('respects pagination options', function (): void {
|
||||
// GIVEN
|
||||
/** @var \Tests\TestCase $this */
|
||||
for ($i = 1; $i <= 3; $i++) {
|
||||
ArcadiaBrand::factory()->create([
|
||||
'code' => $i,
|
||||
'libelle' => "Brand {$i}",
|
||||
]);
|
||||
}
|
||||
|
||||
// WHEN
|
||||
$this->artisan('arcadia:brand:list', ['--page' => '2', '--per-page' => '2'])
|
||||
// THEN
|
||||
->expectsOutput('Showing page 2/2 (Total: 3 brand(s))')
|
||||
->assertSuccessful();
|
||||
});
|
||||
});
|
||||
129
tests/Feature/OpenApi/V1/Brands/BrandOpenApiTest.php
Normal file
129
tests/Feature/OpenApi/V1/Brands/BrandOpenApiTest.php
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\OpenApi\V1\Brands;
|
||||
|
||||
use Infra\Database\Models\ArcadiaBrand;
|
||||
use Infra\Database\Models\Brand;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Spectator\Spectator;
|
||||
|
||||
beforeEach(function (): void {
|
||||
/** @var \Tests\TestCase $this */
|
||||
Spectator::reset();
|
||||
Spectator::using('openapi.yaml');
|
||||
|
||||
Brand::query()->delete();
|
||||
ArcadiaBrand::query()->delete();
|
||||
});
|
||||
|
||||
describe('Brand OpenAPI Conformance', function (): void {
|
||||
it('validates GET /api/v1/brands list response against openapi spec', function (): void {
|
||||
// GIVEN
|
||||
/** @var \Tests\TestCase $test */
|
||||
$test = $this;
|
||||
|
||||
ArcadiaBrand::factory()->create([
|
||||
'code' => 1,
|
||||
'libelle' => 'Le Gaulois',
|
||||
]);
|
||||
ArcadiaBrand::factory()->create([
|
||||
'code' => 2,
|
||||
'libelle' => 'Maître CoQ',
|
||||
]);
|
||||
|
||||
// WHEN
|
||||
/** @var \Illuminate\Testing\TestResponse<\Symfony\Component\HttpFoundation\Response> $response */
|
||||
$response = $test->getJson('/api/v1/brands');
|
||||
|
||||
// THEN
|
||||
$response->assertStatus(200);
|
||||
$response->assertValidRequest();
|
||||
$response->assertValidResponse(200);
|
||||
});
|
||||
|
||||
it('validates GET /api/v1/brands/{identifier} detail response by UUID against openapi spec', function (): void {
|
||||
// GIVEN
|
||||
/** @var \Tests\TestCase $test */
|
||||
$test = $this;
|
||||
|
||||
$arcadia = ArcadiaBrand::factory()->create([
|
||||
'code' => 1,
|
||||
'libelle' => 'Le Gaulois',
|
||||
]);
|
||||
|
||||
$uuid = Uuid::uuid7()->toString();
|
||||
Brand::factory()->create([
|
||||
'id' => $uuid,
|
||||
'arcadia_id' => $arcadia->code,
|
||||
'uwil_public_id' => 'LGL01',
|
||||
]);
|
||||
|
||||
// WHEN
|
||||
/** @var \Illuminate\Testing\TestResponse<\Symfony\Component\HttpFoundation\Response> $response */
|
||||
$response = $test->getJson("/api/v1/brands/{$uuid}");
|
||||
|
||||
// THEN
|
||||
$response->assertStatus(200);
|
||||
$response->assertValidRequest();
|
||||
$response->assertValidResponse(200);
|
||||
});
|
||||
|
||||
it('validates GET /api/v1/brands/{identifier} detail response by BrandCode against openapi spec', function (): void {
|
||||
// GIVEN
|
||||
/** @var \Tests\TestCase $test */
|
||||
$test = $this;
|
||||
|
||||
$arcadia = ArcadiaBrand::factory()->create([
|
||||
'code' => 2,
|
||||
'libelle' => 'Maître CoQ',
|
||||
]);
|
||||
|
||||
Brand::factory()->create([
|
||||
'id' => Uuid::uuid7()->toString(),
|
||||
'arcadia_id' => $arcadia->code,
|
||||
'uwil_public_id' => 'MCQ02',
|
||||
]);
|
||||
|
||||
// WHEN
|
||||
/** @var \Illuminate\Testing\TestResponse<\Symfony\Component\HttpFoundation\Response> $response */
|
||||
$response = $test->getJson('/api/v1/brands/MCQ02');
|
||||
|
||||
// THEN
|
||||
$response->assertStatus(200);
|
||||
$response->assertValidRequest();
|
||||
$response->assertValidResponse(200);
|
||||
});
|
||||
|
||||
it('validates GET /api/v1/brands/{identifier} not found response against openapi spec', function (): void {
|
||||
// GIVEN
|
||||
/** @var \Tests\TestCase $test */
|
||||
$test = $this;
|
||||
$unexistingId = Uuid::uuid7()->toString();
|
||||
|
||||
// WHEN
|
||||
/** @var \Illuminate\Testing\TestResponse<\Symfony\Component\HttpFoundation\Response> $response */
|
||||
$response = $test->getJson("/api/v1/brands/{$unexistingId}");
|
||||
|
||||
// THEN
|
||||
$response->assertStatus(404);
|
||||
$response->assertValidRequest();
|
||||
$response->assertValidResponse(404);
|
||||
});
|
||||
|
||||
it('validates GET /api/v1/brands/{identifier} validation error for invalid identifier', function (): void {
|
||||
// GIVEN
|
||||
/** @var \Tests\TestCase $test */
|
||||
$test = $this;
|
||||
$invalidId = 'invalid-format-123';
|
||||
|
||||
// WHEN
|
||||
/** @var \Illuminate\Testing\TestResponse<\Symfony\Component\HttpFoundation\Response> $response */
|
||||
$response = $test->getJson("/api/v1/brands/{$invalidId}");
|
||||
|
||||
// THEN
|
||||
$response->assertStatus(422);
|
||||
$response->assertValidResponse(422);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\Arcadia\Usecase\Brands\Find;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\Find\FindBrand;
|
||||
use Application\Arcadia\Usecase\Brands\Find\Input;
|
||||
use Application\Arcadia\Usecase\Brands\Find\Output;
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Infra\Adapters\Repositories\InMemoryBrandRepository;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(
|
||||
FindBrand::class,
|
||||
Input::class,
|
||||
Output::class,
|
||||
);
|
||||
|
||||
describe('FindBrand Usecase Unit Tests', function (): void {
|
||||
$repository = new InMemoryBrandRepository();
|
||||
|
||||
beforeEach(function () use ($repository): void {
|
||||
$repository->clear();
|
||||
});
|
||||
|
||||
it('executes successfully and returns Output when brand exists by BrandId', function () use ($repository): void {
|
||||
// GIVEN
|
||||
$brandId = new BrandId(Uuid::uuid7());
|
||||
$brandName = new BrandName('Le Gaulois');
|
||||
$brandCode = new BrandCode('LGL01');
|
||||
$arcadiaId = new ArcadiaId(1);
|
||||
|
||||
$brand = Brand::hydrate(
|
||||
id: $brandId,
|
||||
name: $brandName,
|
||||
code: $brandCode,
|
||||
arcadiaId: $arcadiaId,
|
||||
);
|
||||
|
||||
$repository->add($brand);
|
||||
$usecase = new FindBrand($repository);
|
||||
$input = Input::byId($brandId);
|
||||
|
||||
// WHEN
|
||||
$result = $usecase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class)
|
||||
->and($result->id)->toBe($brandId)
|
||||
->and($result->name)->toBe($brandName)
|
||||
->and($result->code)->toBe($brandCode)
|
||||
->and($result->arcadiaId)->toBe($arcadiaId);
|
||||
});
|
||||
|
||||
it('executes successfully and returns Output when brand exists by BrandCode', function () use ($repository): void {
|
||||
// GIVEN
|
||||
$brandId = new BrandId(Uuid::uuid7());
|
||||
$brandName = new BrandName('Maître CoQ');
|
||||
$brandCode = new BrandCode('MCQ02');
|
||||
$arcadiaId = new ArcadiaId(2);
|
||||
|
||||
$brand = Brand::hydrate(
|
||||
id: $brandId,
|
||||
name: $brandName,
|
||||
code: $brandCode,
|
||||
arcadiaId: $arcadiaId,
|
||||
);
|
||||
|
||||
$repository->add($brand);
|
||||
$usecase = new FindBrand($repository);
|
||||
$input = Input::byCode($brandCode);
|
||||
|
||||
// WHEN
|
||||
$result = $usecase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class)
|
||||
->and($result->id)->toBe($brandId)
|
||||
->and($result->name)->toBe($brandName)
|
||||
->and($result->code)->toBe($brandCode)
|
||||
->and($result->arcadiaId)->toBe($arcadiaId);
|
||||
});
|
||||
|
||||
it('throws BrandNotFoundException when brand is not found by BrandId', function () use ($repository): void {
|
||||
// GIVEN
|
||||
$brandId = new BrandId(Uuid::uuid7());
|
||||
$usecase = new FindBrand($repository);
|
||||
$input = Input::byId($brandId);
|
||||
|
||||
// WHEN
|
||||
$execution = fn () => $usecase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($execution)->toThrow(BrandNotFoundException::class, "Brand '{$brandId->value}' not found.");
|
||||
});
|
||||
|
||||
it('throws BrandNotFoundException when brand is not found by BrandCode', function () use ($repository): void {
|
||||
// GIVEN
|
||||
$brandCode = new BrandCode('LGL98');
|
||||
$usecase = new FindBrand($repository);
|
||||
$input = Input::byCode($brandCode);
|
||||
|
||||
// WHEN
|
||||
$execution = fn () => $usecase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($execution)->toThrow(BrandNotFoundException::class, "Brand with code '{$brandCode->value}' not found.");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Application\Arcadia\Usecase\Brands\FindAll;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\BrandItem;
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\FindAllBrands;
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\Input;
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\Output;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Domain\Services\BrandCodeGenerator;
|
||||
use Domain\Services\BrandResolver;
|
||||
use Infra\Adapters\Directories\InMemoryArcadiaBrandDirectory;
|
||||
use Infra\Adapters\Repositories\InMemoryBrandRepository;
|
||||
use Psr\Log\NullLogger;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\Pagination;
|
||||
|
||||
covers(
|
||||
FindAllBrands::class,
|
||||
Input::class,
|
||||
Output::class,
|
||||
BrandItem::class,
|
||||
);
|
||||
|
||||
describe('FindAllBrands Usecase Unit Tests', function (): void {
|
||||
$arcadiaDirectory = new InMemoryArcadiaBrandDirectory();
|
||||
$brandRepository = new InMemoryBrandRepository();
|
||||
$codeGenerator = new BrandCodeGenerator();
|
||||
$logger = new NullLogger();
|
||||
|
||||
beforeEach(function () use ($arcadiaDirectory, $brandRepository): void {
|
||||
$arcadiaDirectory->clear();
|
||||
$brandRepository->clear();
|
||||
});
|
||||
|
||||
it('returns existing brands and generates missing ones automatically', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $logger): void {
|
||||
// GIVEN
|
||||
$arcadia1 = new ArcadiaBrand(new ArcadiaId(1), new BrandName('Le Gaulois'));
|
||||
$arcadia2 = new ArcadiaBrand(new ArcadiaId(2), new BrandName('Maître CoQ'));
|
||||
$arcadiaDirectory->add($arcadia1, $arcadia2);
|
||||
|
||||
// Already existing brand in our repository for Arcadia 1
|
||||
$existingBrand = Brand::hydrate(
|
||||
id: new BrandId(Uuid::uuid7()),
|
||||
name: new BrandName('Le Gaulois'),
|
||||
code: new BrandCode('LGL01'),
|
||||
arcadiaId: new ArcadiaId(1),
|
||||
);
|
||||
$brandRepository->add($existingBrand);
|
||||
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $logger);
|
||||
$usecase = new FindAllBrands($resolver);
|
||||
$input = new Input(Pagination::ofPage(1, 25));
|
||||
|
||||
// WHEN
|
||||
$result = $usecase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class)
|
||||
->and($result->brands)->toHaveCount(2)
|
||||
->and($result->total)->toBe(2)
|
||||
->and($result->page)->toBe(1)
|
||||
->and($result->perPage)->toBe(25);
|
||||
|
||||
// First item was the existing one
|
||||
expect($result->brands[0]->id)->toBe($existingBrand->id())
|
||||
->and($result->brands[0]->code->value)->toBe('LGL01')
|
||||
->and($result->brands[0]->name->value)->toBe('Le Gaulois');
|
||||
|
||||
// Second item was generated and persisted
|
||||
expect($result->brands[1]->arcadiaId->value)->toBe(2)
|
||||
->and($result->brands[1]->name->value)->toBe('Maître CoQ')
|
||||
->and(str_starts_with($result->brands[1]->code->value, 'MCQ'))->toBeTrue();
|
||||
|
||||
// Check that the second brand was persisted in repository
|
||||
expect($brandRepository->findByArcadiaId(new ArcadiaId(2)))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('slices brands according to pagination', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $logger): void {
|
||||
// GIVEN
|
||||
$arcadia1 = new ArcadiaBrand(new ArcadiaId(1), new BrandName('Le Gaulois'));
|
||||
$arcadia2 = new ArcadiaBrand(new ArcadiaId(2), new BrandName('Maître CoQ'));
|
||||
$arcadia3 = new ArcadiaBrand(new ArcadiaId(3), new BrandName('Loué'));
|
||||
$arcadiaDirectory->add($arcadia1, $arcadia2, $arcadia3);
|
||||
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $logger);
|
||||
$usecase = new FindAllBrands($resolver);
|
||||
$input = new Input(Pagination::ofPage(2, 2));
|
||||
|
||||
// WHEN
|
||||
$result = $usecase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class)
|
||||
->and($result->brands)->toHaveCount(1)
|
||||
->and($result->total)->toBe(3)
|
||||
->and($result->page)->toBe(2)
|
||||
->and($result->perPage)->toBe(2);
|
||||
});
|
||||
|
||||
it('returns an empty list when there are no arcadia brands', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $logger): void {
|
||||
// GIVEN
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $logger);
|
||||
$usecase = new FindAllBrands($resolver);
|
||||
$input = new Input(Pagination::ofPage(1, 25));
|
||||
|
||||
// WHEN
|
||||
$result = $usecase->execute($input);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeInstanceOf(Output::class)
|
||||
->and($result->brands)->toBeEmpty()
|
||||
->and($result->total)->toBe(0);
|
||||
});
|
||||
});
|
||||
78
tests/Unit/Presentation/Api/V1/Dto/Brands/BrandDtoTest.php
Normal file
78
tests/Unit/Presentation/Api/V1/Dto/Brands/BrandDtoTest.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Presentation\Api\V1\Dto\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\BrandItem;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Presentation\Api\V1\Dto\Brands\BrandDto;
|
||||
use Presentation\Api\V1\Presenters\Brands\BrandShowView;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(BrandDto::class);
|
||||
|
||||
describe('BrandDto Unit Tests', function (): void {
|
||||
it('creates BrandDto from BrandShowView', function (): void {
|
||||
// GIVEN
|
||||
$id = new BrandId(Uuid::uuid7());
|
||||
$name = new BrandName('Le Gaulois');
|
||||
$code = new BrandCode('LGL01');
|
||||
$arcadiaId = new ArcadiaId(1);
|
||||
$createdAt = new \DateTimeImmutable('2024-01-15T08:00:00Z');
|
||||
|
||||
$view = new BrandShowView(
|
||||
id: $id,
|
||||
name: $name,
|
||||
code: $code,
|
||||
arcadiaId: $arcadiaId,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$dto = BrandDto::fromView($view);
|
||||
|
||||
// THEN
|
||||
expect($dto->id)->toBe((string) $id)
|
||||
->and($dto->name)->toBe('Le Gaulois')
|
||||
->and($dto->code)->toBe('LGL01')
|
||||
->and($dto->arcadiaId)->toBe(1)
|
||||
->and($dto->createdAt)->toBe($createdAt->format(\DateTimeInterface::ATOM))
|
||||
->and($dto->toArray())->toBe([
|
||||
'id' => (string) $id,
|
||||
'code' => 'LGL01',
|
||||
'name' => 'Le Gaulois',
|
||||
'arcadiaId' => 1,
|
||||
'createdAt' => $createdAt->format(\DateTimeInterface::ATOM),
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates BrandDto from BrandItem', function (): void {
|
||||
// GIVEN
|
||||
$id = new BrandId(Uuid::uuid7());
|
||||
$name = new BrandName('Maître CoQ');
|
||||
$code = new BrandCode('MCQ02');
|
||||
$arcadiaId = new ArcadiaId(2);
|
||||
$createdAt = new \DateTimeImmutable('2024-01-15T08:00:00Z');
|
||||
|
||||
$item = new BrandItem(
|
||||
id: $id,
|
||||
name: $name,
|
||||
code: $code,
|
||||
arcadiaId: $arcadiaId,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
// WHEN
|
||||
$dto = BrandDto::fromBrandItem($item);
|
||||
|
||||
// THEN
|
||||
expect($dto->id)->toBe((string) $id)
|
||||
->and($dto->name)->toBe('Maître CoQ')
|
||||
->and($dto->code)->toBe('MCQ02')
|
||||
->and($dto->arcadiaId)->toBe(2);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Presentation\Api\V1\Http\Requests\Brands;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Presentation\Api\V1\Http\Requests\Brands\BrandShowRequest;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(BrandShowRequest::class);
|
||||
|
||||
describe('BrandShowRequest Unit Tests', function (): void {
|
||||
it('returns validation rules', function (): void {
|
||||
// GIVEN
|
||||
$request = new BrandShowRequest();
|
||||
|
||||
// WHEN & THEN
|
||||
expect($request->rules())->toHaveKey('identifier');
|
||||
});
|
||||
|
||||
it('parses UUID identifier into BrandId', function (): void {
|
||||
// GIVEN
|
||||
$uuid = Uuid::uuid7()->toString();
|
||||
$request = new BrandShowRequest();
|
||||
$request->merge(['identifier' => $uuid]);
|
||||
|
||||
// WHEN
|
||||
$identifier = $request->identifier();
|
||||
|
||||
// THEN
|
||||
expect($identifier)->toBeInstanceOf(BrandId::class)
|
||||
->and($identifier->value)->toBe($uuid);
|
||||
});
|
||||
|
||||
it('parses code identifier into BrandCode', function (): void {
|
||||
// GIVEN
|
||||
$request = new BrandShowRequest();
|
||||
$request->merge(['identifier' => 'LGL01']);
|
||||
|
||||
// WHEN
|
||||
$identifier = $request->identifier();
|
||||
|
||||
// THEN
|
||||
expect($identifier)->toBeInstanceOf(BrandCode::class)
|
||||
->and($identifier->value)->toBe('LGL01');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Presentation\Api\V1\Presenters\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\Find\Output;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Presentation\Api\V1\Presenters\Brands\BrandShowPresenter;
|
||||
use Presentation\Api\V1\Presenters\Brands\BrandShowView;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(
|
||||
BrandShowPresenter::class,
|
||||
BrandShowView::class,
|
||||
);
|
||||
|
||||
describe('BrandShowPresenter Unit Tests', function (): void {
|
||||
it('transforms Output into BrandShowView', function (): void {
|
||||
// GIVEN
|
||||
$id = new BrandId(Uuid::uuid7());
|
||||
$name = new BrandName('Le Gaulois');
|
||||
$code = new BrandCode('LGL01');
|
||||
$arcadiaId = new ArcadiaId(1);
|
||||
$createdAt = new \DateTimeImmutable('2024-01-15T08:00:00Z');
|
||||
|
||||
$output = new Output(
|
||||
id: $id,
|
||||
name: $name,
|
||||
code: $code,
|
||||
arcadiaId: $arcadiaId,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
$presenter = new BrandShowPresenter();
|
||||
|
||||
// WHEN
|
||||
$view = $presenter->present($output);
|
||||
|
||||
// THEN
|
||||
expect($view)->toBeInstanceOf(BrandShowView::class)
|
||||
->and($view->id)->toBe($id)
|
||||
->and($view->name)->toBe($name)
|
||||
->and($view->code)->toBe($code)
|
||||
->and($view->arcadiaId)->toBe($arcadiaId)
|
||||
->and($view->createdAt)->toBe($createdAt);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Presentation\Api\V1\Presenters\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\BrandItem;
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\Output;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Presentation\Api\V1\Presenters\Brands\BrandsIndexPresenter;
|
||||
use Presentation\Api\V1\Presenters\Brands\BrandsIndexView;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(
|
||||
BrandsIndexPresenter::class,
|
||||
BrandsIndexView::class,
|
||||
);
|
||||
|
||||
describe('BrandsIndexPresenter Unit Tests', function (): void {
|
||||
it('transforms Output into BrandsIndexView', function (): void {
|
||||
// GIVEN
|
||||
$item1 = new BrandItem(
|
||||
id: new BrandId(Uuid::uuid7()),
|
||||
name: new BrandName('Le Gaulois'),
|
||||
code: new BrandCode('LGL01'),
|
||||
arcadiaId: new ArcadiaId(1),
|
||||
createdAt: new \DateTimeImmutable('2024-01-15T08:00:00Z'),
|
||||
);
|
||||
|
||||
$output = new Output([$item1], 1, 1, 25);
|
||||
$presenter = new BrandsIndexPresenter();
|
||||
|
||||
// WHEN
|
||||
$view = $presenter->present($output);
|
||||
|
||||
// THEN
|
||||
expect($view)->toBeInstanceOf(BrandsIndexView::class)
|
||||
->and($view->brands)->toHaveCount(1)
|
||||
->and($view->brands[0])->toBe($item1)
|
||||
->and($view->total)->toBe(1)
|
||||
->and($view->page)->toBe(1)
|
||||
->and($view->perPage)->toBe(25);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Presentation\Console\Presenters\Brands;
|
||||
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\BrandItem;
|
||||
use Application\Arcadia\Usecase\Brands\FindAll\Output;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Presentation\Console\Presenters\Brands\BrandRow;
|
||||
use Presentation\Console\Presenters\Brands\ListBrandsPresenter;
|
||||
use Presentation\Console\Presenters\Brands\ListBrandsView;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(
|
||||
ListBrandsPresenter::class,
|
||||
ListBrandsView::class,
|
||||
BrandRow::class,
|
||||
);
|
||||
|
||||
describe('ListBrandsPresenter Unit Tests', function (): void {
|
||||
it('transforms Output into ListBrandsView with formatted rows', function (): void {
|
||||
// GIVEN
|
||||
$id = new BrandId(Uuid::uuid7());
|
||||
$name = new BrandName('Le Gaulois');
|
||||
$code = new BrandCode('LGL01');
|
||||
$arcadiaId = new ArcadiaId(1);
|
||||
$createdAt = new \DateTimeImmutable('2024-01-15 08:00:00');
|
||||
|
||||
$item = new BrandItem($id, $name, $code, $arcadiaId, $createdAt);
|
||||
$output = new Output([$item], 1, 1, 25);
|
||||
|
||||
$presenter = new ListBrandsPresenter();
|
||||
|
||||
// WHEN
|
||||
$view = $presenter->present($output);
|
||||
|
||||
// THEN
|
||||
expect($view)->toBeInstanceOf(ListBrandsView::class)
|
||||
->and($view->isEmpty())->toBeFalse()
|
||||
->and($view->total)->toBe(1)
|
||||
->and($view->page)->toBe(1)
|
||||
->and($view->perPage)->toBe(25)
|
||||
->and($view->totalPages)->toBe(1)
|
||||
->and($view->rows)->toHaveCount(1)
|
||||
->and($view->rows[0]->id)->toBe((string) $id)
|
||||
->and($view->rows[0]->code)->toBe('LGL01')
|
||||
->and($view->rows[0]->name)->toBe('Le Gaulois')
|
||||
->and($view->rows[0]->arcadiaId)->toBe(1)
|
||||
->and($view->rows[0]->createdAt)->toBe('2024-01-15 08:00:00');
|
||||
});
|
||||
|
||||
it('returns empty view when no brands in output', function (): void {
|
||||
// GIVEN
|
||||
$output = new Output([], 0, 1, 25);
|
||||
$presenter = new ListBrandsPresenter();
|
||||
|
||||
// WHEN
|
||||
$view = $presenter->present($output);
|
||||
|
||||
// THEN
|
||||
expect($view)->toBeInstanceOf(ListBrandsView::class)
|
||||
->and($view->isEmpty())->toBeTrue()
|
||||
->and($view->total)->toBe(0)
|
||||
->and($view->totalPages)->toBe(0)
|
||||
->and($view->rows)->toBeEmpty();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue