# Entity Versioning — Design Spec

**Date :** 2026-03-25
**Entites concernees :** Machine, Composant, Piece, Produit
**Approche :** Extension du systeme AuditLog existant

---

## Objectif

Permettre de consulter l'historique des versions numerotees (v1, v2, v3...) des entites principales et de restaurer n'importe quelle version anterieure, afin de ne jamais perdre de donnees.

---

## Regles metier

### Creation de version
- Chaque `create` ou `update` sur une entite incremente automatiquement le compteur `version` de l'entite
- Le numero de version est enregistre dans l'AuditLog correspondant (nouvelle colonne `version`)

### Restauration
- La restauration cree une **nouvelle version** (v+1) — on ne supprime jamais d'historique
- Le service `EntityVersionService::restore()` cree **manuellement** un AuditLog avec `action = "restore"` et le diff contient `restoredFromVersion: N`
- Important : le flush du restore declenche les AuditSubscribers, qui produiraient un `update` duplique. Pour eviter cela, l'entite porte un flag transitoire `$skipAudit = true` que les subscribers verifient

### Controle de squelette (Composant, Piece, Produit uniquement)
- Avant restauration, on compare le ModelType actuel avec celui du snapshot
- **Meme squelette (ModelType)** : restore complet — champs de base + slots + custom fields
- **Squelette different** : restore partiel — uniquement les champs de base (nom, description, reference, constructeurs, prix)

### Controle d'integrite
- Avant restauration, on verifie que toutes les entites liees dans le snapshot existent encore en base :
  - **Composant** : pieces selectionnees dans les slots, produits, sous-composants, constructeurs
  - **Piece** : produits selectionnes dans les slots, constructeurs
  - **Produit** : constructeurs
  - **Machine** : site, liens composants/pieces/produits (MachineComponentLink, MachinePieceLink, MachineProductLink)
- Les entites manquantes generent des **warnings** affiches a l'utilisateur
- Les slots avec des entites supprimees sont restaures **vides** (sans selection)
- Pour les custom field values : restauration par `fieldId` + entite parente (pas par ID de la CustomFieldValue elle-meme, car un sync ModelType peut recreer les CFV avec des IDs differents)
- Les controles d'integrite utilisent des requetes batch (`findBy(['id' => $ids])`) plutot que des requetes individuelles par slot

### Machines
- Pas de controle de squelette (pas de ModelType) : restauration toujours complete
- Controle d'integrite sur le site et les liens machine
- Machine n'a pas de champ `description` (contrairement aux autres entites)

### Permissions
- Consulter les versions : `ROLE_VIEWER`
- Restaurer une version : `ROLE_GESTIONNAIRE` et au-dessus

---

## Modifications backend

### 1. Colonne `version` sur AuditLog

```sql
ALTER TABLE audit_logs ADD COLUMN version INT DEFAULT NULL;
```

Nullable car les AuditLogs existants n'ont pas de version.

### 2. Colonne `version` sur Machine

```sql
ALTER TABLE machine ADD COLUMN version INT NOT NULL DEFAULT 1;
```

Les entites Composant, Piece, Produit ont deja cette colonne.

### 3. Enrichissement des snapshots

Les Audit Subscribers doivent inclure dans le `snapshot` :

**Composant :**
```json
{
  "id": "cl...",
  "name": "...",
  "reference": "...",
  "description": "...",
  "prix": 100.00,
  "typeComposant": { "id": "cl...", "name": "...", "code": "..." },
  "product": { "id": "cl...", "name": "..." },
  "constructeurIds": [{ "id": "cl...", "name": "..." }],
  "customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
  "pieceSlots": [
    { "id": "cl...", "typePieceId": "cl...", "selectedPieceId": "cl...", "quantity": 1, "position": 0 }
  ],
  "subcomponentSlots": [
    { "id": "cl...", "alias": "...", "familyCode": "...", "typeComposantId": "cl...", "selectedComposantId": "cl...", "position": 0 }
  ],
  "productSlots": [
    { "id": "cl...", "typeProductId": "cl...", "selectedProductId": "cl...", "familyCode": "...", "position": 0 }
  ],
  "version": 3
}
```

**Piece :**
```json
{
  "id": "cl...",
  "name": "...",
  "reference": "...",
  "description": "...",
  "prix": 50.00,
  "typePiece": { "id": "cl...", "name": "...", "code": "..." },
  "product": { "id": "cl...", "name": "..." },
  "constructeurIds": [{ "id": "cl...", "name": "..." }],
  "customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
  "productSlots": [
    { "id": "cl...", "typeProductId": "cl...", "selectedProductId": "cl...", "familyCode": "...", "position": 0 }
  ],
  "version": 2
}
```

**Produit :**
```json
{
  "id": "cl...",
  "name": "...",
  "reference": "...",
  "supplierPrice": 25.00,
  "typeProduct": { "id": "cl...", "name": "...", "code": "..." },
  "constructeurIds": [{ "id": "cl...", "name": "..." }],
  "customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
  "version": 1
}
```

**Machine :**
```json
{
  "id": "cl...",
  "name": "...",
  "reference": "...",
  "prix": 1500.00,
  "site": { "id": "cl...", "name": "..." },
  "constructeurIds": [{ "id": "cl...", "name": "..." }],
  "customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
  "version": 4
}
```

### 4. Incrementation automatique de la version

Dans chaque Audit Subscriber, a chaque `create`/`update` :
1. Appeler `$entity->incrementVersion()`
2. Ecrire `$auditLog->setVersion($entity->getVersion())`

