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>
This commit is contained in:
pr-daaif
2026-07-16 15:15:57 +01:00
parent f7702e4252
commit 902efa6192
17 changed files with 1868 additions and 1 deletions

View File

@@ -930,6 +930,271 @@
}
}
},
"/preventive/templates": {
"get": {
"operationId": "listTaskTemplates",
"summary": "Gabarits de tâches du préventif (périodicité en mois)",
"tags": [
"preventive"
],
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Liste",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskTemplatesResponse"
}
}
}
}
}
},
"post": {
"operationId": "createTaskTemplate",
"summary": "Ajouter un gabarit",
"tags": [
"preventive"
],
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskTemplateCreate"
}
}
}
},
"responses": {
"201": {
"description": "Créé",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskTemplate"
}
}
}
},
"409": {
"description": "Libellé déjà utilisé"
}
}
}
},
"/preventive/templates/{id}": {
"patch": {
"operationId": "updateTaskTemplate",
"summary": "Modifier / (dés)activer un gabarit — jamais de suppression si utilisé",
"tags": [
"preventive"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskTemplateUpdate"
}
}
}
},
"responses": {
"200": {
"description": "Mis à jour",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskTemplate"
}
}
}
},
"404": {
"description": "Inconnu"
}
}
}
},
"/preventive/generate": {
"post": {
"operationId": "generatePreventive",
"summary": "Générer la grille du mois — IDEMPOTENT (regénérer ne double rien)",
"tags": [
"preventive"
],
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PreventiveGenerate"
}
}
}
},
"responses": {
"200": {
"description": "Résumé de génération",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PreventiveGenerationResult"
}
}
}
}
}
}
},
"/preventive/status": {
"get": {
"operationId": "getPreventiveStatus",
"summary": "État du mois courant (générées, terminées, en retard, premiers contrôles)",
"tags": [
"preventive"
],
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "État",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PreventiveStatus"
}
}
}
}
}
}
},
"/assets/{id}/meters": {
"get": {
"operationId": "getAssetMeters",
"summary": "Compteurs de lappareil (relevés récents dabord)",
"tags": [
"meters"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Compteurs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MetersResponse"
}
}
}
},
"404": {
"description": "Appareil inconnu"
}
}
}
},
"/assets/{id}/meter-readings": {
"post": {
"operationId": "addMeterReading",
"summary": "Enregistrer un relevé — strictement croissant",
"tags": [
"meters"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MeterReadingCreate"
}
}
}
},
"responses": {
"201": {
"description": "Relevé enregistré",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MetersResponse"
}
}
}
},
"400": {
"description": "Relevé inférieur ou égal au précédent"
}
}
}
},
"/work-orders": {
"get": {
"operationId": "listWorkOrders",
@@ -3308,6 +3573,396 @@
},
"additionalProperties": false
},
"TaskTemplatesResponse": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"templates": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"label": {
"type": "string"
},
"periodMonths": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"isRegulatory": {
"type": "boolean"
},
"isActive": {
"type": "boolean"
},
"componentTypeId": {
"anyOf": [
{
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
{
"type": "null"
}
]
},
"componentTypeName": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"label",
"periodMonths",
"isRegulatory",
"isActive",
"componentTypeId",
"componentTypeName"
],
"additionalProperties": false
}
}
},
"required": [
"templates"
],
"additionalProperties": false
},
"TaskTemplate": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"label": {
"type": "string"
},
"periodMonths": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"isRegulatory": {
"type": "boolean"
},
"isActive": {
"type": "boolean"
},
"componentTypeId": {
"anyOf": [
{
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
{
"type": "null"
}
]
},
"componentTypeName": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"label",
"periodMonths",
"isRegulatory",
"isActive",
"componentTypeId",
"componentTypeName"
],
"additionalProperties": false
},
"TaskTemplateCreate": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"periodMonths": {
"type": "integer",
"minimum": 1,
"maximum": 60
},
"componentTypeId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"isRegulatory": {
"type": "boolean"
}
},
"required": [
"label",
"periodMonths"
],
"additionalProperties": false
},
"TaskTemplateUpdate": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"periodMonths": {
"type": "integer",
"minimum": 1,
"maximum": 60
},
"componentTypeId": {
"anyOf": [
{
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
{
"type": "null"
}
]
},
"isRegulatory": {
"type": "boolean"
},
"isActive": {
"type": "boolean"
}
},
"additionalProperties": false
},
"PreventiveGenerationResult": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"month": {
"type": "string"
},
"created": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"skipped": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"firstControls": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"references": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"month",
"created",
"skipped",
"firstControls",
"references"
],
"additionalProperties": false
},
"PreventiveGenerate": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"month": {
"type": "string",
"pattern": "^\\d{4}-(0[1-9]|1[0-2])$"
}
},
"additionalProperties": false
},
"PreventiveStatus": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"month": {
"type": "string"
},
"assetsUnderContract": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"generated": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"done": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"late": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"firstControls": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
}
},
"required": [
"month",
"assetsUnderContract",
"generated",
"done",
"late",
"firstControls"
],
"additionalProperties": false
},
"MetersResponse": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"meters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": [
"RUNNING_HOURS",
"STARTS"
]
},
"readings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"value": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"readBy": {
"anyOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"displayName": {
"type": "string"
}
},
"required": [
"id",
"displayName"
],
"additionalProperties": false
},
{
"type": "null"
}
]
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
}
},
"required": [
"id",
"value",
"readBy",
"createdAt"
],
"additionalProperties": false
}
}
},
"required": [
"kind",
"readings"
],
"additionalProperties": false
}
}
},
"required": [
"meters"
],
"additionalProperties": false
},
"MeterReadingCreate": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": [
"RUNNING_HOURS",
"STARTS"
]
},
"value": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
},
"required": [
"kind",
"value"
],
"additionalProperties": false
},
"WorkOrdersResponse": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",