diff --git a/documentation/Bruno/Arcadia Api/Api/V1/IncoCompositions/Get Inco Composition.yml b/documentation/Bruno/Arcadia Api/Api/V1/IncoCompositions/Get Inco Composition.yml new file mode 100644 index 0000000..9892ed0 --- /dev/null +++ b/documentation/Bruno/Arcadia Api/Api/V1/IncoCompositions/Get Inco Composition.yml @@ -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 diff --git a/documentation/Bruno/Arcadia Api/Api/V1/IncoCompositions/folder.yml b/documentation/Bruno/Arcadia Api/Api/V1/IncoCompositions/folder.yml new file mode 100644 index 0000000..2b5ce95 --- /dev/null +++ b/documentation/Bruno/Arcadia Api/Api/V1/IncoCompositions/folder.yml @@ -0,0 +1,6 @@ +info: + name: IncoCompositions + seq: 3 + +folder: + auth: inherit diff --git a/documentation/Bruno/Arcadia Api/Api/V1/IncoSheets/Get Inco Sheet.yml b/documentation/Bruno/Arcadia Api/Api/V1/IncoSheets/Get Inco Sheet.yml index 212cdea..3431bfc 100644 --- a/documentation/Bruno/Arcadia Api/Api/V1/IncoSheets/Get Inco Sheet.yml +++ b/documentation/Bruno/Arcadia Api/Api/V1/IncoSheets/Get Inco Sheet.yml @@ -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 diff --git a/documentation/OpenApi/openapi.yaml b/documentation/OpenApi/openapi.yaml index ea32b7d..275fc65 100644 --- a/documentation/OpenApi/openapi.yaml +++ b/documentation/OpenApi/openapi.yaml @@ -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: diff --git a/documentation/OpenApi/original-endpoint.yaml b/documentation/OpenApi/original-endpoint.yaml deleted file mode 100644 index 17fa001..0000000 --- a/documentation/OpenApi/original-endpoint.yaml +++ /dev/null @@ -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 diff --git a/routes/apiv1.php b/routes/apiv1.php index 6299bec..46e69f6 100644 --- a/routes/apiv1.php +++ b/routes/apiv1.php @@ -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'); }); diff --git a/src/Application/Usecase/IncoCompositions/Find/FindIncoComposition.php b/src/Application/Usecase/IncoCompositions/Find/FindIncoComposition.php new file mode 100644 index 0000000..12b640a --- /dev/null +++ b/src/Application/Usecase/IncoCompositions/Find/FindIncoComposition.php @@ -0,0 +1,51 @@ + */ +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 + ); + } +} diff --git a/src/Application/Usecase/IncoCompositions/Find/Input.php b/src/Application/Usecase/IncoCompositions/Find/Input.php new file mode 100644 index 0000000..3f21545 --- /dev/null +++ b/src/Application/Usecase/IncoCompositions/Find/Input.php @@ -0,0 +1,16 @@ + */ +interface IncoCompositionDirectory extends Directory +{ + public function find(IncoCompositionId $id): ?IncoComposition; + + public function findByIncoId(IncoId $incoId): ?IncoComposition; +} diff --git a/src/Infra/Adapters/Directories/EloquentIncoCompositionDirectory.php b/src/Infra/Adapters/Directories/EloquentIncoCompositionDirectory.php new file mode 100644 index 0000000..a1210e7 --- /dev/null +++ b/src/Infra/Adapters/Directories/EloquentIncoCompositionDirectory.php @@ -0,0 +1,392 @@ +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 $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 $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 $data + */ + private function getNutrient(array $data, string $key, string $default = '0'): NutrientValue + { + return new NutrientValue($this->getString($data, $key, $default)); + } + + /** + * @param array $data + */ + private function getPercentage(array $data, string $key, string $default = '0'): PercentageValue + { + return new PercentageValue($this->getString($data, $key, $default)); + } +} diff --git a/src/Infra/Adapters/Directories/EloquentIncoSheetDirectory.php b/src/Infra/Adapters/Directories/EloquentIncoSheetDirectory.php index f72acd2..9e717f9 100644 --- a/src/Infra/Adapters/Directories/EloquentIncoSheetDirectory.php +++ b/src/Infra/Adapters/Directories/EloquentIncoSheetDirectory.php @@ -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 $allegationsList */ - $allegationsList = is_array($allegationsRaw['allegations'] ?? null) ? $allegationsRaw['allegations'] : []; - /** @var array $healthList */ - $healthList = is_array($allegationsRaw['health'] ?? null) ? $allegationsRaw['health'] : []; - /** @var array $nutritionList */ - $nutritionList = is_array($allegationsRaw['nutrition'] ?? null) ? $allegationsRaw['nutrition'] : []; - /** @var array $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> $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 $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 $data + * @return array + */ + 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 $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 $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) { diff --git a/src/Infra/Providers/DomainServiceProvider.php b/src/Infra/Providers/DomainServiceProvider.php index 4828ad3..3539920 100644 --- a/src/Infra/Providers/DomainServiceProvider.php +++ b/src/Infra/Providers/DomainServiceProvider.php @@ -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); } } diff --git a/src/Presentation/Api/V1/Dto/IncoCompositions/IncoCompositionDto.php b/src/Presentation/Api/V1/Dto/IncoCompositions/IncoCompositionDto.php new file mode 100644 index 0000000..242f350 --- /dev/null +++ b/src/Presentation/Api/V1/Dto/IncoCompositions/IncoCompositionDto.php @@ -0,0 +1,54 @@ + $identification + * @param array $energy + * @param array $referenceMacronutrients + * @param array $portionMacronutrients + * @param array $freshProduceAndNuts + * @param array $referenceIntakes + * @param array $nutriScore + * @param array $nutriScorePoints + * @param array $minerals + * @param array $vitamins + * @param array $carbohydratesDetail + * @param array $fattyAcids + * @param array $aminoAcids + * @param array $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 */ + #[\Override] + public function toArray(): array + { + /** @var array */ + return get_object_vars($this); + } +} diff --git a/src/Presentation/Api/V1/Http/Controllers/Employees/EmployeeShowController.php b/src/Presentation/Api/V1/Http/Controllers/Employees/EmployeeShowController.php index 1a3066e..03074bb 100644 --- a/src/Presentation/Api/V1/Http/Controllers/Employees/EmployeeShowController.php +++ b/src/Presentation/Api/V1/Http/Controllers/Employees/EmployeeShowController.php @@ -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(), diff --git a/src/Presentation/Api/V1/Http/Controllers/IncoCompositions/IncoCompositionShowController.php b/src/Presentation/Api/V1/Http/Controllers/IncoCompositions/IncoCompositionShowController.php new file mode 100644 index 0000000..2397812 --- /dev/null +++ b/src/Presentation/Api/V1/Http/Controllers/IncoCompositions/IncoCompositionShowController.php @@ -0,0 +1,45 @@ + $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(), + ); + } +} diff --git a/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsIndexController.php b/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsIndexController.php index 25146a6..b309a55 100644 --- a/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsIndexController.php +++ b/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsIndexController.php @@ -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(), + ); + } } diff --git a/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsSearchController.php b/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsSearchController.php index 0eec6aa..8de2bfd 100644 --- a/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsSearchController.php +++ b/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsSearchController.php @@ -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() + ); + } } diff --git a/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsShowController.php b/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsShowController.php index ca9c3f7..3ab866d 100644 --- a/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsShowController.php +++ b/src/Presentation/Api/V1/Http/Controllers/IncoSheets/IncoSheetsShowController.php @@ -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(), + ); + } } diff --git a/src/Presentation/Api/V1/Http/Requests/IncoCompositions/IncoCompositionShowRequest.php b/src/Presentation/Api/V1/Http/Requests/IncoCompositions/IncoCompositionShowRequest.php new file mode 100644 index 0000000..bd5e5ac --- /dev/null +++ b/src/Presentation/Api/V1/Http/Requests/IncoCompositions/IncoCompositionShowRequest.php @@ -0,0 +1,36 @@ +> */ + 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()); + } +} diff --git a/src/Presentation/Api/V1/Http/Responses/IncoCompositions/IncoCompositionShowResponse.php b/src/Presentation/Api/V1/Http/Responses/IncoCompositions/IncoCompositionShowResponse.php new file mode 100644 index 0000000..96454ac --- /dev/null +++ b/src/Presentation/Api/V1/Http/Responses/IncoCompositions/IncoCompositionShowResponse.php @@ -0,0 +1,287 @@ +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, + ], + ); + } +} diff --git a/src/Presentation/Api/V1/Presenters/IncoCompositions/IncoCompositionShowPresenter.php b/src/Presentation/Api/V1/Presenters/IncoCompositions/IncoCompositionShowPresenter.php new file mode 100644 index 0000000..14fded3 --- /dev/null +++ b/src/Presentation/Api/V1/Presenters/IncoCompositions/IncoCompositionShowPresenter.php @@ -0,0 +1,22 @@ + */ +final readonly class IncoCompositionShowPresenter implements Presenter +{ + #[\Override] + public function present(UsecaseOutput $output): object + { + Assert::isInstanceOf($output, Output::class); + + return IncoCompositionShowView::fromOutput($output); + } +} diff --git a/src/Presentation/Api/V1/Presenters/IncoCompositions/IncoCompositionShowView.php b/src/Presentation/Api/V1/Presenters/IncoCompositions/IncoCompositionShowView.php new file mode 100644 index 0000000..338233f --- /dev/null +++ b/src/Presentation/Api/V1/Presenters/IncoCompositions/IncoCompositionShowView.php @@ -0,0 +1,20 @@ +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); + }); +}); diff --git a/tests/Feature/OpenApi/V1/IncoCompositions/IncoCompositionOpenApiTest.php b/tests/Feature/OpenApi/V1/IncoCompositions/IncoCompositionOpenApiTest.php new file mode 100644 index 0000000..21c60ef --- /dev/null +++ b/tests/Feature/OpenApi/V1/IncoCompositions/IncoCompositionOpenApiTest.php @@ -0,0 +1,39 @@ +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(); + }); +}); diff --git a/tests/Unit/Application/Usecase/IncoCompositions/FindIncoCompositionTest.php b/tests/Unit/Application/Usecase/IncoCompositions/FindIncoCompositionTest.php new file mode 100644 index 0000000..1f0c2f2 --- /dev/null +++ b/tests/Unit/Application/Usecase/IncoCompositions/FindIncoCompositionTest.php @@ -0,0 +1,113 @@ +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); + }); +});