ldc-boutique-cnc-sable/app/Services/ArcadiaIncoService.php

203 lines
8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Dto\ArcadiaIncoDataDto;
use App\Dto\ArcadiaIncoPreviewDto;
use Domain\Enums\Nutriscore;
use Domain\Models\ValueObjects\NutritionalValues;
use Psr\Log\LoggerInterface;
use Uwil\ArcadiaApiPhpClient\Error\ApiError;
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco as LibInco;
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo as LibIncoCompo;
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoId;
use Uwil\ArcadiaApiPhpClient\Service\IncoService as LibIncoService;
class ArcadiaIncoService
{
public function __construct(
private readonly LibIncoService $incoService,
private readonly LoggerInterface $logger,
) {
}
public function fetchInco(int $noArt): ?ArcadiaIncoDataDto
{
$action = "INCO sheet #{$noArt}";
$this->logger->debug("Fetching {$action} from Arcadia API");
try {
$incoId = new IncoId((string) $noArt);
} catch (\DomainException $e) {
$this->logger->warning("Invalid IncoId format for #{$noArt}: {$e->getMessage()}");
return null;
}
$inco = $this->incoService->findBy($incoId);
if ($inco instanceof ApiError) {
$this->logger->error("Failed to fetch {$action}: {$inco->message()}");
throw new \RuntimeException($inco->message());
}
if (! ($inco instanceof LibInco)) {
$this->logger->info("{$action} not found on Arcadia API (404)");
return null;
}
$compo = $this->incoService->findCompo($incoId);
if ($compo instanceof ApiError) {
$this->logger->warning("Failed to fetch composition for {$action}: {$compo->message()}");
$compo = null;
}
return $this->mapToDataDto($inco, $compo instanceof LibIncoCompo ? $compo : null);
}
public function getPreview(int $noArt): ?ArcadiaIncoPreviewDto
{
$data = $this->fetchInco($noArt);
if ($data === null) {
return null;
}
return new ArcadiaIncoPreviewDto(
noArt: $data->noArt,
name: $data->name,
libelle: $data->libelle,
brand: $data->brand,
nutriscore: $data->nutriscore?->value,
servings: $data->servings,
allergens: $data->allergens,
ingredients: $data->ingredients,
preparation: $data->preparation,
energyKcal: $data->nutrition->kcal,
energyKj: $data->nutrition->kj,
fat: $data->nutrition->fatG,
satFat: $data->nutrition->satFatG,
carbs: $data->nutrition->carbG,
sugar: $data->nutrition->sugarG,
fiber: $data->nutrition->fiberG,
protein: $data->nutrition->proteinG,
salt: $data->nutrition->saltG,
);
}
private function mapToDataDto(LibInco $inco, ?LibIncoCompo $compo): ArcadiaIncoDataDto
{
$ingredientsHtml = $this->buildIngredientsHtml($inco);
$preparationHtml = ! empty($inco->preparationAdvices)
? '<p>' . nl2br(e(trim($inco->preparationAdvices))) . '</p>'
: null;
$sellingArgumentHtml = ! empty($inco->sellArguments)
? '<p>' . nl2br(e(trim($inco->sellArguments))) . '</p>'
: null;
$servings = $inco->portions !== null ? (string) $inco->portions : null;
if ($servings !== null && mb_strlen($servings) > 20) {
$servings = mb_substr($servings, 0, 20);
}
$allergens = ! empty($inco->allergens)
? implode(', ', array_map(fn ($a) => $a->label, $inco->allergens))
: null;
$nutriscore = $this->resolveNutriscore($inco, $compo);
$nutrition = $compo !== null ? $this->mapNutrition($compo) : new NutritionalValues();
return new ArcadiaIncoDataDto(
noArt: (int) $inco->id->value,
name: $inco->name,
libelle: $inco->libelle,
brand: $inco->brand->label !== '' ? $inco->brand->label : $inco->brand->code->value,
ingredients: $ingredientsHtml,
preparation: $preparationHtml,
servings: $servings,
allergens: $allergens,
sellingArgument: $sellingArgumentHtml,
nutriscore: $nutriscore,
nutriscoreArgs: null,
nutrition: $nutrition,
);
}
private function buildIngredientsHtml(LibInco $inco): ?string
{
if ($inco->ingredients->isEmpty()) {
return null;
}
$paragraphs = [];
foreach ($inco->ingredients->groups as $title => $lines) {
$text = implode(', ', $lines);
if (is_string($title) && ! is_numeric($title) && $title !== '') {
$paragraphs[] = '<p><strong>' . e($title) . ' :</strong> ' . e($text) . '</p>';
} else {
$paragraphs[] = '<p>' . e($text) . '</p>';
}
}
return implode('', $paragraphs);
}
private function resolveNutriscore(LibInco $inco, ?LibIncoCompo $compo): ?Nutriscore
{
$nutriscoreStr = $inco->nutriscore?->grade;
if (empty($nutriscoreStr) && $compo !== null && ! empty($compo->nutriScore->classement)) {
$nutriscoreStr = $compo->nutriScore->classement;
}
return ! empty($nutriscoreStr) ? Nutriscore::tryFrom(mb_strtoupper(trim($nutriscoreStr))) : null;
}
private function mapNutrition(LibIncoCompo $compo): NutritionalValues
{
$parseFloat = static function (?string $val): ?float {
if ($val === null || trim($val) === '' || ! is_numeric(trim($val))) {
return null;
}
$floatVal = (float) trim($val);
return $floatVal >= 0 ? $floatVal : null;
};
return new NutritionalValues(
refWeightG: $parseFloat($compo->energie->tailleReference),
kj: $parseFloat($compo->energie->valEnergKjReference),
kcal: $parseFloat($compo->energie->valEnergKcalReference),
fatG: $parseFloat($compo->macronutrimentsRef->matGrassReference),
satFatG: $parseFloat($compo->macronutrimentsRef->acideGrasSatureReference),
carbG: $parseFloat($compo->macronutrimentsRef->glucidesReference),
sugarG: $parseFloat($compo->macronutrimentsRef->sucreReference),
fiberG: $parseFloat($compo->macronutrimentsRef->fibAlimReference),
proteinG: $parseFloat($compo->macronutrimentsRef->proteineReference),
saltG: $parseFloat($compo->macronutrimentsRef->selReference),
servingSizeG: $parseFloat($compo->energie->taillePortion),
servingKj: $parseFloat($compo->energie->valEnergKjPortion),
servingKcal: $parseFloat($compo->energie->valEnergKcalPortion),
servingFatG: $parseFloat($compo->macronutrimentsPortion->matGrassPortion),
servingSatFatG: $parseFloat($compo->macronutrimentsPortion->acideGrasSaturePortion),
servingCarbG: $parseFloat($compo->macronutrimentsPortion->glucidesPortion),
servingSugarG: $parseFloat($compo->macronutrimentsPortion->sucrePortion),
servingFiberG: $parseFloat($compo->macronutrimentsPortion->fibAlimPortion),
servingProteinG: $parseFloat($compo->macronutrimentsPortion->proteinePortion),
servingSaltG: $parseFloat($compo->macronutrimentsPortion->selPortion),
dailyEnergy: $parseFloat($compo->apportsReference->pourcentApportRefKjoP),
dailyFat: $parseFloat($compo->apportsReference->pourcentApportRefFatP),
dailySatFat: $parseFloat($compo->apportsReference->pourcentApportRefFatsatP),
dailyCarb: $parseFloat($compo->apportsReference->pourcentApportRefChovalP),
dailySugar: $parseFloat($compo->apportsReference->pourcentApportRefSugarP),
dailyFiber: null,
dailyProtein: $parseFloat($compo->apportsReference->pourcentApportRefProP),
dailySalt: $parseFloat($compo->apportsReference->pourcentApportRefSalteqP),
);
}
}