feat(r1.1): socle backend du référentiel — modèle, contrat, API, seed, tests

- migration r1_referentiel : Category (EQUIPMENT/COMPONENT_TYPE), Location
  (site → zone, lat/lng + colonne PostGIS générée geography(Point,4326)
  + index GIST), Asset (statut d'équipement), AssetComponent (organe sans
  emplacement PAR CONSTRUCTION), Team, invitation sur User ; migration
  autosuffisante (CREATE EXTENSION IF NOT EXISTS postgis)
- contrat : 21 nouvelles opérations (26 total), générateur OpenAPI étendu
  aux paramètres de chemin ; spec + client web régénérés dans ce commit
- API : modules categories/locations/assets/teams + gestion des personnes
  (liste, rôles, invitation lien 7 j à usage unique, activation publique
  qui connecte directement, mise à jour rôle/équipes) — tout sous
  @RequirePermission ; invariants en service (profondeur 2, kinds,
  catégorie jamais supprimée)
- seed : parc de la maquette validée (5 sites + 8 zones, 8 appareils,
  organes A1/B2, 9 catégories, 2 équipes) — idempotent
- 36 tests verts (couverture 96 % stmts / 85 % branches) : recette
  site→zone→appareil→organes, matrice vivante, invitation→activation ;
  smoke test sur build de prod
- CI : postgres → postgis/postgis:18-3.6 (la migration R1 l'exige)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-16 12:27:48 +01:00
parent 6d9aafdab4
commit 266ffaaf1b
36 changed files with 6016 additions and 13 deletions

View File

@@ -50,10 +50,74 @@ model User {
| `demo-login` refuse tout compte `isDemo=false` | service auth (+ test e2e) |
| Matrice complète : chaque rôle a une ligne par catégorie d'objet | seed idempotent (+ test) |
## R1 — Référentiel
```prisma
enum CategoryKind { EQUIPMENT COMPONENT_TYPE } // référentiels administrables
enum AssetStatus { IN_SERVICE OUT_OF_SERVICE UNDER_MAINTENANCE }
model Category { // renommable/désactivable, JAMAIS supprimée si utilisée
id String @id @default(uuid()) @db.Uuid
kind CategoryKind
name String
isActive Boolean @default(true)
@@unique([kind, name])
}
model Location { // site (parentId null) → zone (1 niveau max, vérifié service)
id String @id @default(uuid()) @db.Uuid
name String
parentId String? @db.Uuid // auto-relation « LocationTree »
address String? city String?
guardianName String? guardianPhone String?
latitude Float? longitude Float? // saisies par la carte
// + colonne PostGIS générée (migration SQL) :
// position geography(Point,4326) GENERATED ALWAYS AS (ST_Point(longitude,latitude)::geography) STORED
}
model Asset { // l'appareil (ascenseur, monte-charge…)
id String @id @default(uuid()) @db.Uuid
reference String @unique // « A1 », « B2 » — imprimée sur le QR
brand String model String? serialNumber String?
commissionedAt DateTime? loadKg Int? floors Int?
status AssetStatus @default(IN_SERVICE) // statut d'ÉQUIPEMENT ≠ statut d'OT
categoryId String @db.Uuid // Category(kind=EQUIPMENT)
locationId String @db.Uuid // rattachement obligatoire
components AssetComponent[]
}
model AssetComponent { // organe — PAS de colonne emplacement : la règle
id String @id @default(uuid()) @db.Uuid // « un organe n'a pas d'emplacement
assetId String @db.Uuid // propre » est garantie PAR
typeId String @db.Uuid // CONSTRUCTION (table dédiée),
designation String? // plus besoin du CHECK v1
}
model Team { // équipes par zone — l'assignation d'OT (R2) s'appuiera dessus
id String @id @default(uuid()) @db.Uuid
name String @unique
description String?
members User[] // m2m implicite
}
// User (R0) reçoit : phone?, teams Team[], et l'invitation (ADR maquettes R1) :
// activationToken String? @unique + activationExpiresAt DateTime?
// statut dérivé : invité = passwordHash null && token présent ; actif = hash présent
```
**Invariants R1** :
| Invariant | Où il vit |
| --- | --- |
| Hiérarchie d'emplacements limitée à 2 niveaux (site → zone) | service locations (+ test) |
| Un organe n'a pas d'emplacement propre | par construction (AssetComponent sans locationId) |
| `Category(kind)` cohérente avec l'usage (EQUIPMENT sur Asset, COMPONENT_TYPE sur organe) | services (+ test) |
| Catégorie utilisée : jamais supprimée (désactivation seulement) | service categories (+ test) |
| Lien d'activation : 7 jours, usage unique, aucun compte actif avant | service users (+ test e2e) |
| Position : lat/lng saisis, colonne PostGIS **générée** pour les requêtes spatiales futures | migration SQL |
## À venir (référence v1 éprouvée, sera réintroduit release par release)
- **R1** : `Location` (hiérarchie + colonne PostGIS), `Asset` + organes (hiérarchie 2 niveaux,
CHECK « un organe n'a pas d'emplacement propre »), `Category`, `Team`.
- **R2** : `WorkOrder` (machine à états stricte, priorité « personne bloquée »), `Request`
(lien 1-1 vers OT), `InterventionReport` (bilan codé 6 champs → `ReferenceValue`),
`TaskTemplate`/`PreventivePlan`/`ChecklistItem` (périodicité calendrier), `Meter`.