# Supplier References Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Allow storing a supplier reference (`supplierReference`) per (item, constructeur) pair by converting ManyToMany join tables to proper Link entities.

**Architecture:** Replace the 4 simple join tables (`_MachineConstructeurs`, `_PieceConstructeurs`, `_ComposantConstructeurs`, `_ProductConstructeurs`) with Doctrine entities following the existing `*Link` pattern. Each entity holds `supplierReference` (string, nullable). Update all consumers: audit subscribers, version service, MCP tools, structure controller, category conversion service.

**Tech Stack:** Symfony 8 / Doctrine ORM / API Platform / PHP 8.4 / PostgreSQL

---

## File Structure

### New files
- `src/Entity/MachineConstructeurLink.php` — pivot entity machine↔constructeur
- `src/Entity/PieceConstructeurLink.php` — pivot entity piece↔constructeur
- `src/Entity/ComposantConstructeurLink.php` — pivot entity composant↔constructeur
- `src/Entity/ProductConstructeurLink.php` — pivot entity product↔constructeur
- `src/Repository/MachineConstructeurLinkRepository.php`
- `src/Repository/PieceConstructeurLinkRepository.php`
- `src/Repository/ComposantConstructeurLinkRepository.php`
- `src/Repository/ProductConstructeurLinkRepository.php`
- `migrations/VersionXXX.php` — migration (auto-generated, then adjusted)

### Modified files
- `src/Entity/Machine.php` — replace ManyToMany with OneToMany to Link
- `src/Entity/Piece.php` — replace ManyToMany with OneToMany to Link
- `src/Entity/Composant.php` — replace ManyToMany with OneToMany to Link
- `src/Entity/Product.php` — replace ManyToMany with OneToMany to Link
- `src/Entity/Constructeur.php` — replace 4 ManyToMany with 4 OneToMany to Links
- `src/Controller/MachineStructureController.php` — update clone + normalize
- `src/Service/EntityVersionService.php` — update snapshot/restore/diff for constructeur links
- `src/Service/ModelTypeCategoryConversionService.php` — update table names in raw SQL
- `src/EventSubscriber/AbstractAuditSubscriber.php` — remove ManyToMany collection tracking for constructeurs
- `src/EventSubscriber/MachineAuditSubscriber.php` — update snapshotEntity
- `src/EventSubscriber/PieceAuditSubscriber.php` — update snapshotEntity
- `src/EventSubscriber/ComposantAuditSubscriber.php` — update snapshotEntity
- `src/EventSubscriber/ProductAuditSubscriber.php` — update snapshotEntity
- `src/Mcp/Tool/Machine/CreateMachineTool.php` — use Links instead of addConstructeur
- `src/Mcp/Tool/Machine/UpdateMachineTool.php` — use Links
- `src/Mcp/Tool/Machine/GetMachineTool.php` — read from Links
- `src/Mcp/Tool/Piece/CreatePieceTool.php` — use Links
- `src/Mcp/Tool/Piece/UpdatePieceTool.php` — use Links
- `src/Mcp/Tool/Piece/GetPieceTool.php` — read from Links
- `src/Mcp/Tool/Composant/CreateComposantTool.php` — use Links
- `src/Mcp/Tool/Composant/UpdateComposantTool.php` — use Links
- `src/Mcp/Tool/Composant/GetComposantTool.php` — read from Links
- `src/Mcp/Tool/Product/CreateProductTool.php` — use Links
- `src/Mcp/Tool/Product/UpdateProductTool.php` — use Links
- `src/Mcp/Tool/Product/GetProductTool.php` — read from Links
- `tests/AbstractApiTestCase.php` — add `createConstructeurLink()` factory helpers

---

### Task 1: Create the 4 Link entities + repositories

**Files:**
- Create: `src/Entity/MachineConstructeurLink.php`
- Create: `src/Entity/PieceConstructeurLink.php`
- Create: `src/Entity/ComposantConstructeurLink.php`
- Create: `src/Entity/ProductConstructeurLink.php`
- Create: `src/Repository/MachineConstructeurLinkRepository.php`
- Create: `src/Repository/PieceConstructeurLinkRepository.php`
- Create: `src/Repository/ComposantConstructeurLinkRepository.php`
- Create: `src/Repository/ProductConstructeurLinkRepository.php`

- [ ] **Step 1: Create `MachineConstructeurLink` entity**

