Files
siop2/apps/api/prisma/schema.prisma
pr-daaif 902efa6192 feat(r2.2): préventif — génération mensuelle idempotente — et compteurs
- migration : Asset.underContract + WorkOrder.periodKey avec unicité
  [assetId, periodKey] — l'idempotence de la génération est EN BASE
  (deux appels concurrents ne doublent jamais une grille)
- génération : une grille par appareil sous contrat, périodicités ancrées
  sur la mise en service (mensuelles toujours dues), premier contrôle
  (toutes les tâches) pour un appareil sans historique, échéance fin de
  mois, événement GENERATED ; déclenchement manuel (cron au durcissement)
- gabarits administrables (désactivé = exclu des générations suivantes) ;
  GET /preventive/status (générées, terminées, en retard, premiers contrôles)
- compteurs : GET meters (2 compteurs, récents d'abord) ; relevé
  strictement croissant, refus motivé sinon
- contrat +7 opérations (48) ; 50 tests verts (94,7 % / 78 %) ; smoke test
  prod : juillet généré (9 grilles, 8 premiers contrôles), 2e appel à zéro

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:15:57 +01:00

358 lines
12 KiB
Plaintext
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 R0 — identité & permissions (docs/03-architecture/modele-donnees.md).
// La matrice rôles × objets × droits vit EN BASE ; le JWT ne porte jamais de droits.
generator client {
provider = "prisma-client-js"
// Cibles explicites : poste de dev (native) + conteneur node:24-slim
// (OpenSSL 3) en x64 (serveur partenaire) et arm64 (répétition locale).
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-arm64-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Role {
id String @id @default(uuid()) @db.Uuid
name String @unique // 7 rôles seedés — voir @siop/shared ROLE_NAMES
users User[]
permissions Permission[]
}
model Permission {
id String @id @default(uuid()) @db.Uuid
roleId String @db.Uuid
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
objectCategory String // enum applicatif — voir @siop/shared OBJECT_CATEGORIES
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)
phone String?
roleId String @db.Uuid
role Role @relation(fields: [roleId], references: [id])
isActive Boolean @default(true)
isDemo Boolean @default(false) // seul un compte isDemo est empruntable (ADR-002)
// Invitation (R1) : lien d'activation 7 jours, usage unique.
// Statut dérivé : invité = passwordHash null && token présent.
activationToken String? @unique
activationExpiresAt DateTime?
teams Team[]
// R2 — exploitation
workOrdersAssigned WorkOrder[] @relation("WorkOrderAssignees")
workOrdersCreated WorkOrder[] @relation("WorkOrderCreator")
workOrderEvents WorkOrderEvent[]
requests Request[]
checklistDone ChecklistItem[]
meterReadings MeterReading[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([roleId])
}
// ————— R1 — Référentiel (docs/03-architecture/modele-donnees.md §R1) —————
enum CategoryKind {
EQUIPMENT
COMPONENT_TYPE
}
enum AssetStatus {
IN_SERVICE
OUT_OF_SERVICE
UNDER_MAINTENANCE
}
model Category {
id String @id @default(uuid()) @db.Uuid
kind CategoryKind
name String
isActive Boolean @default(true) // désactivable, jamais supprimée si utilisée
assets Asset[]
components AssetComponent[]
taskTemplates TaskTemplate[]
@@unique([kind, name])
}
model Location {
id String @id @default(uuid()) @db.Uuid
name String
parentId String? @db.Uuid // site (null) → zone ; profondeur max 2 (service)
parent Location? @relation("LocationTree", fields: [parentId], references: [id])
children Location[] @relation("LocationTree")
address String?
city String?
guardianName String?
guardianPhone String?
latitude Float?
longitude Float?
// + colonne PostGIS générée (voir migration r1_referentiel) :
// position geography(Point,4326) GENERATED ALWAYS AS (…) STORED
assets Asset[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([parentId])
}
model Asset {
id String @id @default(uuid()) @db.Uuid
reference String @unique // « A1 » — imprimée sur l'étiquette QR
brand String
model String?
serialNumber String?
commissionedAt DateTime?
loadKg Int?
floors Int?
status AssetStatus @default(IN_SERVICE) // statut d'ÉQUIPEMENT ≠ statut d'OT
underContract Boolean @default(true) // contrat préventif (grille mensuelle)
categoryId String @db.Uuid
category Category @relation(fields: [categoryId], references: [id])
locationId String @db.Uuid
location Location @relation(fields: [locationId], references: [id])
components AssetComponent[]
workOrders WorkOrder[]
requests Request[]
meters Meter[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([locationId])
@@index([categoryId])
}
// Organe : PAS de colonne emplacement — « un organe n'a pas d'emplacement
// propre » est garanti par construction (décision maquettes R1).
model AssetComponent {
id String @id @default(uuid()) @db.Uuid
assetId String @db.Uuid
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
typeId String @db.Uuid
type Category @relation(fields: [typeId], references: [id])
designation String?
@@index([assetId])
}
model Team {
id String @id @default(uuid()) @db.Uuid
name String @unique
description String?
members User[]
}
// ————— R2 — Exploitation (docs/03-architecture/modele-donnees.md §R2) —————
enum WorkOrderType {
CORRECTIVE // Dépannage
PREVENTIVE // Maintenance (grille du mois)
WORKS // Travaux
}
enum WorkOrderStatus {
OPEN
IN_PROGRESS
ON_HOLD
DONE
CANCELLED
}
enum WorkOrderPriority {
NONE
LOW
MEDIUM
HIGH
PERSON_TRAPPED // personne bloquée — urgence absolue
}
enum RequestStatus {
RECEIVED
APPROVED
REJECTED
}
enum ChecklistState {
PENDING
DONE
NA
}
enum BilanField {
DOOR_STATE
CABIN_POSITION
ANOMALY
EXTERNAL_CAUSE
ACTION_TAKEN
COMPONENT_CONCERNED
}
enum MeterKind {
RUNNING_HOURS
STARTS
}
model WorkOrder {
id String @id @default(uuid()) @db.Uuid
reference String @unique // OT-2026-0341
title String
description String?
type WorkOrderType
status WorkOrderStatus @default(OPEN) // machine à états stricte (service)
priority WorkOrderPriority @default(NONE)
assetId String @db.Uuid
asset Asset @relation(fields: [assetId], references: [id])
dueDate DateTime?
assignees User[] @relation("WorkOrderAssignees")
createdById String? @db.Uuid
createdBy User? @relation("WorkOrderCreator", fields: [createdById], references: [id])
startedAt DateTime?
completedAt DateTime?
cancelledAt DateTime?
events WorkOrderEvent[]
checklist ChecklistItem[]
report InterventionReport?
request Request?
// Grille du mois : « AAAA-MM » — l'unicité [assetId, periodKey] EST
// l'idempotence de la génération (les OT correctifs restent à null).
periodKey String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([assetId, periodKey])
@@index([assetId])
@@index([status])
}
model WorkOrderEvent {
id String @id @default(uuid()) @db.Uuid
workOrderId String @db.Uuid
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
kind String // COMMENT · STATUS_CHANGED · ASSIGNED · CREATED · FROM_REQUEST
message String?
byId String? @db.Uuid
by User? @relation(fields: [byId], references: [id])
createdAt DateTime @default(now())
@@index([workOrderId])
}
model Request {
id String @id @default(uuid()) @db.Uuid
reference String @unique // DEM-2026-0112
description String
isPersonTrapped Boolean @default(false)
status RequestStatus @default(RECEIVED)
rejectionReason String? // REQUIS au rejet (service)
assetId String @db.Uuid
asset Asset @relation(fields: [assetId], references: [id])
requestedById String? @db.Uuid
requestedBy User? @relation(fields: [requestedById], references: [id])
requesterName String? // portail public via QR (R2.4)
workOrderId String? @unique @db.Uuid // lien 1-1 — jamais de doublon
workOrder WorkOrder? @relation(fields: [workOrderId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([assetId])
}
// Référentiels administrables du bilan codé (un par champ)
model ReferenceValue {
id String @id @default(uuid()) @db.Uuid
field BilanField
label String
isActive Boolean @default(true)
doorStates InterventionReport[] @relation("BilanDoorState")
cabinPositions InterventionReport[] @relation("BilanCabinPosition")
anomalies InterventionReport[] @relation("BilanAnomaly")
externalCauses InterventionReport[] @relation("BilanExternalCause")
actionsTaken InterventionReport[] @relation("BilanActionTaken")
componentsConcerned InterventionReport[] @relation("BilanComponentConcerned")
@@unique([field, label])
}
model InterventionReport {
id String @id @default(uuid()) @db.Uuid
workOrderId String @unique @db.Uuid
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
note String?
doorStateId String? @db.Uuid
doorState ReferenceValue? @relation("BilanDoorState", fields: [doorStateId], references: [id])
cabinPositionId String? @db.Uuid
cabinPosition ReferenceValue? @relation("BilanCabinPosition", fields: [cabinPositionId], references: [id])
anomalyId String? @db.Uuid
anomaly ReferenceValue? @relation("BilanAnomaly", fields: [anomalyId], references: [id])
externalCauseId String? @db.Uuid
externalCause ReferenceValue? @relation("BilanExternalCause", fields: [externalCauseId], references: [id])
actionTakenId String? @db.Uuid
actionTaken ReferenceValue? @relation("BilanActionTaken", fields: [actionTakenId], references: [id])
componentConcernedId String? @db.Uuid
componentConcerned ReferenceValue? @relation("BilanComponentConcerned", fields: [componentConcernedId], references: [id])
updatedAt DateTime @updatedAt
}
model TaskTemplate {
id String @id @default(uuid()) @db.Uuid
label String @unique
componentTypeId String? @db.Uuid
componentType Category? @relation(fields: [componentTypeId], references: [id])
periodMonths Int // 1, 3, 6, 12…
isRegulatory Boolean @default(false) // essai parachute
isActive Boolean @default(true)
checklistItems ChecklistItem[]
}
model ChecklistItem {
id String @id @default(uuid()) @db.Uuid
workOrderId String @db.Uuid
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
label String
state ChecklistState @default(PENDING)
templateId String? @db.Uuid
template TaskTemplate? @relation(fields: [templateId], references: [id])
doneById String? @db.Uuid
doneBy User? @relation(fields: [doneById], references: [id])
doneAt DateTime?
@@index([workOrderId])
}
model Meter {
id String @id @default(uuid()) @db.Uuid
assetId String @db.Uuid
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
kind MeterKind
readings MeterReading[]
@@unique([assetId, kind])
}
model MeterReading {
id String @id @default(uuid()) @db.Uuid
meterId String @db.Uuid
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
value Int // strictement croissant (service)
readById String? @db.Uuid
readBy User? @relation(fields: [readById], references: [id])
createdAt DateTime @default(now())
@@index([meterId])
}