Files
siop2/docs/03-architecture/modele-donnees.md
pr-daaif 266ffaaf1b 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>
2026-07-16 12:27:48 +01:00

134 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Modèle de données — SIOP V2
> **Rôle de ce document (playbook)** : le modèle est introduit **par release** (on ne
> modélise pas ce qu'on ne construit pas). Chaque section explique les invariants et OÙ
> ils vivent (base / service). La v1 sert de référence éprouvée pour les entités à venir.
## R0 — Identité & permissions
```prisma
model Role {
id String @id @default(uuid()) @db.Uuid
name String @unique // Administrateur, Dispatcher, Technicien,
users User[] // Technicien limité, Gestionnaire,
permissions Permission[] // Demandeur, Vue seule
}
model Permission { // matrice rôles × objets × droits — EN BASE, jamais dans le JWT
id String @id @default(uuid()) @db.Uuid
roleId String @db.Uuid
role Role @relation(...)
objectCategory String // enum applicatif : WORK_ORDERS, ASSETS, …
canView Boolean @default(false)
canViewOther Boolean @default(false) // « voir autre » : au-delà de ses propres objets
canCreate Boolean @default(false)
canEdit Boolean @default(false)
canDelete Boolean @default(false)
@@unique([roleId, objectCategory])
}
model User {
id String @id @default(uuid()) @db.Uuid
email String @unique
displayName String
passwordHash String? // null tant que le compte n'est pas activé (R1)
roleId String @db.Uuid
role Role @relation(...)
isActive Boolean @default(true)
isDemo Boolean @default(false) // seul un compte isDemo est empruntable (ADR-002)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
**Invariants R0** :
| Invariant | Où il vit |
| --- | --- |
| Un rôle par utilisateur ; la matrice décide de tout accès | `PermissionsGuard` (relit la base, cache 60 s) |
| Le JWT ne porte jamais de droits (seulement l'identité + roleId) | conception auth |
| `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)
- **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`.
- **R3** : `Part`/`StockMovement` (stock **dérivé des mouvements**), `PurchaseOrder`,
`Partner`, `LaborTime` (taux figé), `Document`.
- **R4** : `WorkOrder.version` (verrou optimiste de la synchro mobile).
- **R5** : tables d'embeddings pgvector (côté service IA).
## Journal des migrations
| # | Migration | Contenu |
| --- | --- | --- |
| 1 | `r0_identity` | Role, Permission, User (+ index & uniques ci-dessus) |