feat(inco): rename identifiers to incoId/incoCompoId and resolve sellFamilly label

This commit is contained in:
Jérémy Munsch 2026-08-31 17:16:38 +02:00
parent 00f38b7d41
commit 4434eb5757
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
17 changed files with 148 additions and 69 deletions

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Database\ArcadiaReplicate\Factories\Refp;
use Database\ArcadiaReplicate\Factories\AbstractTableFactory;
use Database\ArcadiaReplicate\Tables\Refp\ArticleRefTable;
final class ArticleRefFactory extends AbstractTableFactory
{
public static function table(): string
{
return ArticleRefTable::class;
}
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'NO_ART' => fake()->unique()->numberBetween(10000, 99999),
'COD_SOC' => 1,
'COD_FAMVTE' => 31,
'LIB_CCIAL' => fake()->words(3, true),
'IS_A_SUPPRIMER' => false,
];
}
}

View file

@ -6,6 +6,7 @@ namespace Database\Seeders;
use Database\ArcadiaReplicate\Factories\Metap\MetaUtilFactory;
use Database\ArcadiaReplicate\Factories\Refp\AdditifFactory;
use Database\ArcadiaReplicate\Factories\Refp\ArticleRefFactory;
use Database\ArcadiaReplicate\Factories\Refp\FamVenteFactory;
use Database\ArcadiaReplicate\Factories\Refp\IncoAllegationsFactory;
use Database\ArcadiaReplicate\Factories\Refp\IncoAllergeneFactory;
@ -306,6 +307,8 @@ final class ArcadiaReplicateSeeder extends Seeder
['COD_FAMVTE' => 120, 'LIB_FAMVTE' => 'PLATS CUISINES FRAIS'],
['COD_FAMVTE' => 135, 'LIB_FAMVTE' => 'TRAITEUR ASIATIQUE'],
['COD_FAMVTE' => 150, 'LIB_FAMVTE' => 'CHARCUTERIE DE VOLAILLE'],
['COD_FAMVTE' => 216, 'LIB_FAMVTE' => 'PANES DU VOLAILLER <=400G'],
['COD_FAMVTE' => 99, 'LIB_FAMVTE' => 'ABATS & FOIES'],
];
foreach ($familles as $fam) {
@ -480,5 +483,25 @@ final class ArcadiaReplicateSeeder extends Seeder
$creator();
}
}
// 3. Références articles (liens vers familles de vente)
$articleRefs = [
['NO_ART' => 47789, 'COD_SOC' => 1, 'COD_FAMVTE' => 216, 'LIB_CCIAL' => 'BQ NUGET PLT X10 200G SAT LG'],
['NO_ART' => 47996, 'COD_SOC' => 1, 'COD_FAMVTE' => 99, 'LIB_CCIAL' => 'ETUI GES FOIE VOL DUO 300G SV LG'],
['NO_ART' => 4, 'COD_SOC' => 1, 'COD_FAMVTE' => 306, 'LIB_CCIAL' => 'POULET FERMIER LOUE'],
['NO_ART' => 51201, 'COD_SOC' => 1, 'COD_FAMVTE' => 31, 'LIB_CCIAL' => 'CORDON BLEU DINDE'],
['NO_ART' => 62305, 'COD_SOC' => 1, 'COD_FAMVTE' => 120, 'LIB_CCIAL' => 'LASAGNES BOLOGNAISE'],
['NO_ART' => 73412, 'COD_SOC' => 1, 'COD_FAMVTE' => 135, 'LIB_CCIAL' => 'NEMS POULET'],
['NO_ART' => 84520, 'COD_SOC' => 1, 'COD_FAMVTE' => 31, 'LIB_CCIAL' => 'EMINCES POULET ROTI'],
['NO_ART' => 48102, 'COD_SOC' => 1, 'COD_FAMVTE' => 319, 'LIB_CCIAL' => 'FILET CANARD SUD OUEST'],
['NO_ART' => 52340, 'COD_SOC' => 1, 'COD_FAMVTE' => 150, 'LIB_CCIAL' => 'AIGUILLETTES POULET THYM'],
['NO_ART' => 63450, 'COD_SOC' => 1, 'COD_FAMVTE' => 120, 'LIB_CCIAL' => 'GRATIN DAUPHINOIS'],
];
foreach ($articleRefs as $artRef) {
if (! ArticleRefFactory::query()->where('NO_ART', $artRef['NO_ART'])->where('COD_SOC', $artRef['COD_SOC'])->exists()) {
ArticleRefFactory::new()->create($artRef);
}
}
}
}