```php
<?php

declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use App\Entity\Trait\CuidEntityTrait;
use App\Repository\MachineConstructeurLinkRepository;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: MachineConstructeurLinkRepository::class)]
#[ORM\Table(name: 'machine_constructeur_links')]
#[ORM\UniqueConstraint(name: 'uniq_machine_constructeur', columns: ['machineid', 'constructeurid'])]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
    description: 'Liaisons machine–fournisseur. Chaque liaison peut porter une référence fournisseur spécifique.',
    operations: [
        new Get(security: "is_granted('ROLE_VIEWER')"),
        new GetCollection(security: "is_granted('ROLE_VIEWER')"),
        new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
        new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
        new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
        new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
    ]
)]
class MachineConstructeurLink
{
    use CuidEntityTrait;

    #[ORM\Id]
    #[ORM\Column(type: Types::STRING, length: 36)]
    private ?string $id = null;

    #[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'constructeurLinks')]
    #[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
    private Machine $machine;

    #[ORM\ManyToOne(targetEntity: Constructeur::class, inversedBy: 'machineLinks')]
    #[ORM\JoinColumn(name: 'constructeurId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
    private Constructeur $constructeur;

    #[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'supplierReference')]
    private ?string $supplierReference = null;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
    private DateTimeImmutable $createdAt;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
    private DateTimeImmutable $updatedAt;

    public function __construct()
    {
        $this->createdAt = new DateTimeImmutable();
        $this->updatedAt = new DateTimeImmutable();
    }

    public function getMachine(): Machine
    {
        return $this->machine;
    }

    public function setMachine(Machine $machine): static
    {
        $this->machine = $machine;

        return $this;
    }

    public function getConstructeur(): Constructeur
    {
        return $this->constructeur;
    }

    public function setConstructeur(Constructeur $constructeur): static
    {
        $this->constructeur = $constructeur;

        return $this;
    }

    public function getSupplierReference(): ?string
    {
        return $this->supplierReference;
    }

    public function setSupplierReference(?string $supplierReference): static
    {
        $this->supplierReference = $supplierReference;

        return $this;
    }
}
```

- [ ] **Step 2: Create `PieceConstructeurLink` entity**

Same structure as `MachineConstructeurLink`, replacing:
- Class name: `PieceConstructeurLink`
- Repository: `PieceConstructeurLinkRepository`
- Table: `piece_constructeur_links`
- Unique constraint: `uniq_piece_constructeur` on `['pieceid', 'constructeurid']`
- ManyToOne: `Piece` (inversedBy: `constructeurLinks`, joinColumn: `pieceId`)
- Constructeur inversedBy: `pieceLinks`
- Description: `'Liaisons pièce–fournisseur. Chaque liaison peut porter une référence fournisseur spécifique.'`
- Property + getter/setter: `piece` / `getPiece()` / `setPiece(Piece $piece)`

- [ ] **Step 3: Create `ComposantConstructeurLink` entity**

Same structure, replacing:
- Class name: `ComposantConstructeurLink`
- Repository: `ComposantConstructeurLinkRepository`
- Table: `composant_constructeur_links`
- Unique constraint: `uniq_composant_constructeur` on `['composantid', 'constructeurid']`
- ManyToOne: `Composant` (inversedBy: `constructeurLinks`, joinColumn: `composantId`)
- Constructeur inversedBy: `composantLinks`
- Description: `'Liaisons composant–fournisseur. Chaque liaison peut porter une référence fournisseur spécifique.'`
- Property + getter/setter: `composant` / `getComposant()` / `setComposant(Composant $composant)`

- [ ] **Step 4: Create `ProductConstructeurLink` entity**

Same structure, replacing:
- Class name: `ProductConstructeurLink`
- Repository: `ProductConstructeurLinkRepository`
- Table: `product_constructeur_links`
- Unique constraint: `uniq_product_constructeur` on `['productid', 'constructeurid']`
- ManyToOne: `Product` (inversedBy: `constructeurLinks`, joinColumn: `productId`)
- Constructeur inversedBy: `productLinks`
- Description: `'Liaisons produit–fournisseur. Chaque liaison peut porter une référence fournisseur spécifique.'`
- Property + getter/setter: `product` / `getProduct()` / `setProduct(Product $product)`

- [ ] **Step 5: Create the 4 repositories**

Each repository follows the `MachinePieceLinkRepository` pattern:

```php
<?php

declare(strict_types=1);

namespace App\Repository;

use App\Entity\MachineConstructeurLink;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @extends ServiceEntityRepository<MachineConstructeurLink>
 */
class MachineConstructeurLinkRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, MachineConstructeurLink::class);
    }
}
```

Repeat for `PieceConstructeurLinkRepository`, `ComposantConstructeurLinkRepository`, `ProductConstructeurLinkRepository` (changing entity class).