Pour Machine, ajouter la methode `incrementVersion()` et la propriete `version` a l'entite.

### 5. Nouveaux endpoints — `EntityVersionController`

| Methode | Route | Description | Role |
|---------|-------|-------------|------|
| GET | `/api/{entity}/{id}/versions` | Liste des versions | ROLE_VIEWER |
| GET | `/api/{entity}/{id}/versions/{version}/preview` | Preview + controles avant restore | ROLE_GESTIONNAIRE |
| POST | `/api/{entity}/{id}/versions/{version}/restore` | Execute la restauration | ROLE_GESTIONNAIRE |

`{entity}` = `machines`, `composants`, `pieces`, `products`

**GET versions — Response :**
```json
{
  "items": [
    {
      "version": 3,
      "action": "update",
      "createdAt": "2026-03-25T14:30:00+00:00",
      "actor": { "id": "cl...", "label": "Jean Dupont" },
      "diff": { "name": { "from": "Ancien", "to": "Nouveau" } }
    }
  ],
  "total": 3
}
```

**GET preview — Response :**
```json
{
  "version": 2,
  "restoreMode": "full",
  "diff": {
    "name": { "current": "Nouveau", "restored": "Ancien" },
    "reference": { "current": "REF-002", "restored": "REF-001" }
  },
  "warnings": [
    {
      "field": "pieceSlots[0].selectedPieceId",
      "message": "La piece 'Roulement XY' (cl...) n'existe plus. Le slot sera restaure vide.",
      "missingEntityId": "cl...",
      "missingEntityName": "Roulement XY"
    }
  ],
  "snapshot": { }
}
```

`restoreMode` : `"full"` (meme squelette) ou `"partial"` (squelette different, champs de base uniquement).

**POST restore — Response :**
```json
{
  "success": true,
  "newVersion": 6,
  "restoredFromVersion": 2,
  "restoreMode": "full",
  "warnings": []
}
```

### 6. Service `EntityVersionService`

Service centralise pour la logique de versioning :

- `getVersions(string $entityType, string $entityId): array` — liste des versions depuis AuditLog
- `getRestorePreview(string $entityType, string $entityId, int $version): array` — controles + diff
- `restore(string $entityType, string $entityId, int $version): array` — execution du restore

Methodes internes :
- `checkSkeletonCompatibility(object $entity, array $snapshot): string` — retourne `"full"` ou `"partial"`
- `checkIntegrity(string $entityType, array $snapshot): array` — retourne les warnings
- `applyRestore(object $entity, array $snapshot, string $mode): void` — applique les changements

---

## Modifications frontend

### 1. Composant `EntityVersionList.vue`

Composant reutilisable affiche dans un onglet "Versions" sur les pages de detail.

**Props :**
- `entityType: 'machines' | 'composants' | 'pieces' | 'products'`
- `entityId: string`

**Affichage :**
- Tableau : version, date, auteur, action, diff resume
- Badge "Actuelle" sur la version la plus recente
- Bouton "Restaurer" sur chaque ligne (sauf version actuelle), visible uniquement pour ROLE_GESTIONNAIRE+

### 2. Composant `VersionRestoreModal.vue`

Modal de confirmation avec preview.

**Props :**
- `entityType`, `entityId`, `version` (cible)
- `previewData` (resultat du GET preview)

**Affichage :**
- Indicateur de mode : "Restauration complete" ou "Restauration partielle"
- Diff visuel : champs qui changent (valeur actuelle -> valeur restauree)
- Warnings en alerte orange pour les entites manquantes
- Boutons "Confirmer la restauration" / "Annuler"

### 3. Composable `useEntityVersions.ts`

```typescript
interface Deps {
  entityType: MaybeRef<string>
  entityId: MaybeRef<string>
}

export function useEntityVersions(deps: Deps) {
  // fetchVersions() — GET /api/{entity}/{id}/versions
  // fetchPreview(version) — GET /api/{entity}/{id}/versions/{version}/preview
  // restore(version) — POST /api/{entity}/{id}/versions/{version}/restore
}
```

### 4. Integration dans les pages de detail

Ajouter un onglet "Versions" dans les pages :
- `pages/machines/[id].vue`
- `pages/composants/[id].vue`
- `pages/pieces/[id].vue`
- `pages/products/[id].vue`

L'onglet affiche `EntityVersionList` qui gere l'ouverture de `VersionRestoreModal`.

---

## Migration

Une seule migration PostgreSQL :

```sql
-- Colonne version sur audit_logs
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS version INT DEFAULT NULL;

-- Colonne version sur machine
ALTER TABLE machine ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1;

-- Index pour requetes par version
CREATE INDEX IF NOT EXISTS idx_audit_entity_version ON audit_logs (entity_type, entity_id, version);
```

---

## Ce qui change (breaking)

- **Piece snapshot** : le champ legacy `productIds` (ancien JSON) est remplace par `productSlots` (tables normalisees). Les anciens AuditLogs conservent `productIds` dans leur snapshot mais les nouveaux ne l'auront plus. Le restore utilise `productSlots` exclusivement.

## Ce qui ne change PAS

- L'onglet/page d'historique existant (`EntityHistoryController`) reste inchange
- Les AuditLogs existants (sans version) continuent de fonctionner
- Le mecanisme d'audit automatique via les Subscribers reste identique, juste enrichi
- Les documents ne sont pas versionnes (hors scope)
