feat(r2.1): socle backend exploitation — machine à états, bilan codé, demandes 1-1

- migration r2_exploitation (10 tables) : WorkOrder (référence séquentielle,
  horodatages), WorkOrderEvent, Request (1-1, motif de rejet), ReferenceValue,
  InterventionReport (6 FK), TaskTemplate/ChecklistItem, Meter/MeterReading
- contrat : 15 opérations (41 total) ; la table des transitions et les champs
  requis du bilan vivent dans @siop/shared ; la fiche OT expose
  allowedTransitions + closureBlockers (messages métier)
- API : machine à états stricte ; garde de clôture (bilan 3 champs requis +
  checklist sans tâche en attente) ; approbation → OT lié 1-1 (409 si déjà
  traitée) ; rejet à motif obligatoire ; scoping « voir autre » sur listes et
  accès directs (404 sans fuite) ; validation des valeurs de bilan par champ ;
  « personne bloquée » triée en tête côté API
- seed : 31 valeurs de référentiels, 8 gabarits (parachute réglementaire),
  OT/demandes/compteurs de la maquette — idempotent
- 45 tests verts (95 % stmts / 79 % branches) dont la recette officielle
  rejouée de bout en bout ; smoke test sur build de prod

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-16 13:56:07 +01:00
parent 54d926e9f4
commit f7702e4252
23 changed files with 5568 additions and 4 deletions

View File