- [ ] **Step 6: Run php-cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 7: Commit**

```bash
git add src/Entity/*ConstructeurLink.php src/Repository/*ConstructeurLinkRepository.php
git commit -m "feat(constructeur) : add 4 ConstructeurLink pivot entities with supplierReference"
```

---

### Task 2: Update existing entities (Machine, Piece, Composant, Product, Constructeur)

**Files:**
- Modify: `src/Entity/Machine.php`
- Modify: `src/Entity/Piece.php`
- Modify: `src/Entity/Composant.php`
- Modify: `src/Entity/Product.php`
- Modify: `src/Entity/Constructeur.php`

- [ ] **Step 1: Update `Machine.php`**

Replace the ManyToMany property and methods with OneToMany:

Remove:
```php
/**
 * @var Collection<int, Constructeur>
 */
#[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'machines')]
#[ORM\JoinTable(
    name: '_MachineConstructeurs',
    joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
    inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
)]
private Collection $constructeurs;
```

Add:
```php
/**
 * @var Collection<int, MachineConstructeurLink>
 */
#[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachineConstructeurLink::class, cascade: ['remove'])]
private Collection $constructeurLinks;
```

In `__construct()`, replace `$this->constructeurs = new ArrayCollection();` with `$this->constructeurLinks = new ArrayCollection();`

Replace methods `getConstructeurs()`, `addConstructeur()`, `removeConstructeur()` with:
```php
/**
 * @return Collection<int, MachineConstructeurLink>
 */
public function getConstructeurLinks(): Collection
{
    return $this->constructeurLinks;
}
```

- [ ] **Step 2: Update `Piece.php`**

Same pattern as Machine. Replace ManyToMany `constructeurs` property (with `#[Groups(['piece:read'])]`) with:
```php
/**
 * @var Collection<int, PieceConstructeurLink>
 */
#[ORM\OneToMany(mappedBy: 'piece', targetEntity: PieceConstructeurLink::class, cascade: ['remove'])]
#[Groups(['piece:read'])]
private Collection $constructeurLinks;
```

In `__construct()`: `$this->constructeurLinks = new ArrayCollection();`

Replace getter/add/remove with `getConstructeurLinks(): Collection`.

- [ ] **Step 3: Update `Composant.php`**

Same pattern. Replace ManyToMany (with `#[Groups(['composant:read'])]`) with:
```php
/**
 * @var Collection<int, ComposantConstructeurLink>
 */
#[ORM\OneToMany(mappedBy: 'composant', targetEntity: ComposantConstructeurLink::class, cascade: ['remove'])]
#[Groups(['composant:read'])]
private Collection $constructeurLinks;
```

- [ ] **Step 4: Update `Product.php`**

Same pattern. Replace ManyToMany (with `#[Groups(['product:read'])]`) with:
```php
/**
 * @var Collection<int, ProductConstructeurLink>
 */
#[ORM\OneToMany(mappedBy: 'product', targetEntity: ProductConstructeurLink::class, cascade: ['remove'])]
#[Groups(['product:read'])]
private Collection $constructeurLinks;
```

- [ ] **Step 5: Update `Constructeur.php`**

Replace the 4 ManyToMany properties and their initializations in `__construct()`:

Remove all 4 ManyToMany properties (`machines`, `composants`, `pieces`, `products`) and replace with:
```php
/**
 * @var Collection<int, MachineConstructeurLink>
 */
#[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: MachineConstructeurLink::class, cascade: ['remove'])]
private Collection $machineLinks;

/**
 * @var Collection<int, ComposantConstructeurLink>
 */
#[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: ComposantConstructeurLink::class, cascade: ['remove'])]
private Collection $composantLinks;

/**
 * @var Collection<int, PieceConstructeurLink>
 */
#[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: PieceConstructeurLink::class, cascade: ['remove'])]
private Collection $pieceLinks;

/**
 * @var Collection<int, ProductConstructeurLink>
 */
#[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: ProductConstructeurLink::class, cascade: ['remove'])]
private Collection $productLinks;
```

In `__construct()`, replace the 4 `ArrayCollection` inits with:
```php
$this->machineLinks   = new ArrayCollection();
$this->composantLinks = new ArrayCollection();
$this->pieceLinks     = new ArrayCollection();
$this->productLinks   = new ArrayCollection();
```

- [ ] **Step 6: Run php-cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 7: Commit**

```bash
git add src/Entity/Machine.php src/Entity/Piece.php src/Entity/Composant.php src/Entity/Product.php src/Entity/Constructeur.php
git commit -m "refactor(constructeur) : replace ManyToMany with OneToMany to ConstructeurLink entities"
```

