feat(inco-compositions): add IncoCompositionShow feature

- Implemented IncoCompositionShowController to handle requests for inco composition details.
- Created IncoCompositionDto for response data structure.
- Added IncoCompositionShowRequest for request validation.
- Developed IncoCompositionShowResponse for formatting API responses.
- Introduced IncoCompositionShowPresenter and IncoCompositionShowView for presentation logic.
- Added tests for IncoCompositionShow feature and OpenAPI conformance.
This commit is contained in:
Jérémy Munsch 2026-08-14 15:56:58 +02:00
parent f667532ff7
commit 91114cb67b
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
26 changed files with 1359 additions and 879 deletions

View file

@ -0,0 +1,19 @@
info:
name: Get Inco Composition
type: http
seq: 1
http:
method: GET
url: "{{ENDPOINT}}/api/{{APIV1}}/inco-sheets/:incoSheet/composition"
params:
- name: incoSheet
value: "{{INCOSHEET_ID}}"
type: path
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View file

@ -0,0 +1,6 @@
info:
name: IncoCompositions
seq: 3
folder:
auth: inherit

View file

@ -5,9 +5,9 @@ info:
http:
method: GET
url: "{{ENDPOINT}}/api/{{APIV1}}/inco-sheets/:employeeId"
url: "{{ENDPOINT}}/api/{{APIV1}}/inco-sheets/:incoSheet"
params:
- name: employeeId
- name: incoSheet
value: "{{INCOSHEET_ID}}"
type: path
auth: inherit

View file

@ -1000,7 +1000,7 @@ components:
pattern: '^\d{1,}$'
example: "12345"
identification:
$ref: "#/components/schemas/IncoCompoIdentification"
$ref: "#/components/schemas/IncoCompositionIdentification"
energy:
$ref: "#/components/schemas/IncoCompoEnergy"
referenceMacronutrients:
@ -1077,7 +1077,7 @@ components:
type: string
example: "0"
IncoCompoIdentification:
IncoCompositionIdentification:
type: object
description: Informations d'identification de la composition INCO.
properties:

View file

@ -1,819 +0,0 @@
openapi: 3.1.1
info:
title: Arcadia Genesis API (for PHP client)
version: 1.0.0
summary: Proposed HTTP API contract consumed by arcadia-genesis-php-client
description: |
This specification describes the API surface implied by the PHP client.
It models employee and INCO sheet use cases exposed by the client services.
servers:
- url: https://api.example.com/gaston/api
description: Example production server
security:
- ArcadiaAuth: []
tags:
- name: Employees
- name: IncoSheets
- name: IncoComposition
paths:
/employees:
get:
tags: [Employees]
operationId: listEmployees
summary: List employees
description: |
Returns a paginated list of employees.
Supports optional filtering by update timestamp and site ids.
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
- $ref: '#/components/parameters/Since'
- $ref: '#/components/parameters/EmployeeSortBy'
- $ref: '#/components/parameters/SortDirection'
- name: siteIds
in: query
required: false
description: Filter on one or many site ids
style: form
explode: true
schema:
type: array
minItems: 1
items:
type: string
responses:
'200':
description: Employees page
content:
application/json:
schema:
$ref: '#/components/schemas/EmployeeListResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServiceUnavailable'
/employees/{employeeId}:
get:
tags: [Employees]
operationId: getEmployeeById
summary: Get an employee by id
parameters:
- name: employeeId
in: path
required: true
schema:
type: string
responses:
'200':
description: Employee detail
content:
application/json:
schema:
$ref: '#/components/schemas/EmployeeItemResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: Employee not found
content:
application/json:
schema:
$ref: '#/components/schemas/NotFoundResponse'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServiceUnavailable'
/inco-sheets:
get:
tags: [IncoSheets]
operationId: listIncoSheets
summary: List INCO sheets
description: |
Returns paginated INCO sheets.
Supports brand filtering and optional updated_at lower bound.
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
- $ref: '#/components/parameters/Since'
- $ref: '#/components/parameters/IncoSortBy'
- $ref: '#/components/parameters/SortDirection'
- name: brands
in: query
required: false
description: Filter on one or many brand codes
style: form
explode: true
schema:
type: array
minItems: 1
items:
type: string
responses:
'200':
description: INCO sheets page
content:
application/json:
schema:
$ref: '#/components/schemas/IncoListResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServiceUnavailable'
/inco-sheets/short:
get:
tags: [IncoSheets]
operationId: listIncoSheetsShort
summary: List INCO sheets (short payload)
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
- $ref: '#/components/parameters/IncoSortBy'
- $ref: '#/components/parameters/SortDirection'
- name: brands
in: query
required: true
description: One or many brand codes
style: form
explode: true
schema:
type: array
minItems: 1
items:
type: string
responses:
'200':
description: INCO short page
content:
application/json:
schema:
$ref: '#/components/schemas/IncoShortListResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServiceUnavailable'
/inco-sheets/search:
get:
tags: [IncoSheets]
operationId: searchIncoSheetsByIdLike
summary: Search INCO sheets by id-like pattern
parameters:
- name: incoIdLike
in: query
required: true
schema:
type: string
description: Pattern used with SQL-like matching rules
- name: brands
in: query
required: true
style: form
explode: true
schema:
type: array
minItems: 1
items:
type: string
- name: short
in: query
required: false
schema:
type: boolean
default: false
description: Return short projection when true
responses:
'200':
description: Search result (first page, size 50)
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/IncoListResponse'
- $ref: '#/components/schemas/IncoShortListResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServiceUnavailable'
/inco-sheets/{incoId}:
get:
tags: [IncoSheets]
operationId: getIncoSheetById
summary: Get INCO sheet by id
parameters:
- name: incoId
in: path
required: true
schema:
type: string
pattern: '^\\d{1,}$'
responses:
'200':
description: INCO sheet detail
content:
application/json:
schema:
$ref: '#/components/schemas/IncoItemResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: INCO not found
content:
application/json:
schema:
$ref: '#/components/schemas/NotFoundResponse'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServiceUnavailable'
/inco-sheets/{incoId}/composition:
get:
tags: [IncoComposition]
operationId: getIncoCompositionByIncoId
summary: Get composition by INCO id
parameters:
- name: incoId
in: path
required: true
schema:
type: string
pattern: '^\\d{1,}$'
responses:
'200':
description: Composition detail
content:
application/json:
schema:
$ref: '#/components/schemas/IncoCompoItemResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: Composition not found
content:
application/json:
schema:
$ref: '#/components/schemas/NotFoundResponse'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServiceUnavailable'
/inco-compositions/{incoCompoId}:
get:
tags: [IncoComposition]
operationId: getIncoCompositionById
summary: Get composition by composition id
parameters:
- name: incoCompoId
in: path
required: true
schema:
type: string
pattern: '^\\d{1,}$'
responses:
'200':
description: Composition detail
content:
application/json:
schema:
$ref: '#/components/schemas/IncoCompoItemResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: Composition not found
content:
application/json:
schema:
$ref: '#/components/schemas/NotFoundResponse'
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServiceUnavailable'
components:
securitySchemes:
ArcadiaAuth:
type: apiKey
in: header
name: X-API-Key
description: |
Requests also require `X-API-Signature` and `X-Timestamp` headers.
Signature is computed from method, URI, body payload, shared secret, and timestamp.
parameters:
Page:
name: page
in: query
required: false
schema:
type: integer
minimum: 1
default: 1
description: 1-based page index
PageSize:
name: page_size
in: query
required: false
schema:
type: integer
minimum: 1
default: 50
maximum: 10000
description: Max records per page
Since:
name: since
in: query
required: false
schema:
type: string
format: date-time
description: RFC3339 timestamp lower bound (>=)
SortDirection:
name: sortDirection
in: query
required: false
schema:
type: string
enum: [asc, desc]
default: desc
EmployeeSortBy:
name: sortBy
in: query
required: false
schema:
type: string
enum:
- arcadiaId
- startDate
- endDate
- plannedEndDate
- createdAt
- updatedAt
IncoSortBy:
name: sortBy
in: query
required: false
schema:
type: string
enum:
- id
- createdAt
- updatedAt
responses:
BadRequest:
description: Invalid request
content:
application/json:
schema:
$ref: '#/components/schemas/ApiErrorResponse'
Unauthorized:
description: Authentication failed
content:
application/json:
schema:
$ref: '#/components/schemas/ApiErrorResponse'
TooManyRequests:
description: Rate limited
content:
application/json:
schema:
$ref: '#/components/schemas/ApiErrorResponse'
ServiceUnavailable:
description: Upstream service unavailable
content:
application/json:
schema:
$ref: '#/components/schemas/ApiErrorResponse'
schemas:
Employee:
type: object
additionalProperties: false
required:
- matriculePublique
- lastname
- firstname
- start_date
- establishment
- site
- service
- created_at
properties:
matriculePublique:
type: string
lastname:
type: string
firstname:
type: string
start_date:
type: string
format: date-time
end_date:
type: [string, 'null']
format: date-time
planned_end_date:
type: [string, 'null']
format: date-time
establishment:
type: string
site:
type: string
service:
type: string
created_at:
type: string
format: date-time
updated_at:
type: [string, 'null']
format: date-time
IncoBrand:
type: object
additionalProperties: false
required: [code, label]
properties:
code:
type: string
label:
type: string
Inco:
type: object
additionalProperties: false
required:
- id
- name
- libelle
- description
- allegations
- sellArguments
- preparationAdvices
- ingredients
- allergens
- brand
- isSeasonal
- additives
properties:
id:
type: string
pattern: '^\\d{1,}$'
name:
type: string
libelle:
type: string
description:
type: string
allegations:
type: object
additionalProperties: true
sellArguments:
type: string
preparationAdvices:
type: string
tips:
type: [string, 'null']
sellFamilly:
type: [string, 'null']
ingredients:
type: object
additionalProperties: true
portions:
type: [object, 'null']
additionalProperties: true
allergens:
type: array
items:
type: object
additionalProperties: true
brand:
$ref: '#/components/schemas/IncoBrand'
isSeasonal:
type: boolean
weight:
type: [object, 'null']
additionalProperties: true
nutriscore:
type: [object, 'null']
additionalProperties: true
storageCondition:
type: [object, 'null']
additionalProperties: true
additives:
type: array
items:
type: object
additionalProperties: true
compo:
type: [string, 'null']
pattern: '^\\d{1,}$'
created_at:
type: [string, 'null']
format: date-time
updated_at:
type: [string, 'null']
format: date-time
IncoShort:
type: object
additionalProperties: false
required:
- id
- name
- libelle
- description
properties:
id:
type: string
pattern: '^\\d{1,}$'
name:
type: string
libelle:
type: string
description:
type: string
created_at:
type: [string, 'null']
format: date-time
updated_at:
type: [string, 'null']
format: date-time
IncoCompo:
type: object
additionalProperties: false
required:
- id
- idInco
- identification
- energie
- macronutrimentsRef
- macronutrimentsPortion
- fnl
- apportsReference
- nutriScore
- pointsNutriScore
- mineraux
- vitamines
- glucidesDetail
- acideGras
- acidesAmines
- composesSpeciaux
properties:
id:
type: string
pattern: '^\\d{1,}$'
idInco:
type: string
pattern: '^\\d{1,}$'
identification:
type: object
additionalProperties: true
energie:
type: object
additionalProperties: true
macronutrimentsRef:
type: object
additionalProperties: true
macronutrimentsPortion:
type: object
additionalProperties: true
fnl:
type: object
additionalProperties: true
apportsReference:
type: object
additionalProperties: true
nutriScore:
type: object
additionalProperties: true
pointsNutriScore:
type: object
additionalProperties: true
mineraux:
type: object
additionalProperties: true
vitamines:
type: object
additionalProperties: true
glucidesDetail:
type: object
additionalProperties: true
acideGras:
type: object
additionalProperties: true
acidesAmines:
type: object
additionalProperties: true
composesSpeciaux:
type: object
additionalProperties: true
Pagination:
type: object
additionalProperties: false
required:
- page
- pageSize
- totalItems
- totalPages
- links
properties:
page:
type: integer
minimum: 1
pageSize:
type: integer
minimum: 1
totalItems:
type: integer
minimum: 0
totalPages:
type: integer
minimum: 0
links:
type: object
additionalProperties: false
required: [first, last]
properties:
first:
type: string
format: uri
last:
type: string
format: uri
EmployeeListResponse:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Employee'
pagination:
$ref: '#/components/schemas/Pagination'
EmployeeItemResponse:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
$ref: '#/components/schemas/Employee'
IncoListResponse:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Inco'
pagination:
$ref: '#/components/schemas/Pagination'
IncoShortListResponse:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/IncoShort'
pagination:
$ref: '#/components/schemas/Pagination'
IncoItemResponse:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
$ref: '#/components/schemas/Inco'
IncoCompoItemResponse:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
$ref: '#/components/schemas/IncoCompo'
SuccessEnvelope:
type: object
additionalProperties: false
required: [success, data]
properties:
success:
type: boolean
const: true
data: {}
pagination:
$ref: '#/components/schemas/Pagination'
ApiErrorCode:
type: string
enum:
- '0004'
- ERR_UNAUTHORIZED_PARAMETER
- ERR_DEFAULT
- ERR_LIST_SIZE_MISMATCH
- ERR_METAEXCEPTION
- ERR_SORT_LIST_SIZE_MISMATCH
- ERR_TABLE_NAME_MISSING
- ERR_UNAUTHORIZED_OPERATOR
- ERR_BAD_DATE_FORMAT
- ERR_PAGE_TYPE_MISMATCH
- ERR_PAGE_SIZE_TYPE_MISMATCH
- ERR_TOO_MANY_RECORDS
- ERR_INVALID_API_KEY
- ERR_INVALID_API_SIGNATURE
- ERR_TOO_MANY_REQUESTS
- UNKNOWN
ApiErrorObject:
type: object
additionalProperties: true
required: [code]
properties:
code:
$ref: '#/components/schemas/ApiErrorCode'
message:
type: string
ApiErrorResponse:
type: object
additionalProperties: false
required: [success, error]
properties:
success:
type: boolean
const: false
error:
oneOf:
- $ref: '#/components/schemas/ApiErrorObject'
- type: array
items:
$ref: '#/components/schemas/ApiErrorObject'
NotFoundResponse:
type: object
additionalProperties: false
required: [success, error]
properties:
success:
type: boolean
const: false
error:
type: object
additionalProperties: false
required: [code, message]
properties:
code:
type: string
enum: [NOT_FOUND]
message:
type: string

