# eRegulations-Next — Lot 1 : socle, modèle de données et importeur

- **Date** : 2026-09-24
- **Statut** : conception validée en conversation, spec à relire
- **Référence** : état des lieux du système actuel, `docs/00-etat-des-lieux.md`

## 1. Contexte et objectif

eRegulations (et sa variante TradePortal) tourne aujourd'hui sur trois applications .NET/Angular plus une bibliothèque partagée, une base SQL Server par pays, trois bases partagées entre pays et quatre dossiers de fichiers montés. eRegulations-Next est une réécriture **à fonctionnalités égales**, plus simple, sur le stack des autres applications `*-Next`, capable d'importer les données des instances existantes (7.x et legacy 4.x/5.x).

Ce document spécifie le **lot 1** : le socle de l'application, le modèle de données complet et l'importeur.

## 2. Décisions prises

| Sujet | Décision |
|---|---|
| Périmètre fonctionnel | Toutes les fonctionnalités actuelles sont conservées ; seules leurs implémentations sont simplifiées. |
| Topologie | **Une instance par pays** : une image commune, un déploiement et une base Postgres par pays (serveur Postgres partagé possible). |
| Authentification | **Locale** : identifiant et mot de passe dans la base de l'instance, next-auth v4 (Credentials). Pas de Keycloak. |
| API publique | **Nouvelle API** `/api/v1` documentée en OpenAPI, plus les **routes legacy** utilisées par les externes (`/Filters`, `/Objectives`, `/Procedures`) au format historique à l'identique. Simplifications-Next migre vers la nouvelle API. |
| Découpage | Lot 1 socle + modèle + importeur ; lot 2 site public + API ; lot 3 back-office (édition, publication) ; lot 4 traductions, revue de cohérence, statuts, droits fins ; lot 5 modules TradePortal ; lot 6 exploitation (archives d'export, déploiement de flotte). |
| Ordre | Lot 1, puis lot 2 (site public), puis le back-office. |

## 3. Périmètre du lot 1

**Inclus**
- Application Next.js déployable : connexion locale, `/api/healthz`, mise en page minimale de `/admin` (connexion, déconnexion, page d'accueil vide).
- Schéma Prisma complet couvrant tous les domaines, y compris ceux dont les écrans arrivent dans les lots suivants.
- Catalogue des permissions dans le code et vérification des droits côté serveur (fonctions utilitaires, pas encore d'écrans).
- Format du document publié (`ProcedureVersion.document`) défini et validé par un schéma zod.
- Importeur en ligne de commande avec son rapport.

**Exclus**
- Écrans métier (site public, back-office), API publique : lots 2 et suivants.
- Import incrémental : l'import est toujours un rechargement complet.
- Export d'une instance en archive : lot 6.

## 4. Socle technique

### 4.1 Stack
Aligné sur Academy-Next :
- Next.js 16 (App Router), React 19, TypeScript strict.
- Prisma 7 + Postgres (16 ou plus), extension `pg_trgm` pour la recherche.
- next-auth v4 (Credentials, sessions JWT).
- next-intl : libellés d'interface dans le repo (`en` fait référence, puis `fr`, `es` ; fichiers aux clés identiques).
- Tailwind 4 + composants du registre `@design` (Design-Next), icônes `lucide-react`.
- Jest pour les tests ; image Docker autonome ; l'entrypoint lance `prisma migrate deploy` puis `node server.js`.
- Portes de vérification : `npm run typecheck && npm run lint && npm test && npm run build`.

### 4.2 Structure du repo
```
src/
  app/
    [locale]/            site public (lot 2)
    admin/               back-office
    api/healthz/         santé
    api/v1/              API publique (lot 2)
    embed/               pages intégrables (lot 2)
  server/<domaine>/      server actions et requêtes par domaine
  lib/
    auth/                next-auth, hachage, limitation des tentatives
    permissions/         catalogue et vérification des droits
    i18n-content/        lecture des champs traduisibles (repli sur la langue par défaut)
    publication/         schéma zod du document publié
    schemas/             schémas zod des entrées
  modules/               tariffs, hs-codes, products, abc (lot 5)
prisma/schema.prisma
tools/import/            importeur (section 7)
```

### 4.3 Conventions serveur
Toute modification passe par une server action : vérification des droits, validation zod, transaction Prisma avec écriture d'une entrée `History`, rafraîchissement des pages concernées, retour `ok()` ou `err(clé)`.

### 4.4 Configuration
- **Variables d'environnement** (techniques uniquement) : `DATABASE_URL`, `NEXTAUTH_URL`, `NEXTAUTH_SECRET`, `MEDIA_DIR` (défaut `/data/media`).
- **Tout le reste** vit dans la table `Setting` et se modifie depuis l'admin (lot 3) : langues du contenu, langue et devise par défaut, devises utilisées, modules activés, analytics, reCAPTCHA, SMTP, durée de session, mise en page du site, CSS de l'instance.

### 4.5 Médias
Fichiers dans `MEDIA_DIR`, servis par une route qui vérifie que l'élément propriétaire est visible par le visiteur. Les noms de fichiers d'origine (`<guid>.<ext>`) sont conservés.

## 5. Authentification et droits

### 5.1 Connexion
- Identifiant (insensible à la casse, unique) et mot de passe, chiffré en **bcrypt**.
- Champ `User.passwordFormat` : `BCRYPT` ou `LEGACY_SHA256`. À la connexion, un mot de passe au format `LEGACY_SHA256` est vérifié (SHA-256 hexadécimal des octets UTF-8), puis re-chiffré en bcrypt.
- Session : expiration après inactivité (30 minutes par défaut, réglable), partagée entre onglets. Un champ `User.sessionVersion` est incrémenté à chaque changement de mot de passe ou désactivation : les jetons portant une version antérieure sont refusés.
- Limitation des tentatives par compte et par adresse IP (compteur en base, fenêtre glissante).
- Réinitialisation par e-mail : jeton à usage unique, valable 1 heure, stocké haché.
- Aucun mot de passe dans une URL.

### 5.2 Droits
- **Catalogue des permissions dans le code**, noms `ressource.action` identiques à ceux de la table `Permission` actuelle, ce qui rend l'import direct.
- `Role` : nom, libellé, liste de permissions, indicateur « rôle système » (non supprimable).
- `RoleAssignment` : utilisateur, rôle, et **portée** : vide pour toute l'instance, ou un objectif (la portée s'applique à l'objectif, ses descendants, leurs blocs et leurs étapes).
- Une action est autorisée si l'un des rôles de l'utilisateur, dont la portée couvre l'objet, contient la permission.
- Les exceptions « accordées ou refusées » et les permissions par objet de l'ancien système sont converties à l'import en rôles personnalisés (section 7.4). Il n'y a plus de refus explicite.
- Utilisateurs « maîtres » (personnel CNUCED) : copiés dans chaque instance, comme aujourd'hui.

## 6. Modèle de données

### 6.1 Principes
1. **Identifiants numériques d'origine conservés** pour les objets adressés par URL ou API (objectifs, blocs, étapes, institutions, documents, lois, menus, filtres). Exception : les unités (section 7.4).
2. **Champs traduisibles en JSON** : `{ "fr": "…", "en": "…" }`. Lecture via `lib/i18n-content` avec repli sur la langue par défaut de l'instance. Une colonne `searchText` (texte concaténé de toutes les langues) est mise à jour à chaque écriture et indexée en trigrammes.
3. **Brouillon normalisé, version publiée en document** : publier crée une `ProcedureVersion` contenant la procédure complète dans toutes ses langues. Le site public et l'API ne lisent que ces versions.
4. **Suppression** : `deletedAt` (suppression douce) et `trashedAt` (corbeille), là où l'ancien système avait `Deleted` et `IsInRecycleBin`.
5. **Traçabilité** : `createdAt`, `createdById`, `updatedAt`, `updatedById` (références à `User`). Quand l'ancien nom d'utilisateur ne correspond à aucun compte, il est gardé dans `legacyAuthor`.
6. **Références légales** : partout où une section cite une loi, on utilise la paire `lawId` + `articles` (JSON traduisible).

### 6.2 Tables

**Arbre des procédures**
- `Objective` : name, description, explanatoryText, additionalInfo, summary (traduisibles) ; icon, images (liste de médias) ; visibleToGuests, isFilterSearchResult ; hiddenSections (liste) ; hiddenLanguages (liste) ; seo `{description, keywords}` (traduisible) ; summaryComments.
- `ObjectiveLink` : parentId (nul pour une racine), childId, order. Un objectif peut avoir plusieurs parents.
- `Block` : name, description ; optional, physicalPresence, thirdPartyRepresentation.
- `ObjectiveBlock` : objectiveId, blockId, order.
- `Step` : name, summary, additionalInfo, lawsComments, costsComments, timeframeComments (traduisibles) ; kind `STEP | RECOURSE` ; internal, optional ; categoryId ; online `{url, type}` ; physicalPresence, thirdPartyRepresentation ; hasCosts, noCostReason ; paymentMethods (liste) + paymentOther ; timeframe `{queue:{min,max}, counter:{min,max}, untilNext:{min,max}}` ; contactLawId/Articles, timeframeLawId/Articles, recourseLawId/Articles ; hiddenSections ; certification `{date, userId, organizationId}` ; abcLevelId, numberOfUsers ; status (statut courant du workflow).
- `BlockStep` : blockId, stepId, order, alternative, parallel.
- `StepRecourse` : stepId, recourseStepId, order.

**Contenu d'une étape**
- `StepContact` : stepId, role `MAIN | RECOURSE`, regionId (nul = cas général), organizationId, personId.
- `StepRequirement` : stepId, order, kind `DOCUMENT | PRIOR_RESULT | SEPARATOR`, documentId, priorResultId, originals, copies, certifiedCopies, operator `AND | OR`, audience, lawId/articles, comments.
- `StepResult` : stepId, order, name, type, documentId, lawId/articles, quantités, documentPresent.
- `StepCost` : stepId, order, kind `COST | SEPARATOR`, type `TAX | EXTERNAL_FEE`, value, currency, operator `FIXED | PERCENT | PER`, costVariableId, averageValue, aggregate `AND | OR`, audience, lawId/articles, comments.
- `StepLaw` : stepId, lawId, order, articles, comments, operator.

**Référentiels**
- `Organization` : kind `ENTITY | UNIT`, parentId (institution d'une unité), name, address, phones, faxes, emails, websites (listes), mapUrl, imageId, schedule (JSON 7 jours × matin et soir ; nul = hérité du parent), zoneId, online, inDirectory, ownership, legacyUnitId.
- `Person` : organizationId, name, profession, phones, emails, imageId.
- `Document` : name, description, type, pages, issuedByInstitution, present, url, inDirectory.
- `DocumentAbcCost` : documentId, staffLevelId, task, staffTime, automated.
- `Law` : name, description, inDirectory.
- `CostVariable` : name (unique), label, hypothesis, averageValue, operator, order.
- `Partner` : name, description, logo, url, order.

**Filtres**
- `Filter` : name, inclusive, productRelated, order.
- `FilterOption` : filterId, name, order, hsCodes (liste).
- `ObjectiveFilter` : objectiveId, filterId, defaultOptionId, label, active, enabled.
- `ObjectiveFilterOption` : objectiveFilterId, optionId.

**Médias**
- `Media` : name (traduisible), files (JSON par langue : nom de fichier, extension, taille), externalUrl, previewFile.
- `Attachment` : mediaId, ownerType (énumération reprenant les types actuels : résultat, section contact, document, loi, section coûts, section délais, infos complémentaires, certification, menu, équipe, recours, objectif, institution, résumé d'objectif), ownerId, order.

**Listes de valeurs**
- `Lookup` : type `REGION | ZONE | STEP_CATEGORY | THIRD_PARTY | STAFF_LEVEL | FEEDBACK_CATEGORY`, name (traduisible), value, order, disabled.
- Pays et devises : listes ISO dans le code.

**Site**
- `Menu` : parentId, order, name, content (traduisibles), mapping `{objectiveId | blockId}`, icons, images, columns, widePage, visibleInMenu, visibleOnHomepage, visibleToGuests, hiddenLanguages.
- `Setting` : key, value (JSON), updatedAt, updatedById. Clés : `languages`, `general`, `homepage.<lang>`, `layout`, `customCss`, `modules`, `analytics`, `mail`, `security`, `team`, `abcCosts`.
- `SettingVersion` : historique de chaque modification de `Setting` (permet de revenir en arrière, notamment pour le CSS).
- `TeamMember` : team `AUTHORITIES | EREG_TEAM`, name, role, photo, order.
- `Feedback` : coordonnées, pays, procédure, catégorie, message, date, status.
- `UiMessageOverride` : locale, key, value.

**Publication**
- `ProcedureVersion` : objectiveId, version, publishedAt, publishedById, current (une seule version courante par objectif), languages (langues publiées), document (JSON, section 6.3).

**Workflow**
- `ReviewTicket` : target `{type: HOMEPAGE | MENU | SUMMARY | STEP, id, section}`, priority, status `OPEN | PENDING | FINISHED | ARCHIVED | DELETED`, assigneeId, authorId, dates.
- `ReviewComment` : ticketId, authorId, body, createdAt.
- `StepStatusChange` : stepId, status, version, cycle, userId, at, comment.

**Utilisateurs**
- `User` : username, email, firstName, lastName, title, timezone, imageId, passwordHash, passwordFormat, sessionVersion, isMaster, receiveReviewDigest, disabledAt, lastLoginAt, failedLogins.
- `Role`, `RoleAssignment` (section 5.2).
- `FeedbackRecipient` : userId, targetType, targetId.
- `PasswordResetToken` : userId, tokenHash, expiresAt, usedAt.

**Divers**
- `History` : entity, entityId, action, userId (ou legacyAuthor), at, changes (JSON `{champ: [avant, après]}`).
- `StatsSnapshot` : date, metrics (JSON).
- `TariffsCache` : key, payload, expiresAt.
- `ImportRun` : startedAt, finishedAt, source, options, status, report (JSON).

### 6.3 Document publié
Défini par un schéma zod dans `src/lib/publication/document.ts`, avec un champ `schemaVersion: 1`. Il contient tout ce qu'il faut pour afficher la procédure sans autre requête :
- l'objectif (textes, visibilités, sections masquées, filtres) ;
- les blocs dans l'ordre, et pour chacun ses étapes dans l'ordre (alternative, parallèle) ;
- pour chaque étape, toutes ses sections, avec les contacts (organisation, personne, horaires résolus), documents, résultats, coûts, délais, lois, recours, certification et pièces jointes recopiés dans le document ;
- les totaux pré-calculés (coûts par devise, délais min et max).

Tous les textes y restent au format traduisible `{lang: texte}`.

## 7. Importeur

### 7.1 Utilisation
```
npm run import -- \
  --country pays.bak \
  [--global global.bak] \
  [--consistency consistency.bak --system-id <n>] \
  [--statistics statistics.bak] \
  --content <dossier ou .zip> \
  [--user-map users.json] \
  [--compare-url https://site-public-actuel] \
  --target $DATABASE_URL [--replace]
```

### 7.2 Étapes
1. **Préparation** : lancement d'un conteneur SQL Server 2022 jetable, restauration des sauvegardes (`RESTORE FILELISTONLY` puis `RESTORE … MOVE`), niveau de compatibilité réglé à 150. Les sauvegardes source ne sont jamais modifiées.
2. **Détection et mise à niveau** : une base avec `__EFMigrationsHistory` est une 7.x. Une base legacy est mise au format 7.x dans le conteneur, avec les scripts du provisioner (`part-a.sql`, `part-b.sql`, `views.sql`, `drift-known.sql`, `legacy-objects.yml`), recopiés dans `tools/import/legacy-upgrade/` avec la référence du commit d'origine. Si la base pays n'a pas de table `User`, les utilisateurs sont repris de la base `global` : utilisateurs maîtres, plus ceux rattachés au `systemId`, selon la logique de `provisioner/src/migrate-sql.ts`.
3. **Extraction** : lecture avec le pilote `mssql`, table par table. Chaque ligne est validée par un schéma zod ; une ligne invalide est rejetée et consignée, sans arrêter l'import.
4. **Transformation** : une fonction pure par domaine (section 7.4).
5. **Chargement** : une seule transaction Postgres, identifiants d'origine insérés explicitement, puis recalage des séquences. Si la base cible n'est pas vide, l'import s'arrête, sauf avec `--replace` qui la vide d'abord.
6. **Médias** : copie de `media/` et `PublicContent/` vers `MEDIA_DIR`. Dans une archive `.zip`, les noms non UTF-8 sont décodés en CP850. Chaque fichier référencé en base est vérifié.
7. **Rapport** : écrit en JSON et en Markdown, et enregistré dans `ImportRun` (section 7.5).

### 7.3 Arrêts bloquants
L'import s'arrête sans rien écrire dans les cas suivants :
- sauvegarde illisible ou échec de restauration ;
- version de schéma non reconnue après mise à niveau ;
- doublons de noms d'utilisateur (même nom à la casse près) sans résolution dans `--user-map` ;
- base cible non vide sans `--replace`.

Le fichier `--user-map` indique, pour chaque groupe de doublons, l'identifiant du compte à garder. Les références des comptes écartés (auteur, destinataire de feedback, rôles) sont reportées sur le compte gardé.

### 7.4 Correspondances
| Source (7.x) | Cible | Règle |
|---|---|---|
| `Admin_Objective`, `_i18n`, `SectionVisibility`, `PerLangVisibility` | `Objective` | Textes fusionnés en JSON, indicateurs de visibilité convertis en listes. |
| `Admin_ObjectiveHierarchicalData` | `ObjectiveLink` | `Parent_Id` nul = racine. |
| `Admin_Block` (+`_i18n`), `Admin_Objective_Block` | `Block`, `ObjectiveBlock` | |
| `Admin_Step` (+`_i18n`, `SectionVisibility`) | `Step` + `StepContact` | Colonnes entité/unité/personne et `Recourse_*` converties en lignes `StepContact` (principal, recours). Colonnes de paiement et de délais converties en JSON. |
| `Admin_StepRegional{Entity,Unit,Person}InCharge` | `StepContact` | Avec `regionId`. |
| `Admin_Block_Step`, `Admin_Step_Recourse` | `BlockStep`, `StepRecourse` | |
| `Admin_StepRequirement`, `StepResult`, `StepCost`, `StepLaw` (+`_i18n`) | tables homonymes | `StepCost.Parameter` (nom) résolu en `costVariableId` ; nom introuvable signalé. |
| `EntityInCharge` (+`_i18n`) | `Organization` (ENTITY) | Identifiant conservé. |
| `UnitInCharge` (+`_i18n`) | `Organization` (UNIT) | Nouvel identifiant (les identifiants des unités chevauchent ceux des institutions), ancien gardé dans `legacyUnitId`. `ScheduleIsInherited` donne `schedule = null`. |
| `PersonInCharge` (+`_i18n`) | `Person` | Rattachée à l'organisation de son unité. |
| Contact avec unité et institution | `StepContact.organizationId` | On garde l'unité si renseignée, sinon l'institution. Une unité qui n'appartient pas à l'institution indiquée est signalée. |
| `GenericRequirement` (+`_i18n`, `_Cost`), `Law` (+`_i18n`) | `Document`, `DocumentAbcCost`, `Law` | |
| `Media`, `Media_i18n`, `Admin_Object_Media` | `Media`, `Attachment` | `Type` converti en `ownerType`. |
| `CostVariable` (+`_i18n`) | `CostVariable` | |
| `Filter`, `FilterOption` (+`_i18n`), `FilterOption_Products` | `Filter`, `FilterOption.hsCodes` | |
| `Objective_Filters` (+`_i18n`), `Objective_FilterOptions` | `ObjectiveFilter`, `ObjectiveFilterOption` | |
| `Option` (+`_i18n`) | `Lookup` ou `Setting` | Devises sélectionnées vers `Setting.general.currencies` ; pays vers le code ISO ; coûts ABC vers `Setting.abcCosts`. |
| `Partner` (+`_i18n`) | `Partner` | |
| `Admin_Menu` (+`_i18n`, `HierarchicalData`, `PerLangVisibility`) | `Menu` | |
| `Public_Teams`, `Public_Team_Members` (+`_i18n`) | `Setting.team`, `TeamMember` | |
| `Feedback` | `Feedback` | |
| `SystemLanguage` | `Setting.languages` | |
| `User` (+`_i18n`) | `User` | Mot de passe bcrypt repris ; en clair, chiffré en bcrypt ; SHA-256 marqué `LEGACY_SHA256`. |
| `Role`, `RolePermission`, `User_Role` | `Role`, `RoleAssignment` (portée vide) | |
| `UserPermission` (accordées ou refusées) | rôle personnalisé | Par utilisateur concerné : ses permissions effectives, plus les accordées, moins les refusées. Les ensembles identiques partagent le même rôle. Rôles d'origine remplacés par ce rôle pour cet utilisateur. |
| `ObjectPermission` | rôle personnalisé avec portée | Regroupées par utilisateur et par objet ; seuls les objectifs peuvent porter une portée : une permission sur un bloc ou une étape est rattachée à chacun de ses objectifs et signalée. |
| `UserFeedback` | `FeedbackRecipient` | Lien par `User_ID` ou par nom d'utilisateur selon la base. |
| `AuditRecords`, `AuditRecordFields` | `History` | |
| `Snapshot_Registry` + `Snapshot_*` | `ProcedureVersion` | Toutes les versions sont importées ; celle marquée `IsCurrent` devient la version courante. Le document est construit depuis les snapshots, **pas depuis le brouillon**. |
| `consistency_tickets`, `consistency_comments` | `ReviewTicket`, `ReviewComment` | Filtrés par `systemId`. |
| `consistency_status` | `StepStatusChange`, `Step.status` | Le dernier statut devient le statut courant. |
| `StatisticSet` | `StatsSnapshot` | Si `--statistics` est fourni ; un instantané par date. |
| `PublicConfig/homepage.config.<lang>.json` | `Setting.homepage.<lang>` | |
| `PublicConfig/custom.css` et `custom.css.d/` | `Setting.customCss` et `SettingVersion` | |
| `Multilang/` | *(non importé)* | Les clés de l'ancienne interface ne correspondent pas à la nouvelle. Le rapport liste les libellés propres à l'instance (écarts par rapport au référentiel commun) pour reprise manuelle. |
| `TariffsCache`, `SiteMenu`, `Public_Objective_Filters`, `XMLSerializedItem`, `Workspace`, `Process*`, `ProcedureContext`, `DeprecatedID` | *(non importés)* | Cache, données reconstruites ou mortes. |

### 7.5 Rapport
- Source : fichiers, version détectée, mise à niveau appliquée ou non.
- Nombre d'éléments par type, source contre cible.
- Lignes rejetées, avec la raison.
- Anomalies : fichiers manquants, unités rattachées à la mauvaise institution, variables de coût introuvables, permissions sur bloc ou étape élargies, références d'auteur non résolues.
- Contrôle par échantillon, si l'option `--compare-url <site public actuel>` est fournie : pour 20 procédures publiées (ou toutes s'il y en a moins), les totaux de coûts et de délais recalculés depuis le document importé sont comparés à ceux que renvoie `/api/procedure/{id}/totals` sur le site actuel. Tout écart est signalé. Sans cette option, le contrôle est omis et le rapport le mentionne.
- Libellés d'interface propres à l'instance (section 7.4).

### 7.6 Bascule d'une instance
L'import peut être relancé autant de fois que nécessaire. Le jour de la bascule : gel de l'édition dans l'ancien admin, import final avec `--replace`, lecture du rapport, bascule du domaine.

## 8. Tests et vérification
- **Unitaires** : chaque fonction de transformation, sur de petits jeux de données écrits à la main ; hachage et vérification des mots de passe ; résolution des droits (portées, héritage) ; schéma du document publié.
- **Intégration** : dans le conteneur SQL Server, création d'une base à partir des scripts de schéma de `eRegulations-deploy/instances/pilot-tradeportal/db/`, insertion d'un jeu de données synthétique couvrant chaque correspondance de la section 7.4, import complet, vérification des comptes et du rapport. Ce test est lancé à la demande (il demande Docker), pas dans la porte par défaut.
- **Validation réelle** : import d'une vraie instance (pilot-tradeportal) et lecture du rapport, sans écart inexpliqué.
- **Porte de chaque tâche** : `npm run typecheck && npm run lint && npm test && npm run build`.

## 9. Risques et points ouverts
- **SQL Server sur Mac Apple Silicon** : l'image officielle est x86 ; elle tourne en émulation, plus lentement. Acceptable pour l'import ; à vérifier dès la première tâche.
- **Fidélité du document publié** : la reconstruction depuis `Snapshot_*` doit reproduire ce qu'affiche le site actuel ; le contrôle par échantillon (7.5) sert de garde-fou, et le lot 2 permettra la comparaison visuelle.
- **Unité des délais** : les valeurs sont importées telles qu'elles sont stockées. L'unité exacte (et la conversion en journées de 8 heures faite par l'API actuelle) sera confirmée en lisant `SnapshotBuilder` pendant le plan, et reprise à l'identique dans le lot 2.
- **Scripts de mise à niveau legacy** : copiés depuis le provisioner. Si le provisioner les corrige, la copie doit être mise à jour ; la référence du commit d'origine est conservée pour le suivi.
- **Détail colonne par colonne** : la section 7.4 fixe les correspondances table par table ; le détail des colonnes sera établi pendant le plan à partir de `CountryDbContextModelSnapshot.cs`.