---

### Task 3: Generate and adjust migration

**Files:**
- Create: `migrations/VersionXXX.php` (auto-generated)

- [ ] **Step 1: Generate migration diff**

Run inside docker:
```bash
docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:diff
```

- [ ] **Step 2: Edit the generated migration**

The auto-generated migration will try to drop the old tables and create new ones. We need to add data migration in between. Edit the migration `up()` method to follow this order:

1. Create the 4 new tables (keep the auto-generated CREATE TABLE statements)
2. **Add data migration** — insert from old join tables into new Link tables:

```sql
-- Machine constructeur links
INSERT INTO machine_constructeur_links (id, machineid, constructeurid, supplierreference, createdat, updatedat)
SELECT 'cl' || encode(gen_random_bytes(12), 'hex'), a, b, NULL, NOW(), NOW()
FROM "_machineconstructeurs";

-- Piece constructeur links
INSERT INTO piece_constructeur_links (id, pieceid, constructeurid, supplierreference, createdat, updatedat)
SELECT 'cl' || encode(gen_random_bytes(12), 'hex'), a, b, NULL, NOW(), NOW()
FROM "_piececonstructeurs";

-- Composant constructeur links
INSERT INTO composant_constructeur_links (id, composantid, constructeurid, supplierreference, createdat, updatedat)
SELECT 'cl' || encode(gen_random_bytes(12), 'hex'), a, b, NULL, NOW(), NOW()
FROM "_composantconstructeurs";

-- Product constructeur links
INSERT INTO product_constructeur_links (id, productid, constructeurid, supplierreference, createdat, updatedat)
SELECT 'cl' || encode(gen_random_bytes(12), 'hex'), a, b, NULL, NOW(), NOW()
FROM "_productconstructeurs";
```

3. Drop the 4 old join tables (keep the auto-generated DROP TABLE statements)

For `down()`: reverse — recreate old tables, migrate data back (without supplierReference), drop new tables.

- [ ] **Step 3: Run the migration**

```bash
docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate
```
Expected: migration passes without errors.

- [ ] **Step 4: Verify data**

```bash
docker exec -u www-data php-inventory-apache php bin/console doctrine:query:sql "SELECT COUNT(*) FROM machine_constructeur_links"
docker exec -u www-data php-inventory-apache php bin/console doctrine:query:sql "SELECT COUNT(*) FROM piece_constructeur_links"
docker exec -u www-data php-inventory-apache php bin/console doctrine:query:sql "SELECT COUNT(*) FROM composant_constructeur_links"
docker exec -u www-data php-inventory-apache php bin/console doctrine:query:sql "SELECT COUNT(*) FROM product_constructeur_links"
```

Expected: counts match the old join tables.

- [ ] **Step 5: Commit**

```bash
git add migrations/
git commit -m "feat(constructeur) : add migration for ConstructeurLink tables with data migration"
```

---

### Task 4: Update MachineStructureController

**Files:**
- Modify: `src/Controller/MachineStructureController.php:141-144` (clone)
- Modify: `src/Controller/MachineStructureController.php:589` (normalize in structure response)
- Modify: `src/Controller/MachineStructureController.php:814-824` (normalizeConstructeurs)

- [ ] **Step 1: Update clone logic (lines ~141-144)**

Replace:
```php
// Copy constructeurs
foreach ($source->getConstructeurs() as $constructeur) {
    $newMachine->getConstructeurs()->add($constructeur);
}
```

With:
```php
// Copy constructeur links
foreach ($source->getConstructeurLinks() as $link) {
    $newLink = new MachineConstructeurLink();
    $newLink->setMachine($newMachine);
    $newLink->setConstructeur($link->getConstructeur());
    $newLink->setSupplierReference($link->getSupplierReference());
    $this->entityManager->persist($newLink);
}
```

Add `use App\Entity\MachineConstructeurLink;` at top of file.

- [ ] **Step 2: Update structure response (line ~589)**

Replace:
```php
'constructeurs'     => $this->normalizeConstructeurs($machine->getConstructeurs()),
```

With:
```php
'constructeurs'     => $this->normalizeConstructeurLinks($machine->getConstructeurLinks()),
```

- [ ] **Step 3: Update normalizeConstructeurs → normalizeConstructeurLinks (lines ~814-824)**