View file

@ -6,10 +6,21 @@ use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Route;
use Presentation\Api\V1\Http\Controllers\Employees\EmployeeShowController;
use Presentation\Api\V1\Http\Controllers\Employees\EmployeesIndexController;
use Presentation\Api\V1\Http\Controllers\IncoCompositions\IncoCompositionShowController;
use Presentation\Api\V1\Http\Controllers\IncoSheets\IncoSheetsIndexController;
use Presentation\Api\V1\Http\Controllers\IncoSheets\IncoSheetsSearchController;
use Presentation\Api\V1\Http\Controllers\IncoSheets\IncoSheetsShowController;
/*
|--------------------------------------------------------------------------
| API V1 Routes
|--------------------------------------------------------------------------
|
| Version 1 of the Arcadia API.
| All routes are prefixed with /api/v1 (configured in bootstrap/app.php).
|
*/
// * ROOT
Route::get('/', fn () => response()->json([
'message' => Config::string('app.name') . ' v1',
@ -25,14 +36,15 @@ Route::get('/openapi', function () {
})->name('openapi');
// * EMPLOYEES
Route::prefix('employees')->group(function () {
Route::prefix('employees')->group(function (): void {
Route::get('/', EmployeesIndexController::class)->name('employees.index');
Route::get('/{employeeId}', EmployeeShowController::class)->name('employees.show');
});
// * INCO SHEETS
Route::prefix('inco-sheets')->group(function () {
Route::prefix('inco-sheets')->group(function (): void {
Route::get('/', IncoSheetsIndexController::class)->name('inco-sheets.index');
Route::get('/search', IncoSheetsSearchController::class)->name('inco-sheets.search');
Route::get('/{incoId}', IncoSheetsShowController::class)->name('inco-sheets.show');
Route::get('/{incoId}/composition', IncoCompositionShowController::class)->name('inco-sheets.composition');
});

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Application\Usecase\IncoCompositions\Find;
use Application\Exceptions\NotFoundException;
use Domain\Ports\Directories\IncoCompositionDirectory;
use Shared\DDD\Usecase\Input as BaseInput;
use Shared\DDD\Usecase\Usecase;
use Webmozart\Assert\Assert;
/** @implements Usecase<\Shared\DDD\Usecase\Input, \Shared\DDD\Usecase\Output> */
final readonly class FindIncoComposition implements Usecase
{
public function __construct(
private IncoCompositionDirectory $directory
) {
}
#[\Override]
public function execute(BaseInput $input): Output
{
Assert::isInstanceOf($input, Input::class);
$incoComposition = $this->directory->findByIncoId($input->incoId);
if ($incoComposition === null) {
throw NotFoundException::forEntity('IncoComposition', (string) $input->incoId->value);
}
return new Output(
id: $incoComposition->id(),
idInco: $incoComposition->idInco,
identification: $incoComposition->identification,
energy: $incoComposition->energy,
referenceMacronutrients: $incoComposition->referenceMacronutrients,
portionMacronutrients: $incoComposition->portionMacronutrients,
freshProduceAndNuts: $incoComposition->freshProduceAndNuts,
referenceIntakes: $incoComposition->referenceIntakes,
nutriScore: $incoComposition->nutriScore,
nutriScorePoints: $incoComposition->nutriScorePoints,
minerals: $incoComposition->minerals,
vitamins: $incoComposition->vitamins,
carbohydratesDetail: $incoComposition->carbohydratesDetail,
fattyAcids: $incoComposition->fattyAcids,
aminoAcids: $incoComposition->aminoAcids,
specialCompounds: $incoComposition->specialCompounds
);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Application\Usecase\IncoCompositions\Find;
use Domain\Models\ValueObjects\Inco\IncoId;
use Shared\DDD\Usecase\Input as UsecaseInput;
final readonly class Input implements UsecaseInput
{
public function __construct(
public IncoId $incoId,
) {
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Application\Usecase\IncoCompositions\Find;
use Domain\Models\ValueObjects\Inco\Composition\AminoAcids;
use Domain\Models\ValueObjects\Inco\Composition\CarbohydratesDetail;
use Domain\Models\ValueObjects\Inco\Composition\Energy;
use Domain\Models\ValueObjects\Inco\Composition\FattyAcids;
use Domain\Models\ValueObjects\Inco\Composition\FreshProduceAndNuts;
use Domain\Models\ValueObjects\Inco\Composition\Identification;
use Domain\Models\ValueObjects\Inco\Composition\IncoCompositionId;
use Domain\Models\ValueObjects\Inco\Composition\Minerals;
use Domain\Models\ValueObjects\Inco\Composition\NutriScore;
use Domain\Models\ValueObjects\Inco\Composition\NutriScorePoints;
use Domain\Models\ValueObjects\Inco\Composition\PortionMacronutrients;
use Domain\Models\ValueObjects\Inco\Composition\ReferenceIntakes;
use Domain\Models\ValueObjects\Inco\Composition\ReferenceMacronutrients;
use Domain\Models\ValueObjects\Inco\Composition\SpecialCompounds;
use Domain\Models\ValueObjects\Inco\Composition\Vitamins;
use Domain\Models\ValueObjects\Inco\IncoId;
use Shared\DDD\Usecase\Output as UsecaseOutput;
final readonly class Output implements UsecaseOutput
{
public function __construct(
public IncoCompositionId $id,
public IncoId $idInco,
public Identification $identification,
public Energy $energy,
public ReferenceMacronutrients $referenceMacronutrients,
public PortionMacronutrients $portionMacronutrients,
public FreshProduceAndNuts $freshProduceAndNuts,
public ReferenceIntakes $referenceIntakes,
public NutriScore $nutriScore,
public NutriScorePoints $nutriScorePoints,
public Minerals $minerals,
public Vitamins $vitamins,
public CarbohydratesDetail $carbohydratesDetail,
public FattyAcids $fattyAcids,
public AminoAcids $aminoAcids,
public SpecialCompounds $specialCompounds,
) {
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Domain\Ports\Directories;
use Domain\Models\Entities\Inco\Composition\IncoComposition;
use Domain\Models\ValueObjects\Inco\Composition\IncoCompositionId;
use Domain\Models\ValueObjects\Inco\IncoId;
use Shared\DDD\Directory;
/** @extends Directory<IncoCompositionId, IncoComposition> */
interface IncoCompositionDirectory extends Directory
{
public function find(IncoCompositionId $id): ?IncoComposition;
public function findByIncoId(IncoId $incoId): ?IncoComposition;
}

View file

@ -0,0 +1,392 @@
<?php
declare(strict_types=1);
namespace Infra\Adapters\Directories;
use Domain\Models\Entities\Inco\Composition\IncoComposition;
use Domain\Models\ValueObjects\Inco\Composition\AminoAcids;
use Domain\Models\ValueObjects\Inco\Composition\CarbohydratesDetail;
use Domain\Models\ValueObjects\Inco\Composition\Energy;
use Domain\Models\ValueObjects\Inco\Composition\EnergyValues;
use Domain\Models\ValueObjects\Inco\Composition\FattyAcids;
use Domain\Models\ValueObjects\Inco\Composition\FreshProduceAndNuts;
use Domain\Models\ValueObjects\Inco\Composition\FruitAndNutAmount;
use Domain\Models\ValueObjects\Inco\Composition\Identification;
use Domain\Models\ValueObjects\Inco\Composition\IncoCompositionId;
use Domain\Models\ValueObjects\Inco\Composition\Minerals;
use Domain\Models\ValueObjects\Inco\Composition\NutrientValue;
use Domain\Models\ValueObjects\Inco\Composition\NutriScore;
use Domain\Models\ValueObjects\Inco\Composition\NutriScorePoints;
use Domain\Models\ValueObjects\Inco\Composition\PercentageValue;
use Domain\Models\ValueObjects\Inco\Composition\PortionMacronutrients;
use Domain\Models\ValueObjects\Inco\Composition\ReferenceIntakes;
use Domain\Models\ValueObjects\Inco\Composition\ReferenceMacronutrients;
use Domain\Models\ValueObjects\Inco\Composition\SpecialCompounds;
use Domain\Models\ValueObjects\Inco\Composition\Vitamins;
use Domain\Models\ValueObjects\Inco\IncoId;
use Domain\Ports\Directories\IncoCompositionDirectory;
use Infra\Database\Models\IncoCompo as IncoCompoModel;
final class EloquentIncoCompositionDirectory implements IncoCompositionDirectory
{
#[\Override]
public function find(IncoCompositionId $id): ?IncoComposition
{
$model = IncoCompoModel::query()
->where('id', $id->value)
->first();
if ($model === null) {
return null;
}
return $this->toDomain($model);
}
#[\Override]
public function findByIncoId(IncoId $incoId): ?IncoComposition
{
$model = IncoCompoModel::query()
->where('id_inco', $incoId->value)
->first();
if ($model === null) {
return null;
}
return $this->toDomain($model);
}
private function toDomain(IncoCompoModel $model): IncoComposition
{
return new IncoComposition(
id: new IncoCompositionId((string) $model->id),
idInco: new IncoId((string) $model->id_inco),
identification: $this->identificationFrom($model),
energy: $this->energyFrom($model),
referenceMacronutrients: $this->refMacronutrientsFrom($model),
portionMacronutrients: $this->portionMacronutrientsFrom($model),
freshProduceAndNuts: $this->freshProduceAndNutsFrom($model),
referenceIntakes: $this->refIntakesFrom($model),
nutriScore: $this->nutriScoreFrom($model),
nutriScorePoints: $this->nutriScorePointsFrom($model),
minerals: $this->mineralsFrom($model),
vitamins: $this->vitaminsFrom($model),
carbohydratesDetail: $this->carbohydratesDetailFrom($model),
fattyAcids: $this->fattyAcidsFrom($model),
aminoAcids: $this->aminoAcidsFrom($model),
specialCompounds: $this->specialCompoundsFrom($model),
);
}
private function identificationFrom(IncoCompoModel $model): Identification
{
$raw = is_array($model->identification) ? $model->identification : [];
return new Identification(
name: $this->getString($raw, 'name', 'Inconnu'),
number: $this->getString($raw, 'number', (string) $model->id_inco),
source: $this->getString($raw, 'source', 'System'),
);
}
private function energyFrom(IncoCompoModel $model): Energy
{
$energyRaw = is_array($model->energy) ? $model->energy : [];
$refArr = is_array($energyRaw['reference'] ?? null) ? $energyRaw['reference'] : [];
$porArr = is_array($energyRaw['portion'] ?? null) ? $energyRaw['portion'] : [];
$refEnergy = new EnergyValues(
size: $this->getStringWithFallback($refArr, 'size', 'portionAmount', '100g'),
energyKj: $this->getStringWithFallback($refArr, 'energyKj', 'kilojoules'),
energyKcal: $this->getStringWithFallback($refArr, 'energyKcal', 'kilocalories'),
);
$porEnergy = new EnergyValues(
size: $this->getStringWithFallback($porArr, 'size', 'portionAmount', '100g'),
energyKj: $this->getStringWithFallback($porArr, 'energyKj', 'kilojoules'),
energyKcal: $this->getStringWithFallback($porArr, 'energyKcal', 'kilocalories'),
);
return new Energy($refEnergy, $porEnergy);
}
private function refMacronutrientsFrom(IncoCompoModel $model): ReferenceMacronutrients
{
$raw = is_array($model->reference_macronutrients) ? $model->reference_macronutrients : [];
return new ReferenceMacronutrients(
fat: $this->getNutrient($raw, 'fat'),
saturatedFat: $this->getNutrient($raw, 'saturatedFat'),
carbohydrates: $this->getNutrient($raw, 'carbohydrates'),
sugars: $this->getNutrient($raw, 'sugars'),
fiber: $this->getNutrient($raw, 'fiber'),
protein: $this->getNutrient($raw, 'protein'),
salt: $this->getNutrient($raw, 'salt'),
);
}
private function portionMacronutrientsFrom(IncoCompoModel $model): PortionMacronutrients
{
$raw = is_array($model->portion_macronutrients) ? $model->portion_macronutrients : [];
return new PortionMacronutrients(
fat: $this->getNutrient($raw, 'fat'),
saturatedFat: $this->getNutrient($raw, 'saturatedFat'),
carbohydrates: $this->getNutrient($raw, 'carbohydrates'),
sugars: $this->getNutrient($raw, 'sugars'),
fiber: $this->getNutrient($raw, 'fiber'),
protein: $this->getNutrient($raw, 'protein'),
salt: $this->getNutrient($raw, 'salt'),
);
}
private function freshProduceAndNutsFrom(IncoCompoModel $model): FreshProduceAndNuts
{
$raw = is_array($model->fresh_produce_and_nuts) ? $model->fresh_produce_and_nuts : [];
$fruitAmount = new FruitAndNutAmount();
return new FreshProduceAndNuts(
reference: $fruitAmount,
portion: $fruitAmount,
freshFnlPercentage: $this->getPercentage($raw, 'freshFnlPercentage'),
fruitsPercentage: $this->getPercentage($raw, 'fruitsPercentage'),
);
}
private function refIntakesFrom(IncoCompoModel $model): ReferenceIntakes
{
$raw = is_array($model->reference_intakes) ? $model->reference_intakes : [];
return new ReferenceIntakes(
energyE14: $this->getPercentage($raw, 'energyE14'),
energyKj: $this->getPercentage($raw, 'energyKj'),
fat: $this->getPercentage($raw, 'fat'),
saturatedFat: $this->getPercentage($raw, 'saturatedFat'),
carbohydrates: $this->getPercentage($raw, 'carbohydrates'),
sugars: $this->getPercentage($raw, 'sugars'),
protein: $this->getPercentage($raw, 'protein'),
salt: $this->getPercentage($raw, 'salt'),
portionAmount: $this->getString($raw, 'portionAmount', '100g'),
);
}
private function nutriScoreFrom(IncoCompoModel $model): NutriScore
{
$raw = is_array($model->nutri_score) ? $model->nutri_score : [];
return new NutriScore(
negativeComponent: $this->getString($raw, 'negativeComponent'),
score: $this->getString($raw, 'score'),
grade: $this->getString($raw, 'grade', 'A'),
);
}
private function nutriScorePointsFrom(IncoCompoModel $model): NutriScorePoints
{
$raw = is_array($model->nutri_score_points) ? $model->nutri_score_points : [];
return new NutriScorePoints(
energyPoints: $this->getStringWithFallback($raw, 'energyPoints', 'energy'),
saturatedFatPoints: $this->getStringWithFallback($raw, 'saturatedFatPoints', 'saturatedFat'),
sugarsPoints: $this->getStringWithFallback($raw, 'sugarsPoints', 'sugars'),
saltPoints: $this->getStringWithFallback($raw, 'saltPoints', 'sodium'),
fiberPoints: $this->getStringWithFallback($raw, 'fiberPoints', 'fiber'),
proteinPoints: $this->getStringWithFallback($raw, 'proteinPoints', 'proteins'),
freshFnlPoints: $this->getStringWithFallback($raw, 'freshFnlPoints', 'fruitsNuts'),
);
}
private function mineralsFrom(IncoCompoModel $model): Minerals
{
$raw = is_array($model->minerals) ? $model->minerals : [];
return new Minerals(
calcium: $this->getString($raw, 'calcium'),
magnesium: $this->getString($raw, 'magnesium'),
potassium: $this->getString($raw, 'potassium'),
phosphorus: $this->getString($raw, 'phosphorus'),
chloride: $this->getString($raw, 'chloride'),
iron: $this->getString($raw, 'iron'),
zinc: $this->getString($raw, 'zinc'),
copper: $this->getString($raw, 'copper'),
manganese: $this->getString($raw, 'manganese'),
selenium: $this->getString($raw, 'selenium'),
iodine: $this->getString($raw, 'iodine'),
chromium: $this->getString($raw, 'chromium'),
molybdenum: $this->getString($raw, 'molybdenum'),
sulfur: $this->getString($raw, 'sulfur'),
nitrates: $this->getString($raw, 'nitrates'),
);
}
private function vitaminsFrom(IncoCompoModel $model): Vitamins
{
$raw = is_array($model->vitamins) ? $model->vitamins : [];
return new Vitamins(
vitaminA: $this->getString($raw, 'vitaminA'),
vitaminB6: $this->getString($raw, 'vitaminB6'),
vitaminB12: $this->getString($raw, 'vitaminB12'),
vitaminC: $this->getString($raw, 'vitaminC'),
vitaminD: $this->getString($raw, 'vitaminD'),
vitaminE: $this->getString($raw, 'vitaminE'),
vitaminK: $this->getString($raw, 'vitaminK'),
cholecalciferol: $this->getString($raw, 'cholecalciferol'),
niacin: $this->getString($raw, 'niacin'),
riboflavin: $this->getString($raw, 'riboflavin'),
thiamine: $this->getString($raw, 'thiamine'),
pantothenicAcid: $this->getString($raw, 'pantothenicAcid'),
biotin: $this->getString($raw, 'biotin'),
folates: $this->getString($raw, 'folates'),
);
}
private function carbohydratesDetailFrom(IncoCompoModel $model): CarbohydratesDetail
{
$raw = is_array($model->carbohydrates_detail) ? $model->carbohydrates_detail : [];
return new CarbohydratesDetail(
starch: $this->getString($raw, 'starch'),
maltodextrin: $this->getString($raw, 'maltodextrin'),
sucrose: $this->getString($raw, 'sucrose'),
glucose: $this->getString($raw, 'glucose'),
lactose: $this->getString($raw, 'lactose'),
maltose: $this->getString($raw, 'maltose'),
galactose: $this->getString($raw, 'galactose'),
carbohydrateHydrates: $this->getString($raw, 'carbohydrateHydrates'),
boundFructose: $this->getString($raw, 'boundFructose'),
freeFructose: $this->getString($raw, 'freeFructose'),
oligosaccharides: $this->getString($raw, 'oligosaccharides'),
polyols: $this->getString($raw, 'polyols'),
solubleFiber: $this->getString($raw, 'solubleFiber'),
inositol: $this->getString($raw, 'inositol'),
);
}
private function fattyAcidsFrom(IncoCompoModel $model): FattyAcids
{
$raw = is_array($model->fatty_acids) ? $model->fatty_acids : [];
return new FattyAcids(
fd: $this->getString($raw, 'fd'),
cholesterol: $this->getString($raw, 'cholesterol'),
transFat: $this->getString($raw, 'transFat'),
formic: $this->getString($raw, 'formic'),
acetic: $this->getString($raw, 'acetic'),
propionic: $this->getString($raw, 'propionic'),
butyric: $this->getString($raw, 'butyric'),
valeric: $this->getString($raw, 'valeric'),
caproic: $this->getString($raw, 'caproic'),
enanthic: $this->getString($raw, 'enanthic'),
caprylic: $this->getString($raw, 'caprylic'),
pelargonic: $this->getString($raw, 'pelargonic'),
capric: $this->getString($raw, 'capric'),
undecylic: $this->getString($raw, 'undecylic'),
lauric: $this->getString($raw, 'lauric'),
tridecylic: $this->getString($raw, 'tridecylic'),
myristic: $this->getString($raw, 'myristic'),
pentadecylic: $this->getString($raw, 'pentadecylic'),
f16d0: $this->getString($raw, 'f16d0'),
margaric: $this->getString($raw, 'margaric'),
stearic: $this->getString($raw, 'stearic'),
arachidic: $this->getString($raw, 'arachidic'),
heneicosylic: $this->getString($raw, 'heneicosylic'),
behenic: $this->getString($raw, 'behenic'),
tricosylic: $this->getString($raw, 'tricosylic'),
lignoceric: $this->getString($raw, 'lignoceric'),
carboceric: $this->getString($raw, 'carboceric'),
cerotic: $this->getString($raw, 'cerotic'),
ceroplastic: $this->getString($raw, 'ceroplastic'),
montanic: $this->getString($raw, 'montanic'),
nonacosylic: $this->getString($raw, 'nonacosylic'),
melissic: $this->getString($raw, 'melissic'),
laceric: $this->getString($raw, 'laceric'),
hentriacontylic: $this->getString($raw, 'hentriacontylic'),
hextriacontylic: $this->getString($raw, 'hextriacontylic'),
heptatriacontylic: $this->getString($raw, 'heptatriacontylic'),
octatriacontylic: $this->getString($raw, 'octatriacontylic'),
nonadylie: $this->getString($raw, 'nonadylie'),
psyllic: $this->getString($raw, 'psyllic'),
palmitic: $this->getString($raw, 'palmitic'),
oleic: $this->getString($raw, 'oleic'),
f18d: $this->getString($raw, 'f18d'),
f19d: $this->getString($raw, 'f19d'),
f11eico: $this->getString($raw, 'f11eico'),
geddic: $this->getString($raw, 'geddic'),
erucic: $this->getString($raw, 'erucic'),
nervonic: $this->getString($raw, 'nervonic'),
hyenic: $this->getString($raw, 'hyenic'),
amscis: $this->getString($raw, 'amscis'),
f18d2cn6: $this->getString($raw, 'f18d2cn6'),
f18d3n3: $this->getString($raw, 'f18d3n3'),
f20d4: $this->getString($raw, 'f20d4'),
f20d5n3: $this->getString($raw, 'f20d5n3'),
f22d6n3: $this->getString($raw, 'f22d6n3'),
fapun3: $this->getString($raw, 'fapun3'),
fapun6: $this->getString($raw, 'fapun6'),
fapun9: $this->getString($raw, 'fapun9'),
fapucis: $this->getString($raw, 'fapucis'),
fapulc: $this->getString($raw, 'fapulc'),
);
}
private function aminoAcidsFrom(IncoCompoModel $model): AminoAcids
{
$raw = is_array($model->amino_acids) ? $model->amino_acids : [];
return new AminoAcids(
essentialAminoAcids: $this->getString($raw, 'essentialAminoAcids'),
caseins: $this->getString($raw, 'caseins'),
wheyProtein: $this->getString($raw, 'wheyProtein'),
);
}
private function specialCompoundsFrom(IncoCompoModel $model): SpecialCompounds
{
$raw = is_array($model->special_compounds) ? $model->special_compounds : [];
return new SpecialCompounds(
choline: $this->getString($raw, 'choline'),
carnitine: $this->getString($raw, 'carnitine'),
taurine: $this->getString($raw, 'taurine'),
nucleotides: $this->getString($raw, 'nucleotides'),
);
}
/**
* @param array<mixed> $data
*/
private function getString(array $data, string $key, string $default = '0'): string
{
$val = $data[$key] ?? null;
return is_scalar($val) ? (string) $val : $default;
}
/**
* @param array<mixed> $data
*/
private function getStringWithFallback(array $data, string $key, string $fallbackKey, string $default = '0'): string
{
$val = $data[$key] ?? $data[$fallbackKey] ?? null;
return is_scalar($val) ? (string) $val : $default;
}
/**
* @param array<mixed> $data
*/
private function getNutrient(array $data, string $key, string $default = '0'): NutrientValue
{
return new NutrientValue($this->getString($data, $key, $default));
}
/**
* @param array<mixed> $data
*/
private function getPercentage(array $data, string $key, string $default = '0'): PercentageValue
{
return new PercentageValue($this->getString($data, $key, $default));
}
}

View file

@ -134,8 +134,8 @@ final class EloquentIncoSheetDirectory implements IncoSheetDirectory
allegations: $allegations,
sellArguments: (string) $model->sell_arguments,
preparationAdvices: (string) $model->preparation_advices,
tips: $model->tips !== null ? (string) $model->tips : null,
sellFamilly: $model->sell_familly !== null ? (string) $model->sell_familly : null,
tips: $this->toNullableString($model->tips),
sellFamilly: $this->toNullableString($model->sell_familly),
ingredients: $ingredients,
portions: $portions,
allergens: $allergens,
@ -153,50 +153,38 @@ final class EloquentIncoSheetDirectory implements IncoSheetDirectory
private function allegationsFrom(IncoSheetModel $model): Allegations
{
$allegationsRaw = is_array($model->allegations) ? $model->allegations : [];
/** @var array<string> $allegationsList */
$allegationsList = is_array($allegationsRaw['allegations'] ?? null) ? $allegationsRaw['allegations'] : [];
/** @var array<string> $healthList */
$healthList = is_array($allegationsRaw['health'] ?? null) ? $allegationsRaw['health'] : [];
/** @var array<string> $nutritionList */
$nutritionList = is_array($allegationsRaw['nutrition'] ?? null) ? $allegationsRaw['nutrition'] : [];
/** @var array<string> $otherList */
$otherList = is_array($allegationsRaw['other'] ?? null) ? $allegationsRaw['other'] : [];
$raw = is_array($model->allegations) ? $model->allegations : [];
return new Allegations(
allegations: $allegationsList,
health: $healthList,
nutrition: $nutritionList,
other: $otherList,
allegations: $this->getArrayOfStrings($raw, 'allegations'),
health: $this->getArrayOfStrings($raw, 'health'),
nutrition: $this->getArrayOfStrings($raw, 'nutrition'),
other: $this->getArrayOfStrings($raw, 'other'),
);
}
private function ingredientsFrom(IncoSheetModel $model): Ingredients
{
$ingredientsRaw = is_array($model->ingredients) ? $model->ingredients : [];
$raw = is_array($model->ingredients) ? $model->ingredients : [];
/** @var array<int|string, array<string>> $ingredientsGroups */
$ingredientsGroups = $ingredientsRaw;
$ingredientsGroups = $raw;
return new Ingredients($ingredientsGroups);
}
private function portionsFrom(IncoSheetModel $model): ?PortionAmount
{
$portionsRaw = is_array($model->portions) ? $model->portions : null;
$raw = is_array($model->portions) ? $model->portions : null;
if ($portionsRaw === null) {
if ($raw === null) {
return null;
}
$lowPartVal = is_numeric($portionsRaw['lowPart'] ?? null) ? (float) $portionsRaw['lowPart'] : 0.0;
$highPartVal = is_numeric($portionsRaw['highPart'] ?? null) ? (float) $portionsRaw['highPart'] : 0.0;
return new PortionAmount(
lowPart: $lowPartVal,
highPart: $highPartVal,
approximative: (bool) ($portionsRaw['approximative'] ?? false),
lowPart: $this->getFloat($raw, 'lowPart'),
highPart: $this->getFloat($raw, 'highPart'),
approximative: (bool) ($raw['approximative'] ?? false),
);
}
@ -217,52 +205,52 @@ final class EloquentIncoSheetDirectory implements IncoSheetDirectory
private function brandFrom(IncoSheetModel $model): Brand
{
$brandRaw = is_array($model->brand) ? $model->brand : ['code' => 'M01', 'label' => 'Standard'];
$raw = is_array($model->brand) ? $model->brand : [];
return new Brand(
code: is_scalar($brandRaw['code'] ?? null) ? (string) $brandRaw['code'] : 'M01',
label: is_scalar($brandRaw['label'] ?? null) ? (string) $brandRaw['label'] : 'Standard',
code: $this->getString($raw, 'code', 'M01'),
label: $this->getString($raw, 'label', 'Standard'),
);
}
private function weightFrom(IncoSheetModel $model): ?IncoWeight
{
$weightRaw = is_array($model->weight) ? $model->weight : null;
$raw = is_array($model->weight) ? $model->weight : null;
if ($weightRaw === null) {
if ($raw === null) {
return null;
}
return new IncoWeight(
netWeight: is_numeric($weightRaw['netWeight'] ?? null) ? (int) $weightRaw['netWeight'] : 0,
grossWeight: is_numeric($weightRaw['grossWeight'] ?? null) ? (int) $weightRaw['grossWeight'] : 0,
theoreticalAverage: is_numeric($weightRaw['theoreticalAverage'] ?? null) ? (int) $weightRaw['theoreticalAverage'] : 0,
netWeight: $this->getInt($raw, 'netWeight'),
grossWeight: $this->getInt($raw, 'grossWeight'),
theoreticalAverage: $this->getInt($raw, 'theoreticalAverage'),
);
}
private function nutriscoreFrom(IncoSheetModel $model): ?Nutriscore
{
$nutriscoreRaw = is_array($model->nutriscore) ? $model->nutriscore : null;
$raw = is_array($model->nutriscore) ? $model->nutriscore : null;
if ($nutriscoreRaw === null) {
if ($raw === null) {
return null;
}
return new Nutriscore(
grade: is_scalar($nutriscoreRaw['grade'] ?? null) ? (string) $nutriscoreRaw['grade'] : 'A',
grade: $this->getString($raw, 'grade', 'A'),
);
}
private function storageConditionFrom(IncoSheetModel $model): ?StorageCondition
{
$storageRaw = is_array($model->storage_condition) ? $model->storage_condition : null;
$raw = is_array($model->storage_condition) ? $model->storage_condition : null;
if ($storageRaw === null) {
if ($raw === null) {
return null;
}
return new StorageCondition(
instructions: is_scalar($storageRaw['instructions'] ?? null) ? (string) $storageRaw['instructions'] : '',
instructions: $this->getString($raw, 'instructions', ''),
);
}
@ -281,6 +269,63 @@ final class EloquentIncoSheetDirectory implements IncoSheetDirectory
return $additives;
}
/**
* @param array<mixed> $data
*/
private function getString(array $data, string $key, string $default = ''): string
{
$val = $data[$key] ?? null;
return is_scalar($val) ? (string) $val : $default;
}
private function toNullableString(mixed $val): ?string
{
return is_scalar($val) ? (string) $val : null;
}
/**
* @param array<mixed> $data
* @return array<string>
*/
private function getArrayOfStrings(array $data, string $key): array
{
$val = $data[$key] ?? null;
if (! is_array($val)) {
return [];
}
$result = [];
foreach ($val as $item) {
if (is_string($item)) {
$result[] = $item;
}
}
return $result;
}
/**
* @param array<mixed> $data
*/
private function getInt(array $data, string $key, int $default = 0): int
{
$val = $data[$key] ?? null;
return is_numeric($val) ? (int) $val : $default;
}
/**
* @param array<mixed> $data
*/
private function getFloat(array $data, string $key, float $default = 0.0): float
{
$val = $data[$key] ?? null;
return is_numeric($val) ? (float) $val : $default;
}
private function toDateTime(\DateTimeInterface | string $value): \DateTimeInterface
{
if ($value instanceof \DateTimeInterface) {

View file

@ -5,9 +5,11 @@ declare(strict_types=1);
namespace Infra\Providers;
use Domain\Ports\Directories\EmployeeDirectory;
use Domain\Ports\Directories\IncoCompositionDirectory;
use Domain\Ports\Directories\IncoSheetDirectory;
use Illuminate\Support\ServiceProvider;
use Infra\Adapters\Directories\EloquentEmployeeDirectory;
use Infra\Adapters\Directories\EloquentIncoCompositionDirectory;
use Infra\Adapters\Directories\EloquentIncoSheetDirectory;
use Infra\Usecase\LaravelExecutor;
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
@ -19,6 +21,7 @@ class DomainServiceProvider extends ServiceProvider
{
$this->app->bind(EmployeeDirectory::class, EloquentEmployeeDirectory::class);
$this->app->bind(IncoSheetDirectory::class, EloquentIncoSheetDirectory::class);
$this->app->bind(IncoCompositionDirectory::class, EloquentIncoCompositionDirectory::class);
$this->app->singleton(UsecaseExecutor::class, LaravelExecutor::class);
}
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Presentation\Api\V1\Dto\IncoCompositions;
use Presentation\Api\V1\Contracts\ApiResponseDto;
final readonly class IncoCompositionDto implements ApiResponseDto
{
/**
* @param array<string, mixed> $identification
* @param array<string, mixed> $energy
* @param array<string, mixed> $referenceMacronutrients
* @param array<string, mixed> $portionMacronutrients
* @param array<string, mixed> $freshProduceAndNuts
* @param array<string, mixed> $referenceIntakes
* @param array<string, mixed> $nutriScore
* @param array<string, mixed> $nutriScorePoints
* @param array<string, mixed> $minerals
* @param array<string, mixed> $vitamins
* @param array<string, mixed> $carbohydratesDetail
* @param array<string, mixed> $fattyAcids
* @param array<string, mixed> $aminoAcids
* @param array<string, mixed> $specialCompounds
*/
public function __construct(
public string $id,
public string $incoId,
public array $identification,
public array $energy,
public array $referenceMacronutrients,
public array $portionMacronutrients,
public array $freshProduceAndNuts,
public array $referenceIntakes,
public array $nutriScore,
public array $nutriScorePoints,
public array $minerals,
public array $vitamins,
public array $carbohydratesDetail,
public array $fattyAcids,
public array $aminoAcids,
public array $specialCompounds,
) {
}
/** @return array<string, mixed> */
#[\Override]
public function toArray(): array
{
/** @var array<string, mixed> */
return get_object_vars($this);
}
}

View file

@ -25,16 +25,18 @@ final readonly class EmployeeShowController
public function __invoke(EmployeeShowRequest $request): ApiResponse
{
$input = $this->inputFrom($request);
$response = UsecaseBuilder::using($this->executor)
->do($this->usecase)
->from($this->toInput($request))
->from($input)
->with($this->presenter)
->run();
return new EmployeeShowResponse($response);
}
private function toInput(EmployeeShowRequest $request): Input
private function inputFrom(EmployeeShowRequest $request): Input
{
return new Input(
id: $request->employeeId(),

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Presentation\Api\V1\Http\Controllers\IncoCompositions;
use Application\Usecase\IncoCompositions\Find\FindIncoComposition;
use Application\Usecase\IncoCompositions\Find\Input;
use Presentation\Api\V1\Contracts\ApiResponse;
use Presentation\Api\V1\Http\Requests\IncoCompositions\IncoCompositionShowRequest;
use Presentation\Api\V1\Http\Responses\IncoCompositions\IncoCompositionShowResponse;
use Presentation\Api\V1\Presenters\IncoCompositions\IncoCompositionShowPresenter;
use Shared\DDD\Usecase\Builder\UsecaseBuilder;
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
final readonly class IncoCompositionShowController
{
/** @param \Shared\DDD\Usecase\Builder\UsecaseExecutor<\Shared\DDD\Usecase\Input, \Shared\DDD\Usecase\Output> $executor */
public function __construct(
private FindIncoComposition $usecase,
private UsecaseExecutor $executor,
private IncoCompositionShowPresenter $presenter,
) {
}
public function __invoke(IncoCompositionShowRequest $request): ApiResponse
{
$input = $this->inputFrom($request);
$view = UsecaseBuilder::using($this->executor)
->do($this->usecase)
->from($input)
->with($this->presenter)
->run();
return new IncoCompositionShowResponse($view);
}
private function inputFrom(IncoCompositionShowRequest $request): Input
{
return new Input(
incoId: $request->incoId(),
);
}
}

View file

@ -25,11 +25,7 @@ final readonly class IncoSheetsIndexController
public function __invoke(IncoSheetsIndexRequest $request): ApiResponse
{
$input = new Input(
pagination: $request->pagination(),
sorter: $request->sorter(),
filter: $request->filter(),
);
$input = $this->inputFrom($request);
$response = UsecaseBuilder::using($this->executor)
->do($this->usecase)
@ -39,4 +35,13 @@ final readonly class IncoSheetsIndexController
return new IncoSheetsIndexResponse($response, $request->toLeagueUri());
}
private function inputFrom(IncoSheetsIndexRequest $request): Input
{
return new Input(
pagination: $request->pagination(),
sorter: $request->sorter(),
filter: $request->filter(),
);
}
}

View file

@ -24,10 +24,7 @@ final readonly class IncoSheetsSearchController
public function __invoke(IncoSheetsSearchRequest $request): ApiResponse
{
$input = new SearchInput(
$request->incoIdSearch(),
...$request->brands()
);
$input = $this->inputFrom($request);
$view = UsecaseBuilder::using($this->executor)
->do($this->usecase)
@ -40,4 +37,12 @@ final readonly class IncoSheetsSearchController
baseUri: $request->toLeagueUri(),
);
}
private function inputFrom(IncoSheetsSearchRequest $request): SearchInput
{
return new SearchInput(
$request->incoIdSearch(),
...$request->brands()
);
}
}

View file

@ -25,9 +25,7 @@ final readonly class IncoSheetsShowController
public function __invoke(IncoSheetsShowRequest $request): ApiResponse
{
$input = new Input(
id: $request->incoId(),
);
$input = $this->inputFrom($request);
$response = UsecaseBuilder::using($this->executor)
->do($this->usecase)
@ -37,4 +35,11 @@ final readonly class IncoSheetsShowController
return new IncoSheetsShowResponse($response);
}
private function inputFrom(IncoSheetsShowRequest $request): Input
{
return new Input(
id: $request->incoId(),
);
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Presentation\Api\V1\Http\Requests\IncoCompositions;
use Domain\Models\ValueObjects\Inco\IncoId;
use Illuminate\Foundation\Http\FormRequest;
final class IncoCompositionShowRequest extends FormRequest
{
/** @return array<string, array<int, mixed>> */
public function rules(): array
{
return [
'incoId' => ['required', 'string'],
];
}
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'incoId' => $this->route('incoId'),
]);
}
public function incoId(): IncoId
{
return new IncoId((string) $this->string('incoId')->value());
}
}

View file

@ -0,0 +1,287 @@
<?php
declare(strict_types=1);
namespace Presentation\Api\V1\Http\Responses\IncoCompositions;
use Application\Usecase\IncoCompositions\Find\Output;
use Illuminate\Http\JsonResponse;
use Presentation\Api\V1\Contracts\ApiResponse;
use Presentation\Api\V1\Dto\IncoCompositions\IncoCompositionDto;
use Presentation\Api\V1\Presenters\IncoCompositions\IncoCompositionShowView;
final readonly class IncoCompositionShowResponse implements ApiResponse
{
public function __construct(
private IncoCompositionShowView $view,
) {
}
#[\Override]
public function toResponse($request): JsonResponse
{
return $this->present();
}
#[\Override]
public function present(): JsonResponse
{
return response()->json([
'composition' => $this->fromOutput($this->view->output)->toArray(),
]);
}
private function fromOutput(Output $output): IncoCompositionDto
{
return new IncoCompositionDto(
id: (string) $output->id,
incoId: (string) $output->idInco,
identification: [
'name' => $output->identification->name,
'number' => $output->identification->number,
'source' => $output->identification->source,
],
energy: [
'reference' => [
'size' => $output->energy->reference->size,
'energyKj' => $output->energy->reference->energyKj,
'energyKcal' => $output->energy->reference->energyKcal,
],
'portion' => [
'size' => $output->energy->portion->size,
'energyKj' => $output->energy->portion->energyKj,
'energyKcal' => $output->energy->portion->energyKcal,
],
],
referenceMacronutrients: [
'fat' => [
'amount' => $output->referenceMacronutrients->fat->amount,
'precision' => $output->referenceMacronutrients->fat->precision,
],
'saturatedFat' => [
'amount' => $output->referenceMacronutrients->saturatedFat->amount,
'precision' => $output->referenceMacronutrients->saturatedFat->precision,
],
'carbohydrates' => [
'amount' => $output->referenceMacronutrients->carbohydrates->amount,
'precision' => $output->referenceMacronutrients->carbohydrates->precision,
],
'sugars' => [
'amount' => $output->referenceMacronutrients->sugars->amount,
'precision' => $output->referenceMacronutrients->sugars->precision,
],
'fiber' => [
'amount' => $output->referenceMacronutrients->fiber->amount,
'precision' => $output->referenceMacronutrients->fiber->precision,
],
'protein' => [
'amount' => $output->referenceMacronutrients->protein->amount,
'precision' => $output->referenceMacronutrients->protein->precision,
],
'salt' => [
'amount' => $output->referenceMacronutrients->salt->amount,
'precision' => $output->referenceMacronutrients->salt->precision,
],
'sodium' => '0',
],
portionMacronutrients: [
'fat' => [
'amount' => $output->portionMacronutrients->fat->amount,
'precision' => $output->portionMacronutrients->fat->precision,
],
'saturatedFat' => [
'amount' => $output->portionMacronutrients->saturatedFat->amount,
'precision' => $output->portionMacronutrients->saturatedFat->precision,
],
'carbohydrates' => [
'amount' => $output->portionMacronutrients->carbohydrates->amount,
'precision' => $output->portionMacronutrients->carbohydrates->precision,
],
'sugars' => [
'amount' => $output->portionMacronutrients->sugars->amount,
'precision' => $output->portionMacronutrients->sugars->precision,
],
'fiber' => [
'amount' => $output->portionMacronutrients->fiber->amount,
'precision' => $output->portionMacronutrients->fiber->precision,
],
'protein' => [
'amount' => $output->portionMacronutrients->protein->amount,
'precision' => $output->portionMacronutrients->protein->precision,
],
'salt' => [
'amount' => $output->portionMacronutrients->salt->amount,
'precision' => $output->portionMacronutrients->salt->precision,
],
'sodium' => '0',
],
freshProduceAndNuts: [
'reference' => [
'fruits' => $output->freshProduceAndNuts->reference->fruits,
'nuts' => $output->freshProduceAndNuts->reference->nuts,
'freshFnl' => $output->freshProduceAndNuts->reference->freshFnl,
],
'portion' => [
'fruits' => $output->freshProduceAndNuts->portion->fruits,
'nuts' => $output->freshProduceAndNuts->portion->nuts,
'freshFnl' => $output->freshProduceAndNuts->portion->freshFnl,
],
'freshFnlPercentage' => [
'value' => $output->freshProduceAndNuts->freshFnlPercentage->value,
],
'fruitsPercentage' => [
'value' => $output->freshProduceAndNuts->fruitsPercentage->value,
],
],
referenceIntakes: [
'energyE14' => ['value' => $output->referenceIntakes->energyE14->value],
'energyKj' => ['value' => $output->referenceIntakes->energyKj->value],
'fat' => ['value' => $output->referenceIntakes->fat->value],
'saturatedFat' => ['value' => $output->referenceIntakes->saturatedFat->value],
'carbohydrates' => ['value' => $output->referenceIntakes->carbohydrates->value],
'sugars' => ['value' => $output->referenceIntakes->sugars->value],
'protein' => ['value' => $output->referenceIntakes->protein->value],
'salt' => ['value' => $output->referenceIntakes->salt->value],
'portionAmount' => $output->referenceIntakes->portionAmount,
'proteinToLipidRatio' => $output->referenceIntakes->proteinToLipidRatio,
],
nutriScore: [
'negativeComponent' => $output->nutriScore->negativeComponent,
'negativeComponent2022' => $output->nutriScore->negativeComponent2022 ?? '0',
'positiveComponent' => $output->nutriScore->positiveComponent,
'positiveComponent2022' => $output->nutriScore->positiveComponent2022 ?? '0',
'score' => $output->nutriScore->score,
'score2022' => $output->nutriScore->score2022 ?? '0',
'grade' => $output->nutriScore->grade,
'grade2022' => $output->nutriScore->grade2022 ?? 'A',
'isGradeAArticle' => $output->nutriScore->isGradeAArticle ?? '0',
'isRedMeat' => $output->nutriScore->isRedMeat ?? '0',
],
nutriScorePoints: [
'energyPoints' => $output->nutriScorePoints->energyPoints,
'saturatedFatPoints' => $output->nutriScorePoints->saturatedFatPoints,
'sugarsPoints' => $output->nutriScorePoints->sugarsPoints,
'saltPoints' => $output->nutriScorePoints->saltPoints,
'fiberPoints' => $output->nutriScorePoints->fiberPoints,
'proteinPoints' => $output->nutriScorePoints->proteinPoints,
'freshFnlPoints' => $output->nutriScorePoints->freshFnlPoints,
],
minerals: [
'calcium' => $output->minerals->calcium,
'magnesium' => $output->minerals->magnesium,
'potassium' => $output->minerals->potassium,
'phosphorus' => $output->minerals->phosphorus,
'chloride' => $output->minerals->chloride,
'iron' => $output->minerals->iron,
'zinc' => $output->minerals->zinc,
'copper' => $output->minerals->copper,
'manganese' => $output->minerals->manganese,
'selenium' => $output->minerals->selenium,
'iodine' => $output->minerals->iodine,
'chromium' => $output->minerals->chromium,
'molybdenum' => $output->minerals->molybdenum,
'sulfur' => $output->minerals->sulfur,
'nitrates' => $output->minerals->nitrates,
],
vitamins: [
'vitaminA' => $output->vitamins->vitaminA,
'vitaminB6' => $output->vitamins->vitaminB6,
'vitaminB12' => $output->vitamins->vitaminB12,
'vitaminC' => $output->vitamins->vitaminC,
'vitaminD' => $output->vitamins->vitaminD,
'vitaminE' => $output->vitamins->vitaminE,
'vitaminK' => $output->vitamins->vitaminK,
'cholecalciferol' => $output->vitamins->cholecalciferol,
'niacin' => $output->vitamins->niacin,
],
carbohydratesDetail: [
'starch' => $output->carbohydratesDetail->starch,
'maltodextrin' => $output->carbohydratesDetail->maltodextrin,
'sucrose' => $output->carbohydratesDetail->sucrose,
'glucose' => $output->carbohydratesDetail->glucose,
'lactose' => $output->carbohydratesDetail->lactose,
'maltose' => $output->carbohydratesDetail->maltose,
'galactose' => $output->carbohydratesDetail->galactose,
'carbohydrateHydrates' => $output->carbohydratesDetail->carbohydrateHydrates,
'boundFructose' => $output->carbohydratesDetail->boundFructose,
'freeFructose' => $output->carbohydratesDetail->freeFructose,
'oligosaccharides' => $output->carbohydratesDetail->oligosaccharides,
'polyols' => $output->carbohydratesDetail->polyols,
'solubleFiber' => $output->carbohydratesDetail->solubleFiber,
'inositol' => $output->carbohydratesDetail->inositol,
],
fattyAcids: [
'nutFd' => $output->fattyAcids->fd,
'nutChol' => $output->fattyAcids->cholesterol,
'nutAgt' => $output->fattyAcids->transFat,
'nutFformiq' => $output->fattyAcids->formic,
'nutFacetiq' => $output->fattyAcids->acetic,
'nutFpropioniq' => $output->fattyAcids->propionic,
'nutFbutyriq' => $output->fattyAcids->butyric,
'nutFvaleriq' => $output->fattyAcids->valeric,
'nutFcaproiq' => $output->fattyAcids->caproic,
'nutFenatiq' => $output->fattyAcids->enanthic,
'nutFcapryliq' => $output->fattyAcids->caprylic,
'nutFpelargoniq' => $output->fattyAcids->pelargonic,
'nutFcapriq' => $output->fattyAcids->capric,
'nutFundecyliq' => $output->fattyAcids->undecylic,
'nutFlauriq' => $output->fattyAcids->lauric,
'nutFtridecyliq' => $output->fattyAcids->tridecylic,
'nutFmyristiq' => $output->fattyAcids->myristic,
'nutFpentadecyliq' => $output->fattyAcids->pentadecylic,
'nutF16d0' => $output->fattyAcids->f16d0,
'nutFmargariq' => $output->fattyAcids->margaric,
'nutFsteariq' => $output->fattyAcids->stearic,
'nutFarachidiq' => $output->fattyAcids->arachidic,
'nutFheneicosyliq' => $output->fattyAcids->heneicosylic,
'nutFbeheniq' => $output->fattyAcids->behenic,
'nutFtricosiliq' => $output->fattyAcids->tricosylic,
'nutFlignoceriq' => $output->fattyAcids->lignoceric,
'nutFcarboceriq' => $output->fattyAcids->carboceric,
'nutFcerotiq' => $output->fattyAcids->cerotic,
'nutFceroplastiq' => $output->fattyAcids->ceroplastic,
'nutFmontaniq' => $output->fattyAcids->montanic,
'nutFnonacosyliq' => $output->fattyAcids->nonacosylic,
'nutFmelissiq' => $output->fattyAcids->melissic,
'nutFlaceroiq' => $output->fattyAcids->laceric,
'nutFhentriacontyliq' => $output->fattyAcids->hentriacontylic,
'nutFhextriacontyliq' => $output->fattyAcids->hextriacontylic,
'nutFheptatriacontyliq' => $output->fattyAcids->heptatriacontylic,
'nutFoctatriacontyliq' => $output->fattyAcids->octatriacontylic,
'nutFnonadylique' => $output->fattyAcids->nonadylie,
'nutFpsylliq' => $output->fattyAcids->psyllic,
'nutFpalm' => $output->fattyAcids->palmitic,
'nutFoleiq' => $output->fattyAcids->oleic,
'nutF18d' => $output->fattyAcids->f18d,
'nutF19d' => $output->fattyAcids->f19d,
'nutF11eico' => $output->fattyAcids->f11eico,
'nutFgeddiq' => $output->fattyAcids->geddic,
'nutFeruciq' => $output->fattyAcids->erucic,
'nutFnervoniq' => $output->fattyAcids->nervonic,
'nutFhyeniq' => $output->fattyAcids->hyenic,
'nutFamscis' => $output->fattyAcids->amscis,
'nutF18d2cn6' => $output->fattyAcids->f18d2cn6,
'nutF18d3n3' => $output->fattyAcids->f18d3n3,
'nutF20d4' => $output->fattyAcids->f20d4,
'nutF20d5n3' => $output->fattyAcids->f20d5n3,
'nutF22d6n3' => $output->fattyAcids->f22d6n3,
'nutFapun3' => $output->fattyAcids->fapun3,
'nutFapun6' => $output->fattyAcids->fapun6,
'nutFapun9' => $output->fattyAcids->fapun9,
'nutFapucis' => $output->fattyAcids->fapucis,
'nutFapulc' => $output->fattyAcids->fapulc,
],
aminoAcids: [
'essentialAminoAcids' => $output->aminoAcids->essentialAminoAcids,
'caseins' => $output->aminoAcids->caseins,
'wheyProtein' => $output->aminoAcids->wheyProtein,
],
specialCompounds: [
'choline' => $output->specialCompounds->choline,
'carnitine' => $output->specialCompounds->carnitine,
'taurine' => $output->specialCompounds->taurine,
'nucleotides' => $output->specialCompounds->nucleotides,
],
);
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Presentation\Api\V1\Presenters\IncoCompositions;
use Application\Usecase\IncoCompositions\Find\Output;
use Shared\DDD\Usecase\Output as UsecaseOutput;
use Shared\DDD\Usecase\Presenter;
use Webmozart\Assert\Assert;
/** @implements Presenter<\Shared\DDD\Usecase\Output, IncoCompositionShowView> */
final readonly class IncoCompositionShowPresenter implements Presenter
{
#[\Override]
public function present(UsecaseOutput $output): object
{
Assert::isInstanceOf($output, Output::class);
return IncoCompositionShowView::fromOutput($output);
}
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Presentation\Api\V1\Presenters\IncoCompositions;
use Application\Usecase\IncoCompositions\Find\Output;
final readonly class IncoCompositionShowView
{
public function __construct(
public Output $output,
) {
}
public static function fromOutput(Output $output): self
{
return new self($output);
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Api\V1\IncoCompositions;
use Infra\Database\Models\IncoCompo;
use Infra\Database\Models\IncoSheet;
describe('IncoCompositions Show Feature API', function (): void {
it('returns 200 OK with inco composition details by inco sheet ID', function (): void {
// GIVEN
/** @var \Tests\TestCase $test */
$test = $this;
$sheet = IncoSheet::factory()->create([
'id' => '12345',
'compo_id' => '67890',
]);
IncoCompo::factory()->create([
'id' => '67890',
'id_inco' => '12345',
'identification' => [
'name' => 'Poulet Rôti 1kg Test',
'number' => '12345',
'source' => 'LDC',
],
]);
// WHEN
$response = $test->getJson("/api/v1/inco-sheets/{$sheet->id}/composition");
// THEN
$response->assertStatus(200);
$response->assertJson([
'composition' => [
'id' => '67890',
'incoId' => '12345',
'identification' => [
'name' => 'Poulet Rôti 1kg Test',
],
],
]);
});
it('returns 404 Not Found when composition does not exist', function (): void {
// GIVEN
/** @var \Tests\TestCase $test */
$test = $this;
// WHEN
$response = $test->getJson('/api/v1/inco-sheets/999999/composition');
// THEN
$response->assertStatus(404);
});
});

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\OpenApi\V1\IncoCompositions;
use Infra\Database\Models\IncoCompo;
use Infra\Database\Models\IncoSheet;
describe('IncoComposition OpenAPI Conformance', function (): void {
it('validates GET /api/v1/inco-sheets/{incoId}/composition response against OpenAPI spec', function (): void {
// GIVEN
/** @var \Tests\TestCase $test */
$test = $this;
$sheet = IncoSheet::factory()->create([
'id' => '12345',
'compo_id' => '67890',
]);
IncoCompo::factory()->create([
'id' => '67890',
'id_inco' => '12345',
'identification' => [
'name' => 'Poulet Rôti 1kg Test',
'number' => '12345',
'source' => 'LDC',
],
]);
// WHEN
$response = $test->getJson("/api/v1/inco-sheets/{$sheet->id}/composition");
// THEN
$response->assertStatus(200);
$response->assertValidRequest();
$response->assertValidResponse();
});
});

View file

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Application\Usecase\IncoCompositions;
use Application\Exceptions\NotFoundException;
use Application\Usecase\IncoCompositions\Find\FindIncoComposition;
use Application\Usecase\IncoCompositions\Find\Input;
use Application\Usecase\IncoCompositions\Find\Output;
use Domain\Models\Entities\Inco\Composition\IncoComposition;
use Domain\Models\ValueObjects\Inco\Composition\AminoAcids;
use Domain\Models\ValueObjects\Inco\Composition\CarbohydratesDetail;
use Domain\Models\ValueObjects\Inco\Composition\Energy;
use Domain\Models\ValueObjects\Inco\Composition\EnergyValues;
use Domain\Models\ValueObjects\Inco\Composition\FattyAcids;
use Domain\Models\ValueObjects\Inco\Composition\FreshProduceAndNuts;
use Domain\Models\ValueObjects\Inco\Composition\FruitAndNutAmount;
use Domain\Models\ValueObjects\Inco\Composition\Identification;
use Domain\Models\ValueObjects\Inco\Composition\IncoCompositionId;
use Domain\Models\ValueObjects\Inco\Composition\Minerals;
use Domain\Models\ValueObjects\Inco\Composition\NutrientValue;
use Domain\Models\ValueObjects\Inco\Composition\NutriScore;
use Domain\Models\ValueObjects\Inco\Composition\NutriScorePoints;
use Domain\Models\ValueObjects\Inco\Composition\PercentageValue;
use Domain\Models\ValueObjects\Inco\Composition\PortionMacronutrients;
use Domain\Models\ValueObjects\Inco\Composition\ReferenceIntakes;
use Domain\Models\ValueObjects\Inco\Composition\ReferenceMacronutrients;
use Domain\Models\ValueObjects\Inco\Composition\SpecialCompounds;
use Domain\Models\ValueObjects\Inco\Composition\Vitamins;
use Domain\Models\ValueObjects\Inco\IncoId;
use Domain\Ports\Directories\IncoCompositionDirectory;
use Mockery;
covers(FindIncoComposition::class);
describe('FindIncoComposition Usecase Unit Tests', function (): void {
it('executes successfully and returns Output when composition exists', function (): void {
// GIVEN
$incoId = new IncoId('12345');
$compoId = new IncoCompositionId('67890');
$identification = new Identification('Poulet Rôti 1kg Test', '12345', 'LDC');
$energyValues = new EnergyValues('100g', '0', '0');
$energy = new Energy($energyValues, $energyValues);
$nutrient = new NutrientValue('0');
$refMacro = new ReferenceMacronutrients($nutrient, $nutrient, $nutrient, $nutrient, $nutrient, $nutrient, $nutrient);
$porMacro = new PortionMacronutrients($nutrient, $nutrient, $nutrient, $nutrient, $nutrient, $nutrient, $nutrient);
$fnAmount = new FruitAndNutAmount();
$pct = new PercentageValue('0');
$fresh = new FreshProduceAndNuts($fnAmount, $fnAmount, $pct, $pct);
$refIntakes = new ReferenceIntakes($pct, $pct, $pct, $pct, $pct, $pct, $pct, $pct, '100g');
$nutriScore = new NutriScore('0', '0', 'A');
$nutriScorePoints = new NutriScorePoints('0', '0', '0', '0', '0', '0', '0');
$minerals = new Minerals('0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0');
$vitamins = new Vitamins('0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0');
$carbs = new CarbohydratesDetail('0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0');
$fatty = new FattyAcids('0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0');
$amino = new AminoAcids('0', '0', '0');
$special = new SpecialCompounds('0', '0', '0', '0');
$composition = new IncoComposition(
id: $compoId,
idInco: $incoId,
identification: $identification,
energy: $energy,
referenceMacronutrients: $refMacro,
portionMacronutrients: $porMacro,
freshProduceAndNuts: $fresh,
referenceIntakes: $refIntakes,
nutriScore: $nutriScore,
nutriScorePoints: $nutriScorePoints,
minerals: $minerals,
vitamins: $vitamins,
carbohydratesDetail: $carbs,
fattyAcids: $fatty,
aminoAcids: $amino,
specialCompounds: $special,
);
$directory = Mockery::mock(IncoCompositionDirectory::class);
$directory->shouldReceive('findByIncoId')->with($incoId)->andReturn($composition);
$usecase = new FindIncoComposition($directory);
$input = new Input($incoId);
// WHEN
$output = $usecase->execute($input);
// THEN
expect($output)->toBeInstanceOf(Output::class)
->and($output->id)->toBe($compoId)
->and($output->idInco)->toBe($incoId);
});
it('throws NotFoundException when composition does not exist', function (): void {
// GIVEN
$incoId = new IncoId('99999');
$directory = Mockery::mock(IncoCompositionDirectory::class);
$directory->shouldReceive('findByIncoId')->with($incoId)->andReturn(null);
$usecase = new FindIncoComposition($directory);
$input = new Input($incoId);
// WHEN & THEN
expect(fn () => $usecase->execute($input))->toThrow(NotFoundException::class);
});
});