View file

@ -949,7 +949,7 @@ components:
type: object
description: Fiche produit INCO complète.
required:
- id
- incoId
- name
- label
- description
@ -962,7 +962,7 @@ components:
- isSeasonal
- additives
properties:
id:
incoId:
type: string
pattern: '^\d{1,}$'
example: "12345"
@ -1021,7 +1021,7 @@ components:
type: array
items:
$ref: "#/components/schemas/IncoAdditive"
compo:
incoCompoId:
type:
- string
- "null"
@ -1173,11 +1173,11 @@ components:
type: object
description: Fiche produit INCO projetée en format court.
required:
- id
- incoId
- name
- label
properties:
id:
incoId:
type: string
pattern: '^\d{1,}$'
example: "12345"
@ -1202,7 +1202,7 @@ components:
type: object
description: Composition nutritionnelle détaillée d'une fiche INCO.
required:
- id
- incoCompoId
- incoId
- identification
- energy
@ -1219,7 +1219,7 @@ components:
- aminoAcids
- specialCompounds
properties:
id:
incoCompoId:
type: string
pattern: '^\d{1,}$'
example: "67890"

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Infra\Adapters\Directories;
use Database\ArcadiaReplicate\Tables\Refp\ArticleRefTable;
use Database\ArcadiaReplicate\Tables\Refp\FamVenteTable;
use Database\ArcadiaReplicate\Tables\Refp\IncoTable;
use Domain\Models\Entities\Inco\IncoSheet;
@ -36,13 +37,7 @@ final class ArcadiaIncoSheetDirectory implements IncoSheetDirectory
IncoSheetSorter $sorter,
?IncoSheetFilter $filter = null
): Paginated {
$incoTable = IncoTable::table($this->connection);
$famVenteTable = FamVenteTable::table($this->connection);
$query = $this->connection
->table($incoTable)
->leftJoin($famVenteTable, 'INCO.FAMILLE_VTE', '=', 'FAM_VENTE.COD_FAMVTE')
->select(['INCO.*', 'FAM_VENTE.LIB_FAMVTE as FAMILLE_VTE_LIBELLE']);
$query = $this->createBaseQuery();
if ($filter !== null) {
if ($filter->since !== null) {
@ -85,13 +80,7 @@ final class ArcadiaIncoSheetDirectory implements IncoSheetDirectory
Pagination $pagination,
BrandCode ...$brands
): Paginated {
$incoTable = IncoTable::table($this->connection);
$famVenteTable = FamVenteTable::table($this->connection);
$dbQuery = $this->connection
->table($incoTable)
->leftJoin($famVenteTable, 'INCO.FAMILLE_VTE', '=', 'FAM_VENTE.COD_FAMVTE')
->select(['INCO.*', 'FAM_VENTE.LIB_FAMVTE as FAMILLE_VTE_LIBELLE']);
$dbQuery = $this->createBaseQuery();
$dbQuery->where('INCO.NO_ART', 'LIKE', $query->value);
@ -120,13 +109,7 @@ final class ArcadiaIncoSheetDirectory implements IncoSheetDirectory
#[\Override]
public function find(IncoId $id): ?IncoSheet
{
$incoTable = IncoTable::table($this->connection);
$famVenteTable = FamVenteTable::table($this->connection);
$row = $this->connection
->table($incoTable)
->leftJoin($famVenteTable, 'INCO.FAMILLE_VTE', '=', 'FAM_VENTE.COD_FAMVTE')
->select(['INCO.*', 'FAM_VENTE.LIB_FAMVTE as FAMILLE_VTE_LIBELLE'])
$row = $this->createBaseQuery()
->where('INCO.NO_ART', $id->value)
->first();
@ -137,6 +120,24 @@ final class ArcadiaIncoSheetDirectory implements IncoSheetDirectory
return $this->mapper->toDomain($row);
}
private function createBaseQuery(): Builder
{
$incoTable = IncoTable::table($this->connection);
$articleRefTable = ArticleRefTable::table($this->connection);
$famVenteTable = FamVenteTable::table($this->connection);
$articleRefSubQuery = $this->connection
->table($articleRefTable)
->select(['NO_ART', $this->connection->raw('MIN(COD_FAMVTE) as COD_FAMVTE')])
->groupBy('NO_ART');
return $this->connection
->table($incoTable)
->leftJoinSub($articleRefSubQuery, 'ARTICLE_REF', 'INCO.NO_ART', '=', 'ARTICLE_REF.NO_ART')
->leftJoin($famVenteTable, 'ARTICLE_REF.COD_FAMVTE', '=', 'FAM_VENTE.COD_FAMVTE')
->select(['INCO.*', 'FAM_VENTE.LIB_FAMVTE as FAMILLE_VTE_LIBELLE']);
}
/**
* @return array<int|string>
*/
@ -169,9 +170,9 @@ final class ArcadiaIncoSheetDirectory implements IncoSheetDirectory
private function toSqlColumn(IncoSheetSortColumn $column): string
{
return match ($column) {
IncoSheetSortColumn::incoSheetId => 'NO_ART',
IncoSheetSortColumn::createdAt => 'DAT_CRE',
IncoSheetSortColumn::updatedAt => 'DAT_MAJ',
IncoSheetSortColumn::incoSheetId => 'INCO.NO_ART',
IncoSheetSortColumn::createdAt => 'INCO.DAT_CRE',
IncoSheetSortColumn::updatedAt => 'INCO.DAT_MAJ',
};
}
}

View file

@ -56,16 +56,17 @@ final readonly class IncoMapper
$rawConserv = $data['CONDIT_CONSERV'] ?? $data['condit_conserv'] ?? $data['storage_condition'] ?? null;
$rawAdditif = $data['LST_ADDITIF'] ?? $data['lst_additif'] ?? $data['additives'] ?? null;
$rawFamille = $data['FAMILLE_VTE_LIBELLE']
?? $data['LIB_FAMVTE']
?? $data['FAMILLE_VTE']
?? $data['famille_vte']
?? $data['COD_FAMVTE']
?? $data['sell_familly']
?? null;
$rawSaison = $data['PROD_SAISON'] ?? $data['prod_saison'] ?? $data['is_seasonal'] ?? false;
$rawCompo = $data['REF_COMPO'] ?? $data['ref_compo'] ?? $data['compo_id'] ?? null;
$rawCreated = $data['DAT_CRE'] ?? $data['dat_cre'] ?? $data['created_at'] ?? null;
$rawUpdated = $data['DAT_MAJ'] ?? $data['dat_maj'] ?? $data['updated_at'] ?? null;
?? $data['famille_vte_libelle']
?? $data['LIB_FAMVTE']
?? $data['lib_famvte']
?? $data['FAMILLE_VTE']
?? $data['famille_vte']
?? $data['sell_familly']
?? null;
$rawSaison = $data['PROD_SAISON'] ?? $data['prod_saison'] ?? $data['is_seasonal'] ?? false;
$rawCompo = $data['REF_COMPO'] ?? $data['ref_compo'] ?? $data['compo_id'] ?? null;
$rawCreated = $data['DAT_CRE'] ?? $data['dat_cre'] ?? $data['created_at'] ?? null;
$rawUpdated = $data['DAT_MAJ'] ?? $data['dat_maj'] ?? $data['updated_at'] ?? null;
$incoId = new IncoId($rawId);
@ -95,8 +96,10 @@ final readonly class IncoMapper
$nutriscore = IncoParser::toNutriscore($rawNutriscore);
$storageCondition = IncoParser::toStorageCondition($rawConserv);
$additives = IncoParser::toAdditives($rawAdditif);
$sellFamilly = is_scalar($rawFamille) && trim((string) $rawFamille) !== '' ? IncoStringParser::toPhrase((string) $rawFamille) : null;
$compo = is_scalar($rawCompo) && trim((string) $rawCompo) !== '' ? new IncoCompositionId((string) $rawCompo) : null;
$sellFamilly = is_string($rawFamille) && trim($rawFamille) !== '' && ! ctype_digit(trim($rawFamille))
? IncoStringParser::toPhrase($rawFamille)
: null;
$compo = is_scalar($rawCompo) && trim((string) $rawCompo) !== '' ? new IncoCompositionId((string) $rawCompo) : null;
$createdAt = self::parseDate($rawCreated);
$updatedAt = self::parseDate($rawUpdated);

View file

@ -25,7 +25,7 @@ final readonly class IncoCompositionDto implements ApiResponseDto
* @param array<string, mixed> $specialCompounds
*/
public function __construct(
public string $id,
public string $incoCompoId,
public string $incoId,
public array $identification,
public array $energy,

View file

@ -21,7 +21,7 @@ final readonly class IncoSheetDto implements ApiResponseDto
* @param array<int, AdditiveDto> $additives
*/
public function __construct(
public string $id,
public string $incoId,
public string $name,
public string $label,
public string $description,
@ -39,7 +39,7 @@ final readonly class IncoSheetDto implements ApiResponseDto
public ?NutriscoreDto $nutriscore,
public ?StorageConditionDto $storageCondition,
public array $additives,
public ?string $compo,
public ?string $incoCompoId,
public ?string $createdAt,
public ?string $updatedAt,
) {

View file

@ -9,7 +9,7 @@ use Presentation\Api\V1\Contracts\ApiResponseDto;
final readonly class IncoShortSheetDto implements ApiResponseDto
{
public function __construct(
public string $id,
public string $incoId,
public string $name,
public string $label,
public ?string $createdAt,

View file

@ -34,7 +34,7 @@ final readonly class IncoCompositionShowResponse implements ApiResponse
private function fromOutput(Output $output): IncoCompositionDto
{
return new IncoCompositionDto(
id: (string) $output->id,
incoCompoId: (string) $output->id,
incoId: (string) $output->idInco,
identification: [
'name' => $output->identification->name,

View file

@ -66,7 +66,7 @@ final readonly class IncoSheetsIndexResponse implements ApiResponse
private function fromPaginatedShortIncoSheet(PaginatedShortIncoSheet $sheet): IncoShortSheetDto
{
return new IncoShortSheetDto(
id: (string) $sheet->id,
incoId: (string) $sheet->id,
name: $sheet->name,
label: $sheet->label,
createdAt: Utc::toNullableRfc3339($sheet->createdAt),
@ -80,7 +80,7 @@ final readonly class IncoSheetsIndexResponse implements ApiResponse
$ingObject = empty($ingGroups) ? (object) [] : (object) $ingGroups;
return new IncoSheetDto(
id: (string) $sheet->id,
incoId: (string) $sheet->id,
name: $sheet->name,
label: $sheet->label,
description: $sheet->description,
@ -104,7 +104,7 @@ final readonly class IncoSheetsIndexResponse implements ApiResponse
static fn (Additive $a): AdditiveDto => AdditiveDto::fromDomain($a),
$sheet->additives
)),
compo: $sheet->compo !== null ? (string) $sheet->compo : null,
incoCompoId: $sheet->compo !== null ? (string) $sheet->compo : null,
createdAt: Utc::toNullableRfc3339($sheet->createdAt),
updatedAt: Utc::toNullableRfc3339($sheet->updatedAt),
);

View file

@ -66,7 +66,7 @@ final readonly class IncoSheetsSearchResponse implements ApiResponse
private function fromPaginatedIncoSheetToShort(PaginatedShortIncoSheet $sheet): IncoShortSheetDto
{
return new IncoShortSheetDto(
id: (string) $sheet->id,
incoId: (string) $sheet->id,
name: $sheet->name,
label: $sheet->label,
createdAt: Utc::toNullableRfc3339($sheet->createdAt),
@ -80,7 +80,7 @@ final readonly class IncoSheetsSearchResponse implements ApiResponse
$ingObject = empty($ingGroups) ? (object) [] : (object) $ingGroups;
return new IncoSheetDto(
id: (string) $sheet->id,
incoId: (string) $sheet->id,
name: $sheet->name,
label: $sheet->label,
description: $sheet->description,
@ -104,7 +104,7 @@ final readonly class IncoSheetsSearchResponse implements ApiResponse
static fn (Additive $a): AdditiveDto => AdditiveDto::fromDomain($a),
$sheet->additives
)),
compo: $sheet->compo !== null ? (string) $sheet->compo : null,
incoCompoId: $sheet->compo !== null ? (string) $sheet->compo : null,
createdAt: Utc::toNullableRfc3339($sheet->createdAt),
updatedAt: Utc::toNullableRfc3339($sheet->updatedAt),
);

View file

@ -48,7 +48,7 @@ final readonly class IncoSheetsShowResponse implements ApiResponse
$ingObject = empty($ingGroups) ? (object) [] : (object) $ingGroups;
return new IncoSheetDto(
id: (string) $output->id,
incoId: (string) $output->id,
name: $output->name,
label: $output->label,
description: $output->description,
@ -72,7 +72,7 @@ final readonly class IncoSheetsShowResponse implements ApiResponse
static fn (Additive $a): AdditiveDto => AdditiveDto::fromDomain($a),
$output->additives
)),
compo: $output->compo !== null ? (string) $output->compo : null,
incoCompoId: $output->compo !== null ? (string) $output->compo : null,
createdAt: Utc::toNullableRfc3339($output->createdAt),
updatedAt: Utc::toNullableRfc3339($output->updatedAt),
);

View file

@ -36,7 +36,7 @@ describe('IncoCompositions Show Feature API', function (): void {
$response->assertStatus(200);
$response->assertJson([
'composition' => [
'id' => '67890',
'incoCompoId' => '67890',
'incoId' => '12345',
'identification' => [
'name' => 'Poulet Rôti 1kg Test',

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Tests\Feature\Api\V1\IncoSheets;
use Database\ArcadiaReplicate\Factories\Refp\IncoFactory;
use Database\Seeders\ArcadiaReplicateSeeder;
describe('IncoSheet Show Feature API', function (): void {
beforeEach(function (): void {
@ -28,9 +29,26 @@ describe('IncoSheet Show Feature API', function (): void {
$response->assertStatus(200);
$response->assertJson([
'incoSheet' => [
'id' => '12345',
'label' => 'Poulet Rôti 1Kg Test',
'incoId' => '12345',
'label' => 'Poulet Rôti 1Kg Test',
],
]);
});
it('returns 200 OK with formatted sellFamilly for seeded inco sheet', function (): void {
// GIVEN
/** @var \Tests\TestCase $test */
$test = $this;
$seeder = new ArcadiaReplicateSeeder();
$seeder->run();
// WHEN
$response = $test->getJson('/api/v1/inco-sheets/47789');
// THEN
$response->assertStatus(200);
$response->assertJsonPath('incoSheet.incoId', '47789');
$response->assertJsonPath('incoSheet.sellFamilly', 'Panes du volailler <=400g');
});
});

View file

@ -99,7 +99,7 @@ describe('IncoSheets Index Feature API', function (): void {
// THEN
$response->assertStatus(200);
$response->assertJsonPath('incoSheets.0.id', '9999001')
$response->assertJsonPath('incoSheets.0.incoId', '9999001')
->assertJsonPath('incoSheets.0.brand.code', $brand->code->value)
->assertJsonPath('incoSheets.0.brand.label', 'Le Gaulois Professionnel');
});
@ -129,7 +129,7 @@ describe('IncoSheets Index Feature API', function (): void {
$response->assertJsonStructure([
'incoSheets' => [
'*' => [
'id',
'incoId',
'name',
'label',
'createdAt',
@ -141,7 +141,7 @@ describe('IncoSheets Index Feature API', function (): void {
$firstItem = $response->json('incoSheets.0');
expect($firstItem)
->toHaveKeys(['id', 'name', 'label', 'createdAt', 'updatedAt'])
->toHaveKeys(['incoId', 'name', 'label', 'createdAt', 'updatedAt'])
->and($firstItem)->not->toHaveKey('description')
->and($firstItem)->not->toHaveKey('allegations')
->and($firstItem)->not->toHaveKey('ingredients')
@ -161,7 +161,7 @@ describe('IncoSheets Index Feature API', function (): void {
$response->assertJsonStructure([
'incoSheets' => [
'*' => [
'id',
'incoId',
'name',
'label',
'createdAt',
@ -173,7 +173,7 @@ describe('IncoSheets Index Feature API', function (): void {
$firstItem = $response->json('incoSheets.0');
expect($firstItem)
->toHaveKeys(['id', 'name', 'label', 'createdAt', 'updatedAt'])
->toHaveKeys(['incoId', 'name', 'label', 'createdAt', 'updatedAt'])
->and($firstItem)->not->toHaveKey('description')
->and($firstItem)->not->toHaveKey('ingredients');
});

View file

@ -72,7 +72,7 @@ describe('IncoSheets Search Feature API', function (): void {
// THEN
$response->assertStatus(200);
$response->assertJsonCount(1, 'incoSheets');
$response->assertJsonPath('incoSheets.0.id', '1000101')
$response->assertJsonPath('incoSheets.0.incoId', '1000101')
->assertJsonPath('incoSheets.0.brand.code', $brand->code->value);
});
@ -103,7 +103,7 @@ describe('IncoSheets Search Feature API', function (): void {
$response->assertJsonStructure([
'incoSheets' => [
'*' => [
'id',
'incoId',
'name',
'label',
'createdAt',
@ -115,7 +115,7 @@ describe('IncoSheets Search Feature API', function (): void {
$firstItem = $response->json('incoSheets.0');
expect($firstItem)
->toHaveKeys(['id', 'name', 'label', 'createdAt', 'updatedAt'])
->toHaveKeys(['incoId', 'name', 'label', 'createdAt', 'updatedAt'])
->and($firstItem)->not->toHaveKey('description')
->and($firstItem)->not->toHaveKey('allegations')
->and($firstItem)->not->toHaveKey('ingredients')
@ -136,7 +136,7 @@ describe('IncoSheets Search Feature API', function (): void {
$response->assertJsonStructure([
'incoSheets' => [
'*' => [
'id',
'incoId',
'name',
'label',
'createdAt',
@ -148,7 +148,7 @@ describe('IncoSheets Search Feature API', function (): void {
$firstItem = $response->json('incoSheets.0');
expect($firstItem)
->toHaveKeys(['id', 'name', 'label', 'createdAt', 'updatedAt'])
->toHaveKeys(['incoId', 'name', 'label', 'createdAt', 'updatedAt'])
->and($firstItem)->not->toHaveKey('description')
->and($firstItem)->not->toHaveKey('ingredients');
});

View file

@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Tests\Integration\Database\Seeders;
use Database\ArcadiaReplicate\Factories\Metap\MetaUtilFactory;
use Database\ArcadiaReplicate\Factories\Refp\ArticleRefFactory;
use Database\ArcadiaReplicate\Factories\Refp\FamVenteFactory;
use Database\ArcadiaReplicate\Factories\Refp\IncoCompoFactory;
use Database\ArcadiaReplicate\Factories\Refp\IncoFactory;
use Database\ArcadiaReplicate\Factories\Refp\MarqueFactory;
@ -27,6 +29,8 @@ describe('ArcadiaReplicateSeeder Integration Test', function (): void {
expect(MetaUtilFactory::count())->toBeGreaterThan(0)
->and(RuPersonneSirhFactory::count())->toBeGreaterThan(0)
->and(MarqueFactory::count())->toBeGreaterThan(0)
->and(FamVenteFactory::count())->toBeGreaterThan(0)
->and(ArticleRefFactory::count())->toBeGreaterThan(0)
->and(IncoFactory::count())->toBeGreaterThan(0)
->and(IncoCompoFactory::count())->toBeGreaterThan(0);
});