Replace the method:
```php
private function normalizeConstructeurs(Collection $constructeurs): array
{
    $items = [];
    foreach ($constructeurs as $constructeur) {
        $items[] = [
            'id'    => $constructeur->getId(),
            'name'  => $constructeur->getName(),
            'email' => $constructeur->getEmail(),
            'phone' => $constructeur->getPhone(),
        ];
    }

    return $items;
}
```

With:
```php
private function normalizeConstructeurLinks(Collection $constructeurLinks): array
{
    $items = [];
    foreach ($constructeurLinks as $link) {
        $items[] = [
            'id'                => $link->getId(),
            'constructeur'      => [
                'id'    => $link->getConstructeur()->getId(),
                'name'  => $link->getConstructeur()->getName(),
                'email' => $link->getConstructeur()->getEmail(),
                'phone' => $link->getConstructeur()->getPhone(),
            ],
            'supplierReference' => $link->getSupplierReference(),
        ];
    }

    return $items;
}
```

- [ ] **Step 4: Run php-cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 5: Commit**

```bash
git add src/Controller/MachineStructureController.php
git commit -m "refactor(machines) : update structure controller to use ConstructeurLinks"
```

---

### Task 5: Update audit subscribers

**Files:**
- Modify: `src/EventSubscriber/AbstractAuditSubscriber.php:437-458`
- Modify: `src/EventSubscriber/MachineAuditSubscriber.php:107`
- Modify: `src/EventSubscriber/PieceAuditSubscriber.php:66`
- Modify: `src/EventSubscriber/ComposantAuditSubscriber.php:89`
- Modify: `src/EventSubscriber/ProductAuditSubscriber.php:53`

- [ ] **Step 1: Update `AbstractAuditSubscriber.php`**

The ManyToMany collection tracking for `constructeurs` is no longer needed since constructeur links are now separate entities tracked by their own lifecycle.

In `collectCollectionUpdate()` (line ~438), the check for `'constructeurs' !== $fieldName` can be removed or the method can be simplified. Since `constructeurLinks` is a OneToMany (inverse side), Doctrine won't fire collection update events for it — the Link entities themselves are tracked as inserts/updates/deletes.

If `hasCollectionTracking()` was ONLY used for constructeurs, set it to return `false` in all subscribers and remove the `collectCollectionUpdate` method body. If other collections are tracked, keep the infrastructure but remove the constructeurs-specific logic.

Check: search for any other `fieldName` checks in `collectCollectionUpdate`. If `'constructeurs'` is the only one, simplify by removing the collection tracking entirely.

- [ ] **Step 2: Update snapshot methods in all 4 audit subscribers**

Replace `'constructeurIds' => $this->normalizeCollection($entity->getConstructeurs())` with:

```php
'constructeurIds' => array_map(
    fn ($link) => [
        'id'                => $link->getConstructeur()->getId(),
        'name'              => $link->getConstructeur()->getName(),
        'supplierReference' => $link->getSupplierReference(),
    ],
    $entity->getConstructeurLinks()->toArray(),
),
```

Apply this change in:
- `MachineAuditSubscriber.php` (line ~107)
- `PieceAuditSubscriber.php` (line ~66)
- `ComposantAuditSubscriber.php` (line ~89)
- `ProductAuditSubscriber.php` (line ~53)

- [ ] **Step 3: Run php-cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 4: Commit**

```bash
git add src/EventSubscriber/
git commit -m "refactor(audit) : update audit subscribers to use ConstructeurLinks"
```

---

### Task 6: Update EntityVersionService

**Files:**
- Modify: `src/Service/EntityVersionService.php:255-281` (checkIntegrity)
- Modify: `src/Service/EntityVersionService.php:395-410` (buildRestoreDiff)
- Modify: `src/Service/EntityVersionService.php:544-575` (restoreConstructeurs)

- [ ] **Step 1: Update `checkIntegrity()` (lines ~255-281)**

No structural change needed — the integrity check already works with `constructeurIds` from the snapshot and checks if the Constructeur entities still exist. The format is compatible.

- [ ] **Step 2: Update `buildRestoreDiff()` (lines ~395-410)**

Replace:
```php
$currentConstructeurIds = [];
if (method_exists($entity, 'getConstructeurs')) {
    foreach ($entity->getConstructeurs() as $c) {
        $currentConstructeurIds[] = $c->getId();
    }
}
```

With:
```php
$currentConstructeurIds = [];
if (method_exists($entity, 'getConstructeurLinks')) {
    foreach ($entity->getConstructeurLinks() as $link) {
        $currentConstructeurIds[] = $link->getConstructeur()->getId();
    }
}
```

- [ ] **Step 3: Update `restoreConstructeurs()` (lines ~544-575)**