@@ -355,6 +355,213 @@ export interface paths {
patch: operations["updateUser"];
trace?: never;
};
"/work-orders": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Ordres de travail (sans « voir autre » : seulement les siens) */
get: operations["listWorkOrders"];
put?: never;
/** Créer un OT (statut Ouvert) */
post: operations["createWorkOrder"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/work-orders/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Fiche OT (activité, checklist, bilan, transitions autorisées, blocages de clôture) */
get: operations["getWorkOrder"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/work-orders/{id}/transition": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Changer létat (machine à états stricte ; DONE exige bilan + checklist) */
post: operations["transitionWorkOrder"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/work-orders/{id}/comments": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Commenter (activité chronologique) */
post: operations["commentWorkOrder"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/work-orders/{id}/assignees": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
/** Assigner (remplace la liste) */
put: operations["setWorkOrderAssignees"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/work-orders/{id}/report": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
/** Renseigner le bilan codé (null efface un champ) */
put: operations["upsertWorkOrderReport"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/work-orders/{id}/checklist/{itemId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
/** Régler une tâche de la grille (Fait / N-A / à faire) */
patch: operations["patchChecklistItem"];
trace?: never;
};
"/requests": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Demandes (sans « voir autre » : seulement les siennes) */
get: operations["listRequests"];
put?: never;
/** Signaler (interne — le portail public QR arrive en R2.4) */
post: operations["createRequest"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/requests/{id}/approve": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Approuver → crée lOT lié (1-1, jamais de doublon) */
post: operations["approveRequest"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/requests/{id}/reject": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Rejeter — motif obligatoire, lisible côté demandeur */
post: operations["rejectRequest"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/reference-values": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Référentiels du bilan codé (6 champs) */
get: operations["listReferenceValues"];
put?: never;
/** Ajouter une valeur de référentiel */
post: operations["createReferenceValue"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/reference-values/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
/** Renommer ou (dés)activer — jamais de suppression si utilisée */
patch: operations["updateReferenceValue"];
trace?: never;
};
"/health": {
parameters: {
query?: never;
@@ -734,6 +941,285 @@ export interface components {
teamIds?: string[];
isActive?: boolean;
};
WorkOrdersResponse: {
workOrders: {
/** Format: uuid */
id: string;
reference: string;
title: string;
/** @enum {string} */
type: "CORRECTIVE" | "PREVENTIVE" | "WORKS";
/** @enum {string} */
status: "OPEN" | "IN_PROGRESS" | "ON_HOLD" | "DONE" | "CANCELLED";
/** @enum {string} */
priority: "NONE" | "LOW" | "MEDIUM" | "HIGH" | "PERSON_TRAPPED";
/** Format: uuid */
assetId: string;
assetReference: string;
siteName: string;
dueDate: string | null;
assignees: {
/** Format: uuid */
id: string;
displayName: string;
initials: string;
}[];
/** Format: date-time */
createdAt: string;
}[];
};
WorkOrderDetail: {
/** Format: uuid */
id: string;
reference: string;
title: string;
/** @enum {string} */
type: "CORRECTIVE" | "PREVENTIVE" | "WORKS";
/** @enum {string} */
status: "OPEN" | "IN_PROGRESS" | "ON_HOLD" | "DONE" | "CANCELLED";
/** @enum {string} */
priority: "NONE" | "LOW" | "MEDIUM" | "HIGH" | "PERSON_TRAPPED";
/** Format: uuid */
assetId: string;
assetReference: string;
siteName: string;
dueDate: string | null;
assignees: {
/** Format: uuid */
id: string;
displayName: string;
initials: string;
}[];
/** Format: date-time */
createdAt: string;
description: string | null;
locationName: string;
startedAt: string | null;
completedAt: string | null;
cancelledAt: string | null;
createdBy: {
/** Format: uuid */
id: string;
displayName: string;
initials: string;
} | null;
request: {
/** Format: uuid */
id: string;
reference: string;
requesterLabel: string;
} | null;
events: {
/** Format: uuid */
id: string;
kind: string;
message: string | null;
by: {
/** Format: uuid */
id: string;
displayName: string;
initials: string;
} | null;
/** Format: date-time */
createdAt: string;
}[];
checklist: {
/** Format: uuid */
id: string;
label: string;
/** @enum {string} */
state: "PENDING" | "DONE" | "NA";
doneBy: {
/** Format: uuid */
id: string;
displayName: string;
initials: string;
} | null;
doneAt: string | null;
}[];
report: {
note: string | null;
doorState: {
/** Format: uuid */
id: string;
label: string;
} | null;
cabinPosition: {
/** Format: uuid */
id: string;
label: string;
} | null;
anomaly: {
/** Format: uuid */
id: string;
label: string;
} | null;
externalCause: {
/** Format: uuid */
id: string;
label: string;
} | null;
actionTaken: {
/** Format: uuid */
id: string;
label: string;
} | null;
componentConcerned: {
/** Format: uuid */
id: string;
label: string;
} | null;
} | null;
allowedTransitions: ("OPEN" | "IN_PROGRESS" | "ON_HOLD" | "DONE" | "CANCELLED")[];
closureBlockers: string[];
};
WorkOrderCreate: {
title: string;
description?: string;
/** @enum {string} */
type: "CORRECTIVE" | "PREVENTIVE" | "WORKS";
/** @enum {string} */
priority?: "NONE" | "LOW" | "MEDIUM" | "HIGH" | "PERSON_TRAPPED";
/** Format: uuid */
assetId: string;
/** Format: date-time */
dueDate?: string;
assigneeIds?: string[];
};
TransitionRequest: {
/** @enum {string} */
to: "OPEN" | "IN_PROGRESS" | "ON_HOLD" | "DONE" | "CANCELLED";
comment?: string;
};
CommentCreate: {
message: string;
};
AssigneesUpdate: {
assigneeIds: string[];
};
ReportUpsert: {
note?: string | null;
doorStateId?: string | null;
cabinPositionId?: string | null;
anomalyId?: string | null;
externalCauseId?: string | null;
actionTakenId?: string | null;
componentConcernedId?: string | null;
};
ChecklistItem: {
/** Format: uuid */
id: string;
label: string;
/** @enum {string} */
state: "PENDING" | "DONE" | "NA";
doneBy: {
/** Format: uuid */
id: string;
displayName: string;
initials: string;
} | null;
doneAt: string | null;
};
ChecklistPatch: {
/** @enum {string} */
state: "PENDING" | "DONE" | "NA";
};
RequestsResponse: {
requests: {
/** Format: uuid */
id: string;
reference: string;
description: string;
isPersonTrapped: boolean;
/** @enum {string} */
status: "RECEIVED" | "APPROVED" | "REJECTED";
rejectionReason: string | null;
/** Format: uuid */
assetId: string;
assetReference: string;
siteName: string;
requesterLabel: string;
workOrder: {
/** Format: uuid */
id: string;
reference: string;
/** @enum {string} */
status: "OPEN" | "IN_PROGRESS" | "ON_HOLD" | "DONE" | "CANCELLED";
} | null;
/** Format: date-time */
createdAt: string;
}[];
};
RequestSummary: {
/** Format: uuid */
id: string;
reference: string;
description: string;
isPersonTrapped: boolean;
/** @enum {string} */
status: "RECEIVED" | "APPROVED" | "REJECTED";
rejectionReason: string | null;
/** Format: uuid */
assetId: string;
assetReference: string;
siteName: string;
requesterLabel: string;
workOrder: {
/** Format: uuid */
id: string;
reference: string;
/** @enum {string} */
status: "OPEN" | "IN_PROGRESS" | "ON_HOLD" | "DONE" | "CANCELLED";
} | null;
/** Format: date-time */
createdAt: string;
};
RequestCreate: {
/** Format: uuid */
assetId: string;
description: string;
isPersonTrapped?: boolean;
};
RequestApprove: {
title?: string;
/** @enum {string} */
priority?: "NONE" | "LOW" | "MEDIUM" | "HIGH" | "PERSON_TRAPPED";
assigneeIds?: string[];
/** Format: date-time */
dueDate?: string;
};
RequestReject: {
reason: string;
};
ReferenceValuesResponse: {
referenceValues: {
/** Format: uuid */
id: string;
/** @enum {string} */
field: "DOOR_STATE" | "CABIN_POSITION" | "ANOMALY" | "EXTERNAL_CAUSE" | "ACTION_TAKEN" | "COMPONENT_CONCERNED";
label: string;
isActive: boolean;
usageCount: number;
}[];
};
ReferenceValue: {
/** Format: uuid */
id: string;
/** @enum {string} */
field: "DOOR_STATE" | "CABIN_POSITION" | "ANOMALY" | "EXTERNAL_CAUSE" | "ACTION_TAKEN" | "COMPONENT_CONCERNED";
label: string;
isActive: boolean;
usageCount: number;
};
ReferenceValueCreate: {
/** @enum {string} */
field: "DOOR_STATE" | "CABIN_POSITION" | "ANOMALY" | "EXTERNAL_CAUSE" | "ACTION_TAKEN" | "COMPONENT_CONCERNED";
label: string;
};
ReferenceValueUpdate: {
label?: string;
isActive?: boolean;
};
HealthResponse: {
/** @enum {string} */
status: "ok" | "degraded";
@@ -1461,6 +1947,425 @@ export interface operations {
};
};
};
listWorkOrders: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Liste */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WorkOrdersResponse"];
};
};
};
};
createWorkOrder: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["WorkOrderCreate"];
};
};
responses: {
/** @description Créé */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WorkOrderDetail"];
};
};
};
};
getWorkOrder: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Fiche */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WorkOrderDetail"];
};
};
/** @description Inconnu (ou hors de son périmètre) */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
transitionWorkOrder: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["TransitionRequest"];
};
};
responses: {
/** @description État changé */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WorkOrderDetail"];
};
};
/** @description Transition interdite ou clôture bloquée (garde) */
409: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
commentWorkOrder: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["CommentCreate"];
};
};
responses: {
/** @description Commentaire ajouté */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WorkOrderDetail"];
};
};
};
};
setWorkOrderAssignees: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["AssigneesUpdate"];
};
};
responses: {
/** @description Assignés */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WorkOrderDetail"];
};
};
};
};
upsertWorkOrderReport: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ReportUpsert"];
};
};
responses: {
/** @description Bilan enregistré */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WorkOrderDetail"];
};
};
/** @description Valeur hors référentiel du champ */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
patchChecklistItem: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
itemId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ChecklistPatch"];
};
};
responses: {
/** @description Tâche réglée */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ChecklistItem"];
};
};
/** @description Tâche inconnue */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
listRequests: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Liste */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["RequestsResponse"];
};
};
};
};
createRequest: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["RequestCreate"];
};
};
responses: {
/** @description Demande créée */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["RequestSummary"];
};
};
};
};
approveRequest: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["RequestApprove"];
};
};
responses: {
/** @description OT créé et lié */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WorkOrderDetail"];
};
};
/** @description Demande déjà traitée */
409: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
rejectRequest: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["RequestReject"];
};
};
responses: {
/** @description Rejetée */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["RequestSummary"];
};
};
/** @description Demande déjà traitée */
409: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
listReferenceValues: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Liste */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReferenceValuesResponse"];
};
};
};
};
createReferenceValue: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ReferenceValueCreate"];
};
};
responses: {
/** @description Créée */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReferenceValue"];
};
};
/** @description Libellé déjà présent pour ce champ */
409: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
updateReferenceValue: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ReferenceValueUpdate"];
};
};
responses: {
/** @description Mise à jour */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReferenceValue"];
};
};
/** @description Inconnue */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
getHealth: {
parameters: {
query?: never;