Replace the entire method. Instead of add/remove on a ManyToMany collection, we now create/delete Link entities:

```php
private function restoreConstructeurs(object $entity, array $snapshot): void
{
    if (!method_exists($entity, 'getConstructeurLinks')) {
        return;
    }

    $targetIds = [];
    $targetRefs = [];
    foreach ($snapshot['constructeurIds'] ?? [] as $entry) {
        $id = is_array($entry) ? ($entry['id'] ?? null) : $entry;
        if ($id) {
            $targetIds[] = $id;
            $targetRefs[$id] = is_array($entry) ? ($entry['supplierReference'] ?? null) : null;
        }
    }

    // Remove current links not in snapshot
    foreach ($entity->getConstructeurLinks()->toArray() as $link) {
        $cId = $link->getConstructeur()->getId();
        if (!in_array($cId, $targetIds, true)) {
            $this->em->remove($link);
        } else {
            // Update supplierReference if present in snapshot
            if (isset($targetRefs[$cId])) {
                $link->setSupplierReference($targetRefs[$cId]);
            }
        }
    }

    // Add missing constructeur links from snapshot
    $currentIds = array_map(
        fn ($link) => $link->getConstructeur()->getId(),
        $entity->getConstructeurLinks()->toArray(),
    );
    foreach ($targetIds as $id) {
        if (!in_array($id, $currentIds, true)) {
            $constructeur = $this->constructeurs->find($id);
            if (null !== $constructeur) {
                $linkClass = $this->getConstructeurLinkClass($entity);
                $link = new $linkClass();
                $link->{'set' . $this->getEntityShortName($entity)}($entity);
                $link->setConstructeur($constructeur);
                $link->setSupplierReference($targetRefs[$id] ?? null);
                $this->em->persist($link);
            }
        }
    }
}

private function getConstructeurLinkClass(object $entity): string
{
    return match (true) {
        $entity instanceof Machine   => MachineConstructeurLink::class,
        $entity instanceof Piece     => PieceConstructeurLink::class,
        $entity instanceof Composant => ComposantConstructeurLink::class,
        $entity instanceof Product   => ProductConstructeurLink::class,
        default                      => throw new \LogicException('Unsupported entity type'),
    };
}

private function getEntityShortName(object $entity): string
{
    return match (true) {
        $entity instanceof Machine   => 'Machine',
        $entity instanceof Piece     => 'Piece',
        $entity instanceof Composant => 'Composant',
        $entity instanceof Product   => 'Product',
        default                      => throw new \LogicException('Unsupported entity type'),
    };
}
```

Add the necessary use statements at the top.

- [ ] **Step 4: Run php-cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 5: Commit**

```bash
git add src/Service/EntityVersionService.php
git commit -m "refactor(versioning) : update EntityVersionService to use ConstructeurLinks"
```

---

### Task 7: Update ModelTypeCategoryConversionService

**Files:**
- Modify: `src/Service/ModelTypeCategoryConversionService.php:287-299` (piece→composant)
- Modify: `src/Service/ModelTypeCategoryConversionService.php:355-367` (composant→piece)

- [ ] **Step 1: Update piece→composant conversion (lines ~287-299)**

Replace the raw SQL that references `_piececonstructeurs` and `_composantconstructeurs`:

```php
// 2. Transfer constructeur link associations
$this->connection->executeStatement(
    'INSERT INTO composant_constructeur_links (id, composantid, constructeurid, supplierreference, createdat, updatedat)
     SELECT \'cl\' || encode(gen_random_bytes(12), \'hex\'), pcl.pieceid, pcl.constructeurid, pcl.supplierreference, pcl.createdat, pcl.updatedat
     FROM piece_constructeur_links pcl
     WHERE pcl.pieceid IN (SELECT id FROM composants WHERE typecomposantid = :id)',
    ['id' => $modelTypeId],
);

$this->connection->executeStatement(
    'DELETE FROM piece_constructeur_links
     WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)',
    ['id' => $modelTypeId],
);
```

- [ ] **Step 2: Update composant→piece conversion (lines ~355-367)**

```php
// 2. Transfer constructeur link associations
$this->connection->executeStatement(
    'INSERT INTO piece_constructeur_links (id, pieceid, constructeurid, supplierreference, createdat, updatedat)
     SELECT \'cl\' || encode(gen_random_bytes(12), \'hex\'), ccl.composantid, ccl.constructeurid, ccl.supplierreference, ccl.createdat, ccl.updatedat
     FROM composant_constructeur_links ccl
     WHERE ccl.composantid IN (SELECT id FROM pieces WHERE typepieceid = :id)',
    ['id' => $modelTypeId],
);

$this->connection->executeStatement(
    'DELETE FROM composant_constructeur_links
     WHERE composantid IN (SELECT id FROM composants WHERE typecomposantid = :id)',
    ['id' => $modelTypeId],
);
```

- [ ] **Step 3: Run php-cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 4: Commit**

```bash
git add src/Service/ModelTypeCategoryConversionService.php
git commit -m "refactor(conversion) : update category conversion to use ConstructeurLink tables"
```

---

### Task 8: Update MCP Tools

**Files:**
- Modify: `src/Mcp/Tool/Machine/CreateMachineTool.php`
- Modify: `src/Mcp/Tool/Machine/UpdateMachineTool.php`
- Modify: `src/Mcp/Tool/Machine/GetMachineTool.php`
- Modify: `src/Mcp/Tool/Piece/CreatePieceTool.php`
- Modify: `src/Mcp/Tool/Piece/UpdatePieceTool.php`
- Modify: `src/Mcp/Tool/Piece/GetPieceTool.php`
- Modify: `src/Mcp/Tool/Composant/CreateComposantTool.php`
- Modify: `src/Mcp/Tool/Composant/UpdateComposantTool.php`
- Modify: `src/Mcp/Tool/Composant/GetComposantTool.php`
- Modify: `src/Mcp/Tool/Product/CreateProductTool.php`
- Modify: `src/Mcp/Tool/Product/UpdateProductTool.php`
- Modify: `src/Mcp/Tool/Product/GetProductTool.php`

- [ ] **Step 1: Update `CreateMachineTool.php`**

Replace the constructeur-adding loop (lines ~59-65):
```php
foreach ($constructeurIds as $cId) {
    $c = $this->constructeurs->find($cId);
    if (!$c) {
        $this->mcpError('not_found', "Constructeur not found: {$cId}");
    }
    $machine->addConstructeur($c);
}
```

With:
```php
foreach ($constructeurIds as $cId) {
    $c = $this->constructeurs->find($cId);
    if (!$c) {
        $this->mcpError('not_found', "Constructeur not found: {$cId}");
    }
    $link = new MachineConstructeurLink();
    $link->setMachine($machine);
    $link->setConstructeur($c);
    $this->em->persist($link);
}
```

Add `use App\Entity\MachineConstructeurLink;`.

- [ ] **Step 2: Update `UpdateMachineTool.php`**

Replace (lines ~69-80):
```php
if (null !== $constructeurIds) {
    foreach ($machine->getConstructeurs()->toArray() as $existing) {
        $machine->removeConstructeur($existing);
    }
    foreach ($constructeurIds as $cId) {
        $c = $this->constructeurs->find($cId);
        if (!$c) {
            $this->mcpError('not_found', "Constructeur not found: {$cId}");
        }
        $machine->addConstructeur($c);
    }
}
```

With:
```php
if (null !== $constructeurIds) {
    foreach ($machine->getConstructeurLinks()->toArray() as $existing) {
        $this->em->remove($existing);
    }
    foreach ($constructeurIds as $cId) {
        $c = $this->constructeurs->find($cId);
        if (!$c) {
            $this->mcpError('not_found', "Constructeur not found: {$cId}");
        }
        $link = new MachineConstructeurLink();
        $link->setMachine($machine);
        $link->setConstructeur($c);
        $this->em->persist($link);
    }
}
```

Add `use App\Entity\MachineConstructeurLink;`.

- [ ] **Step 3: Update `GetMachineTool.php`**

Replace (lines ~32-38):
```php
$constructeurs = [];
foreach ($machine->getConstructeurs() as $c) {
    $constructeurs[] = [
        'id'   => $c->getId(),
        'name' => $c->getName(),
    ];
}
```

With:
```php
$constructeurs = [];
foreach ($machine->getConstructeurLinks() as $link) {
    $constructeurs[] = [
        'id'                => $link->getConstructeur()->getId(),
        'name'              => $link->getConstructeur()->getName(),
        'supplierReference' => $link->getSupplierReference(),
    ];
}
```

- [ ] **Step 4: Repeat for Piece tools**

Apply the same patterns to `CreatePieceTool.php`, `UpdatePieceTool.php`, `GetPieceTool.php` — replacing `Machine` references with `Piece` and using `PieceConstructeurLink`.

- [ ] **Step 5: Repeat for Composant tools**

Apply to `CreateComposantTool.php`, `UpdateComposantTool.php`, `GetComposantTool.php` — using `ComposantConstructeurLink`.

- [ ] **Step 6: Repeat for Product tools**

Apply to `CreateProductTool.php`, `UpdateProductTool.php`, `GetProductTool.php` — using `ProductConstructeurLink`.

- [ ] **Step 7: Run php-cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 8: Commit**

```bash
git add src/Mcp/Tool/
git commit -m "refactor(mcp) : update MCP tools to use ConstructeurLinks"
```

---

### Task 9: Update test helpers + fix existing tests

**Files:**
- Modify: `tests/AbstractApiTestCase.php`
- Modify: `tests/Mcp/Tool/Machine/MachinesCrudToolTest.php`
- Modify: `tests/Mcp/Tool/Piece/PiecesCrudToolTest.php`
- Modify: `tests/Mcp/Tool/Composant/ComposantsCrudToolTest.php`
- Modify: `tests/Mcp/Tool/Product/ProductsCrudToolTest.php`

- [ ] **Step 1: Add factory helpers in `AbstractApiTestCase.php`**

Add after the existing `createConstructeur()` method:

```php
protected function createMachineConstructeurLink(Machine $machine, Constructeur $constructeur, ?string $supplierReference = null): MachineConstructeurLink
{
    $link = new MachineConstructeurLink();
    $link->setMachine($machine);
    $link->setConstructeur($constructeur);
    $link->setSupplierReference($supplierReference);
    $this->getEntityManager()->persist($link);
    $this->getEntityManager()->flush();

    return $link;
}

protected function createPieceConstructeurLink(Piece $piece, Constructeur $constructeur, ?string $supplierReference = null): PieceConstructeurLink
{
    $link = new PieceConstructeurLink();
    $link->setPiece($piece);
    $link->setConstructeur($constructeur);
    $link->setSupplierReference($supplierReference);
    $this->getEntityManager()->persist($link);
    $this->getEntityManager()->flush();

    return $link;
}

protected function createComposantConstructeurLink(Composant $composant, Constructeur $constructeur, ?string $supplierReference = null): ComposantConstructeurLink
{
    $link = new ComposantConstructeurLink();
    $link->setComposant($composant);
    $link->setConstructeur($constructeur);
    $link->setSupplierReference($supplierReference);
    $this->getEntityManager()->persist($link);
    $this->getEntityManager()->flush();

    return $link;
}

protected function createProductConstructeurLink(Product $product, Constructeur $constructeur, ?string $supplierReference = null): ProductConstructeurLink
{
    $link = new ProductConstructeurLink();
    $link->setProduct($product);
    $link->setConstructeur($constructeur);
    $link->setSupplierReference($supplierReference);
    $this->getEntityManager()->persist($link);
    $this->getEntityManager()->flush();

    return $link;
}
```

Add the use statements for all 4 Link entities.

- [ ] **Step 2: Update MCP test files**

In each test file, replace `$entity->addConstructeur($constructeur)` with the factory helper. For example in `MachinesCrudToolTest.php` (line ~34):

Replace:
```php
$machine->addConstructeur($constructeur);
$this->getEntityManager()->flush();
```

With:
```php
$this->createMachineConstructeurLink($machine, $constructeur);
```

Update assertions to match the new response format (with `supplierReference` field).

Apply the same changes to `PiecesCrudToolTest.php`, `ComposantsCrudToolTest.php`, `ProductsCrudToolTest.php`.

- [ ] **Step 3: Run php-cs-fixer**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 4: Commit**

```bash
git add tests/
git commit -m "test(constructeur) : update test helpers and MCP tests for ConstructeurLinks"
```

---

### Task 10: Run full test suite + fix any remaining issues

- [ ] **Step 1: Run the full test suite**

```bash
make test
```

Expected: all tests pass. If any test fails due to references to the old `getConstructeurs()` / `addConstructeur()` / `removeConstructeur()` methods, fix them to use `getConstructeurLinks()` + Link entities.

- [ ] **Step 2: Fix any failures**

Look for remaining usages of the old API and update them.

- [ ] **Step 3: Run php-cs-fixer one final time**

Run: `make php-cs-fixer-allow-risky`

- [ ] **Step 4: Final commit if needed**

```bash
git add -A
git commit -m "fix(constructeur) : fix remaining references after ConstructeurLink migration"
```

---

### Task 11: Update fixtures dump

- [ ] **Step 1: Dump updated fixtures**

If the dev database has been migrated, dump the new state:

```bash
make fixtures-dump
```

- [ ] **Step 2: Commit**

```bash
git add fixtures/
git commit -m "chore : update fixtures dump after ConstructeurLink migration"
```
