Files
siop2/packages/shared/src/contract.ts
pr-daaif 45ae491827 feat(r5.2): assistant au contrat + suggestion de codes de bilan
apps/ai : /internal/ask — seuil de pertinence, extraits sourcés ou
refus honnête portant la taille du corpus cherché (D2), rédaction via
le Generateur opt-in ; /internal/suggest — similarité sémantique entre
la description libre et les libellés ACTIFS des référentiels, un code
par champ, confiance FORTE/MOYENNE, « N bilans similaires sur ce
parc ». Sans LLM : déterministe, explicable. 23 pytest.

Contrat (74 opérations) : POST /assistant/ask → AssistantAnswer
(EXTRACTIVE/GENERATED/REFUSAL, extraits cités, corpus cherché) et
POST /assistant/suggest-bilan (codes existants seulement) ; clients
web/mobile régénérés.

API NestJS : module assistant — proxy vers siop2-ai (AI_SERVICE_URL/
AI_SERVICE_TOKEN, ADR-004 §4), permissions matrice (ask=view,
suggest=edit), traduction interne→contrat, 503 propre si service
éteint. 6 e2e sur stub HTTP (76 tests API).

Bug débusqué par la vraie chaîne : fastembed ne norme pas ses
vecteurs — la similarité des suggestions dépassait 1 (pgvector
normalisait dans son opérateur, masquant l'écart). Normalisation à
l'encodage + réindexation : bilans en tête (0.41), refus hors corpus,
scores cosinus ≤ 1.

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

1058 lines
34 KiB
TypeScript
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.

import type { z } from 'zod';
import {
AuthResponseSchema,
DemoAccountsResponseSchema,
DemoLoginRequestSchema,
LoginRequestSchema,
} from './schemas/auth';
import { MeResponseSchema } from './schemas/users';
import { HealthResponseSchema } from './schemas/health';
import {
AssetCreateSchema,
AssetComponentCreateSchema,
AssetComponentSchema,
AssetDetailSchema,
AssetOptionsResponseSchema,
AssetsResponseSchema,
AssetUpdateSchema,
CategoriesResponseSchema,
CategoryCreateSchema,
CategorySchema,
CategoryUpdateSchema,
LocationCreateSchema,
LocationSchema,
LocationsResponseSchema,
LocationUpdateSchema,
TeamCreateSchema,
TeamSchema,
TeamsResponseSchema,
TeamUpdateSchema,
} from './schemas/referentiel';
import {
ActivateRequestSchema,
ActivateResponseSchema,
InvitationCreateSchema,
InvitationResponseSchema,
RolesResponseSchema,
UserAdminSchema,
UsersResponseSchema,
UserUpdateSchema,
} from './schemas/users-admin';
import {
PortalAssetSchema,
PortalRequestCreatedSchema,
PortalRequestCreateSchema,
PortalRequestStatusSchema,
} from './schemas/portail';
import {
AnalyticsSummarySchema,
DocumentSchema,
DocumentsResponseSchema,
} from './schemas/documents';
import { SearchResponseSchema } from './schemas/search';
import {
AssistantAnswerSchema,
AssistantAskSchema,
BilanSuggestionsResponseSchema,
SuggestBilanSchema,
} from './schemas/assistant';
import {
ConsumePartSchema,
LaborTimeCreateSchema,
PartCreateSchema,
PartDetailSchema,
PartnerCreateSchema,
PartnerSchema,
PartnersResponseSchema,
PartnerUpdateSchema,
PartsResponseSchema,
PartUpdateSchema,
PurchaseOrderCreateSchema,
PurchaseOrderSchema,
PurchaseOrdersResponseSchema,
PurchaseOrderTransitionSchema,
StockMovementCreateSchema,
} from './schemas/gestion';
import {
MeterReadingCreateSchema,
MetersResponseSchema,
PreventiveGenerateSchema,
PreventiveGenerationResultSchema,
PreventiveStatusSchema,
TaskTemplateCreateSchema,
TaskTemplateSchema,
TaskTemplatesResponseSchema,
TaskTemplateUpdateSchema,
} from './schemas/preventif';
import {
AssigneesUpdateSchema,
ChecklistItemSchema,
ChecklistPatchSchema,
CommentCreateSchema,
ReferenceValueCreateSchema,
ReferenceValueSchema,
ReferenceValuesResponseSchema,
ReferenceValueUpdateSchema,
ReportUpsertSchema,
RequestApproveSchema,
RequestCreateSchema,
RequestRejectSchema,
RequestsResponseSchema,
RequestSummarySchema,
TransitionRequestSchema,
WorkOrderCreateSchema,
WorkOrderDetailSchema,
WorkOrdersResponseSchema,
} from './schemas/exploitation';
/**
* Contrat d'API R0 — source unique de vérité (règle d'or ADR-001).
* `scripts/generate-openapi.ts` en dérive `docs/openapi.json` (committée) ;
* les clients web/mobile sont générés depuis cette spec, dans le même commit.
*/
export interface ApiOperation {
operationId: string;
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
path: string;
summary: string;
tags: string[];
/** Route annotée @Public() côté API (pas de JWT requis). */
isPublic?: boolean;
/** ADR-002 : la route N'EXISTE PAS (404) si DEMO_MODE n'est pas actif. */
demoOnly?: boolean;
/** Paramètres de chemin (`{id}` dans path) — `id`/`…Id` = UUID. */
pathParams?: string[];
/** Paramètres de requête (?a=…&b=…). */
queryParams?: { name: string; required?: boolean; description?: string }[];
/** Upload multipart/form-data : champs déclarés ('file' = binaire). */
multipartFields?: Record<string, 'file' | 'string'>;
/** Réponse 200 binaire (téléchargement streamé). */
binaryResponse?: boolean;
request?: { name: string; schema: z.ZodType };
responses: Record<
number,
{ description: string; name?: string; schema?: z.ZodType }
>;
}
export const API_CONTRACT: ApiOperation[] = [
{
operationId: 'login',
method: 'post',
path: '/auth/login',
summary: 'Connexion par e-mail et mot de passe',
tags: ['auth'],
isPublic: true,
request: { name: 'LoginRequest', schema: LoginRequestSchema },
responses: {
200: { description: 'Jeton émis', name: 'AuthResponse', schema: AuthResponseSchema },
401: { description: 'Identifiants invalides ou compte inactif' },
},
},
{
operationId: 'listDemoAccounts',
method: 'get',
path: '/auth/demo-accounts',
summary: 'Comptes de démonstration (ADR-002 — jamais de secret)',
tags: ['auth', 'demo'],
isPublic: true,
demoOnly: true,
responses: {
200: {
description: 'Comptes isDemo actifs',
name: 'DemoAccountsResponse',
schema: DemoAccountsResponseSchema,
},
},
},
{
operationId: 'demoLogin',
method: 'post',
path: '/auth/demo-login',
summary: 'Connexion 1 clic sur un compte de démonstration (ADR-002)',
tags: ['auth', 'demo'],
isPublic: true,
demoOnly: true,
request: { name: 'DemoLoginRequest', schema: DemoLoginRequestSchema },
responses: {
200: { description: 'Jeton émis', name: 'AuthResponse', schema: AuthResponseSchema },
403: { description: 'Le compte nest pas un compte de démonstration' },
404: { description: 'Compte inconnu' },
},
},
{
operationId: 'getMe',
method: 'get',
path: '/users/me',
summary: 'Profil courant + matrice de permissions du rôle',
tags: ['users'],
responses: {
200: { description: 'Profil', name: 'MeResponse', schema: MeResponseSchema },
401: { description: 'Non authentifié' },
},
},
{
operationId: 'activateAccount',
method: 'post',
path: '/auth/activate',
summary: 'Activer un compte invité (lien 7 jours) — connecte directement',
tags: ['auth'],
isPublic: true,
request: { name: 'ActivateRequest', schema: ActivateRequestSchema },
responses: {
200: { description: 'Compte activé et connecté', name: 'AuthResponse', schema: ActivateResponseSchema },
400: { description: 'Lien invalide ou expiré' },
},
},
// ————— R1 · Référentiel —————
{
operationId: 'listCategories',
method: 'get',
path: '/categories',
summary: 'Référentiels administrables (catégories déquipement, types dorganes)',
tags: ['categories'],
responses: {
200: { description: 'Liste', name: 'CategoriesResponse', schema: CategoriesResponseSchema },
},
},
{
operationId: 'createCategory',
method: 'post',
path: '/categories',
summary: 'Ajouter une catégorie',
tags: ['categories'],
request: { name: 'CategoryCreate', schema: CategoryCreateSchema },
responses: {
201: { description: 'Créée', name: 'Category', schema: CategorySchema },
409: { description: 'Nom déjà utilisé pour ce type' },
},
},
{
operationId: 'updateCategory',
method: 'patch',
path: '/categories/{id}',
summary: 'Renommer ou (dés)activer — jamais de suppression si utilisée',
tags: ['categories'],
pathParams: ['id'],
request: { name: 'CategoryUpdate', schema: CategoryUpdateSchema },
responses: {
200: { description: 'Mise à jour', name: 'Category', schema: CategorySchema },
404: { description: 'Inconnue' },
},
},
{
operationId: 'listLocations',
method: 'get',
path: '/locations',
summary: 'Sites et zones (liste plate, le client construit larbre)',
tags: ['locations'],
responses: {
200: { description: 'Liste', name: 'LocationsResponse', schema: LocationsResponseSchema },
},
},
{
operationId: 'createLocation',
method: 'post',
path: '/locations',
summary: 'Créer un site (sans parent) ou une zone (profondeur max 2)',
tags: ['locations'],
request: { name: 'LocationCreate', schema: LocationCreateSchema },
responses: {
201: { description: 'Créé', name: 'Location', schema: LocationSchema },
400: { description: 'Hiérarchie trop profonde' },
},
},
{
operationId: 'updateLocation',
method: 'patch',
path: '/locations/{id}',
summary: 'Modifier un emplacement',
tags: ['locations'],
pathParams: ['id'],
request: { name: 'LocationUpdate', schema: LocationUpdateSchema },
responses: {
200: { description: 'Mis à jour', name: 'Location', schema: LocationSchema },
404: { description: 'Inconnu' },
},
},
{
operationId: 'listAssets',
method: 'get',
path: '/assets',
summary: 'Inventaire des appareils',
tags: ['assets'],
responses: {
200: { description: 'Liste', name: 'AssetsResponse', schema: AssetsResponseSchema },
},
},
{
operationId: 'listAssetOptions',
method: 'get',
path: '/assets/options',
summary: 'Options dappareils pour le signalement (tout rôle authentifié)',
tags: ['assets'],
responses: {
200: {
description: 'Options',
name: 'AssetOptionsResponse',
schema: AssetOptionsResponseSchema,
},
},
},
{
operationId: 'getAsset',
method: 'get',
path: '/assets/{id}',
summary: 'Fiche appareil (identité + organes)',
tags: ['assets'],
pathParams: ['id'],
responses: {
200: { description: 'Fiche', name: 'AssetDetail', schema: AssetDetailSchema },
404: { description: 'Inconnu' },
},
},
{
operationId: 'createAsset',
method: 'post',
path: '/assets',
summary: 'Créer un appareil (avec ses organes) — le QR découle de la référence',
tags: ['assets'],
request: { name: 'AssetCreate', schema: AssetCreateSchema },
responses: {
201: { description: 'Créé', name: 'AssetDetail', schema: AssetDetailSchema },
409: { description: 'Référence déjà utilisée' },
},
},
{
operationId: 'updateAsset',
method: 'patch',
path: '/assets/{id}',
summary: 'Modifier un appareil (dont son statut déquipement)',
tags: ['assets'],
pathParams: ['id'],
request: { name: 'AssetUpdate', schema: AssetUpdateSchema },
responses: {
200: { description: 'Mis à jour', name: 'AssetDetail', schema: AssetDetailSchema },
404: { description: 'Inconnu' },
},
},
{
operationId: 'addAssetComponent',
method: 'post',
path: '/assets/{id}/components',
summary: 'Ajouter un organe (jamais demplacement propre)',
tags: ['assets'],
pathParams: ['id'],
request: { name: 'AssetComponentCreate', schema: AssetComponentCreateSchema },
responses: {
201: { description: 'Ajouté', name: 'AssetComponent', schema: AssetComponentSchema },
400: { description: 'Le type choisi nest pas un type dorgane' },
},
},
{
operationId: 'removeAssetComponent',
method: 'delete',
path: '/assets/{id}/components/{componentId}',
summary: 'Retirer un organe',
tags: ['assets'],
pathParams: ['id', 'componentId'],
responses: {
204: { description: 'Retiré' },
404: { description: 'Inconnu' },
},
},
{
operationId: 'listTeams',
method: 'get',
path: '/teams',
summary: 'Équipes et leurs membres',
tags: ['teams'],
responses: {
200: { description: 'Liste', name: 'TeamsResponse', schema: TeamsResponseSchema },
},
},
{
operationId: 'createTeam',
method: 'post',
path: '/teams',
summary: 'Créer une équipe',
tags: ['teams'],
request: { name: 'TeamCreate', schema: TeamCreateSchema },
responses: {
201: { description: 'Créée', name: 'Team', schema: TeamSchema },
409: { description: 'Nom déjà utilisé' },
},
},
{
operationId: 'updateTeam',
method: 'patch',
path: '/teams/{id}',
summary: 'Modifier une équipe (nom, description, membres)',
tags: ['teams'],
pathParams: ['id'],
request: { name: 'TeamUpdate', schema: TeamUpdateSchema },
responses: {
200: { description: 'Mise à jour', name: 'Team', schema: TeamSchema },
404: { description: 'Inconnue' },
},
},
{
operationId: 'listUsers',
method: 'get',
path: '/users',
summary: 'Personnes (statut dérivé : actif / invité / désactivé)',
tags: ['users'],
responses: {
200: { description: 'Liste', name: 'UsersResponse', schema: UsersResponseSchema },
},
},
{
operationId: 'listRoles',
method: 'get',
path: '/roles',
summary: 'Les 7 rôles (pour linvitation)',
tags: ['users'],
responses: {
200: { description: 'Liste', name: 'RolesResponse', schema: RolesResponseSchema },
},
},
{
operationId: 'inviteUser',
method: 'post',
path: '/users/invitations',
summary: 'Inviter — crée le compte inactif et émet le lien dactivation (7 j)',
tags: ['users'],
request: { name: 'InvitationCreate', schema: InvitationCreateSchema },
responses: {
201: { description: 'Invitation émise', name: 'InvitationResponse', schema: InvitationResponseSchema },
409: { description: 'Email déjà utilisé' },
},
},
{
operationId: 'resendInvitation',
method: 'post',
path: '/users/{id}/invitation',
summary: 'Régénérer le lien dactivation dun compte non activé',
tags: ['users'],
pathParams: ['id'],
responses: {
201: { description: 'Nouveau lien', name: 'InvitationResponse', schema: InvitationResponseSchema },
409: { description: 'Compte déjà activé' },
},
},
{
operationId: 'updateUser',
method: 'patch',
path: '/users/{id}',
summary: 'Modifier une personne (rôle, équipes, activation du compte)',
tags: ['users'],
pathParams: ['id'],
request: { name: 'UserUpdate', schema: UserUpdateSchema },
responses: {
200: { description: 'Mise à jour', name: 'UserAdmin', schema: UserAdminSchema },
404: { description: 'Inconnue' },
},
},
// ————— R3 · Bibliothèque & analytics —————
{
operationId: 'listDocuments',
method: 'get',
path: '/documents',
summary: 'Bibliothèque (filtrable par appareil ou OT)',
tags: ['documents'],
queryParams: [{ name: 'assetId' }, { name: 'workOrderId' }, { name: 'kind' }],
responses: {
200: { description: 'Liste', name: 'DocumentsResponse', schema: DocumentsResponseSchema },
},
},
{
operationId: 'uploadDocument',
method: 'post',
path: '/documents',
summary: 'Téléverser (PDF/JPG/PNG, 20 Mo max, rattachement appareil OU OT requis)',
tags: ['documents'],
multipartFields: { file: 'file', kind: 'string', assetId: 'string', workOrderId: 'string' },
responses: {
201: { description: 'Document rangé', name: 'Document', schema: DocumentSchema },
400: { description: 'Type/taille refusé ou rattachement manquant' },
},
},
{
operationId: 'downloadDocument',
method: 'get',
path: '/documents/{id}/download',
summary: 'Télécharger — streamé par lAPI (MinIO jamais exposé)',
tags: ['documents'],
pathParams: ['id'],
binaryResponse: true,
responses: {
200: { description: 'Fichier' },
404: { description: 'Inconnu' },
},
},
{
operationId: 'deleteDocument',
method: 'delete',
path: '/documents/{id}',
summary: 'Supprimer (permission dédition sur la cible)',
tags: ['documents'],
pathParams: ['id'],
responses: {
204: { description: 'Supprimé' },
404: { description: 'Inconnu' },
},
},
{
operationId: 'getAnalyticsSummary',
method: 'get',
path: '/analytics/summary',
summary: 'Le tableau de la direction : coûts, pannes par organe (bilans), préventif, top équipements',
tags: ['analytics'],
queryParams: [{ name: 'months', description: 'Période en mois : 3, 6 ou 12 (défaut)' }],
responses: {
200: { description: 'Synthèse', name: 'AnalyticsSummary', schema: AnalyticsSummarySchema },
},
},
{
operationId: 'askAssistant',
method: 'post',
path: '/assistant/ask',
summary: 'Assistant R5 — sourcé ou silencieux : extraits cités ou refus honnête (D2)',
tags: ['assistant'],
request: { name: 'AssistantAsk', schema: AssistantAskSchema },
responses: {
200: { description: 'Réponse sourcée ou refus', name: 'AssistantAnswer', schema: AssistantAnswerSchema },
503: { description: 'Service IA indisponible' },
},
},
{
operationId: 'suggestBilan',
method: 'post',
path: '/assistant/suggest-bilan',
summary: 'Suggérer des codes de bilan depuis une description libre (D1 — lhumain valide)',
tags: ['assistant'],
request: { name: 'SuggestBilan', schema: SuggestBilanSchema },
responses: {
200: { description: 'Suggestions (codes existants seulement)', name: 'BilanSuggestions', schema: BilanSuggestionsResponseSchema },
503: { description: 'Service IA indisponible' },
},
},
{
operationId: 'globalSearch',
method: 'get',
path: '/search',
summary: 'Recherche globale (OT, ascenseurs, sites) — chaque famille filtrée par la matrice',
tags: ['search'],
queryParams: [{ name: 'q', required: true, description: '2 caractères minimum' }],
responses: {
200: { description: 'Résultats (5 max par famille)', name: 'SearchResponse', schema: SearchResponseSchema },
},
},
// ————— R3 · Gestion (stock, achats, tiers, coûts) —————
{
operationId: 'listPartners',
method: 'get',
path: '/partners',
summary: 'Tiers (fournisseurs, syndics) — permission PURCHASE_ORDERS',
tags: ['partners'],
responses: {
200: { description: 'Liste', name: 'PartnersResponse', schema: PartnersResponseSchema },
},
},
{
operationId: 'createPartner',
method: 'post',
path: '/partners',
summary: 'Créer un tiers',
tags: ['partners'],
request: { name: 'PartnerCreate', schema: PartnerCreateSchema },
responses: {
201: { description: 'Créé', name: 'Partner', schema: PartnerSchema },
409: { description: 'Nom déjà utilisé' },
},
},
{
operationId: 'updatePartner',
method: 'patch',
path: '/partners/{id}',
summary: 'Modifier / (dés)activer un tiers',
tags: ['partners'],
pathParams: ['id'],
request: { name: 'PartnerUpdate', schema: PartnerUpdateSchema },
responses: {
200: { description: 'Mis à jour', name: 'Partner', schema: PartnerSchema },
404: { description: 'Inconnu' },
},
},
{
operationId: 'listParts',
method: 'get',
path: '/parts',
summary: 'Stock de pièces — quantités DÉRIVÉES des mouvements',
tags: ['parts'],
responses: {
200: { description: 'Liste', name: 'PartsResponse', schema: PartsResponseSchema },
},
},
{
operationId: 'getPart',
method: 'get',
path: '/parts/{id}',
summary: 'Fiche pièce : les mouvements SONT le stock',
tags: ['parts'],
pathParams: ['id'],
responses: {
200: { description: 'Fiche', name: 'PartDetail', schema: PartDetailSchema },
404: { description: 'Inconnue' },
},
},
{
operationId: 'createPart',
method: 'post',
path: '/parts',
summary: 'Créer une pièce (référence P-#### générée)',
tags: ['parts'],
request: { name: 'PartCreate', schema: PartCreateSchema },
responses: {
201: { description: 'Créée', name: 'PartDetail', schema: PartDetailSchema },
},
},
{
operationId: 'updatePart',
method: 'patch',
path: '/parts/{id}',
summary: 'Modifier une pièce (désignation, seuil, fournisseur…)',
tags: ['parts'],
pathParams: ['id'],
request: { name: 'PartUpdate', schema: PartUpdateSchema },
responses: {
200: { description: 'Mise à jour', name: 'PartDetail', schema: PartDetailSchema },
404: { description: 'Inconnue' },
},
},
{
operationId: 'addStockMovement',
method: 'post',
path: '/parts/{id}/movements',
summary: 'Entrée manuelle (+) ou ajustement (± motif REQUIS) — jamais de saisie de stock',
tags: ['parts'],
pathParams: ['id'],
request: { name: 'StockMovementCreate', schema: StockMovementCreateSchema },
responses: {
201: { description: 'Mouvement tracé', name: 'PartDetail', schema: PartDetailSchema },
409: { description: 'Le stock ne peut pas devenir négatif' },
},
},
{
operationId: 'listPurchaseOrders',
method: 'get',
path: '/purchase-orders',
summary: 'Bons de commande',
tags: ['purchase-orders'],
responses: {
200: {
description: 'Liste',
name: 'PurchaseOrdersResponse',
schema: PurchaseOrdersResponseSchema,
},
},
},
{
operationId: 'getPurchaseOrder',
method: 'get',
path: '/purchase-orders/{id}',
summary: 'Fiche BC (lignes, total)',
tags: ['purchase-orders'],
pathParams: ['id'],
responses: {
200: { description: 'Fiche', name: 'PurchaseOrder', schema: PurchaseOrderSchema },
404: { description: 'Inconnu' },
},
},
{
operationId: 'createPurchaseOrder',
method: 'post',
path: '/purchase-orders',
summary: 'Créer un BC (brouillon)',
tags: ['purchase-orders'],
request: { name: 'PurchaseOrderCreate', schema: PurchaseOrderCreateSchema },
responses: {
201: { description: 'Créé', name: 'PurchaseOrder', schema: PurchaseOrderSchema },
400: { description: 'Fournisseur ou pièce invalide' },
},
},
{
operationId: 'transitionPurchaseOrder',
method: 'post',
path: '/purchase-orders/{id}/transition',
summary: 'Envoyer / réceptionner (→ entrées de stock, PU figés) / annuler',
tags: ['purchase-orders'],
pathParams: ['id'],
request: { name: 'PurchaseOrderTransition', schema: PurchaseOrderTransitionSchema },
responses: {
200: { description: 'État changé', name: 'PurchaseOrder', schema: PurchaseOrderSchema },
409: { description: 'Transition interdite' },
},
},
{
operationId: 'consumePart',
method: 'post',
path: '/work-orders/{id}/consume-part',
summary: 'Consommer une pièce sur lOT — stock décrémenté, PRIX FIGÉ',
tags: ['work-orders'],
pathParams: ['id'],
request: { name: 'ConsumePart', schema: ConsumePartSchema },
responses: {
201: { description: 'Consommée', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
409: { description: 'Stock insuffisant ou prix inconnu' },
},
},
{
operationId: 'addLaborTime',
method: 'post',
path: '/work-orders/{id}/labor',
summary: 'Saisir de la main-dœuvre — TAUX FIGÉ à la saisie',
tags: ['work-orders'],
pathParams: ['id'],
request: { name: 'LaborTimeCreate', schema: LaborTimeCreateSchema },
responses: {
201: { description: 'Saisie', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
409: { description: 'Taux horaire non défini pour cette personne' },
},
},
// ————— R2 · Portail public (QR cabine — aucun compte) —————
{
operationId: 'getPortalAsset',
method: 'get',
path: '/portal/assets/{reference}',
summary: 'Résoudre le QR scanné (référence → appareil, préremplissage)',
tags: ['portal'],
isPublic: true,
pathParams: ['reference'],
responses: {
200: { description: 'Appareil', name: 'PortalAsset', schema: PortalAssetSchema },
404: { description: 'Référence inconnue' },
},
},
{
operationId: 'createPortalRequest',
method: 'post',
path: '/portal/requests',
summary: 'Signaler sans compte — renvoie le jeton de suivi (à garder sur le téléphone)',
tags: ['portal'],
isPublic: true,
request: { name: 'PortalRequestCreate', schema: PortalRequestCreateSchema },
responses: {
201: {
description: 'Signalement enregistré',
name: 'PortalRequestCreated',
schema: PortalRequestCreatedSchema,
},
404: { description: 'Référence inconnue' },
429: { description: 'Trop de signalements — réessayez dans une minute' },
},
},
{
operationId: 'getPortalRequestStatus',
method: 'get',
path: '/portal/requests/{reference}/{token}',
summary: 'Suivi sans jargon (Reçu → Intervention → Résolu) — jeton requis',
tags: ['portal'],
isPublic: true,
pathParams: ['reference', 'token'],
responses: {
200: { description: 'Avancement', name: 'PortalRequestStatus', schema: PortalRequestStatusSchema },
404: { description: 'Signalement inconnu ou jeton invalide' },
},
},
// ————— R2 · Préventif & compteurs —————
{
operationId: 'listTaskTemplates',
method: 'get',
path: '/preventive/templates',
summary: 'Gabarits de tâches du préventif (périodicité en mois)',
tags: ['preventive'],
responses: {
200: { description: 'Liste', name: 'TaskTemplatesResponse', schema: TaskTemplatesResponseSchema },
},
},
{
operationId: 'createTaskTemplate',
method: 'post',
path: '/preventive/templates',
summary: 'Ajouter un gabarit',
tags: ['preventive'],
request: { name: 'TaskTemplateCreate', schema: TaskTemplateCreateSchema },
responses: {
201: { description: 'Créé', name: 'TaskTemplate', schema: TaskTemplateSchema },
409: { description: 'Libellé déjà utilisé' },
},
},
{
operationId: 'updateTaskTemplate',
method: 'patch',
path: '/preventive/templates/{id}',
summary: 'Modifier / (dés)activer un gabarit — jamais de suppression si utilisé',
tags: ['preventive'],
pathParams: ['id'],
request: { name: 'TaskTemplateUpdate', schema: TaskTemplateUpdateSchema },
responses: {
200: { description: 'Mis à jour', name: 'TaskTemplate', schema: TaskTemplateSchema },
404: { description: 'Inconnu' },
},
},
{
operationId: 'generatePreventive',
method: 'post',
path: '/preventive/generate',
summary: 'Générer la grille du mois — IDEMPOTENT (regénérer ne double rien)',
tags: ['preventive'],
request: { name: 'PreventiveGenerate', schema: PreventiveGenerateSchema },
responses: {
200: {
description: 'Résumé de génération',
name: 'PreventiveGenerationResult',
schema: PreventiveGenerationResultSchema,
},
},
},
{
operationId: 'getPreventiveStatus',
method: 'get',
path: '/preventive/status',
summary: 'État du mois courant (générées, terminées, en retard, premiers contrôles)',
tags: ['preventive'],
responses: {
200: { description: 'État', name: 'PreventiveStatus', schema: PreventiveStatusSchema },
},
},
{
operationId: 'getAssetMeters',
method: 'get',
path: '/assets/{id}/meters',
summary: 'Compteurs de lappareil (relevés récents dabord)',
tags: ['meters'],
pathParams: ['id'],
responses: {
200: { description: 'Compteurs', name: 'MetersResponse', schema: MetersResponseSchema },
404: { description: 'Appareil inconnu' },
},
},
{
operationId: 'addMeterReading',
method: 'post',
path: '/assets/{id}/meter-readings',
summary: 'Enregistrer un relevé — strictement croissant',
tags: ['meters'],
pathParams: ['id'],
request: { name: 'MeterReadingCreate', schema: MeterReadingCreateSchema },
responses: {
201: { description: 'Relevé enregistré', name: 'MetersResponse', schema: MetersResponseSchema },
400: { description: 'Relevé inférieur ou égal au précédent' },
},
},
// ————— R2 · Exploitation —————
{
operationId: 'listWorkOrders',
method: 'get',
path: '/work-orders',
summary: 'Ordres de travail (sans « voir autre » : seulement les siens)',
tags: ['work-orders'],
responses: {
200: { description: 'Liste', name: 'WorkOrdersResponse', schema: WorkOrdersResponseSchema },
},
},
{
operationId: 'getWorkOrder',
method: 'get',
path: '/work-orders/{id}',
summary: 'Fiche OT (activité, checklist, bilan, transitions autorisées, blocages de clôture)',
tags: ['work-orders'],
pathParams: ['id'],
responses: {
200: { description: 'Fiche', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
404: { description: 'Inconnu (ou hors de son périmètre)' },
},
},
{
operationId: 'createWorkOrder',
method: 'post',
path: '/work-orders',
summary: 'Créer un OT (statut Ouvert)',
tags: ['work-orders'],
request: { name: 'WorkOrderCreate', schema: WorkOrderCreateSchema },
responses: {
201: { description: 'Créé', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
},
},
{
operationId: 'transitionWorkOrder',
method: 'post',
path: '/work-orders/{id}/transition',
summary: 'Changer létat (machine à états stricte ; DONE exige bilan + checklist)',
tags: ['work-orders'],
pathParams: ['id'],
request: { name: 'TransitionRequest', schema: TransitionRequestSchema },
responses: {
200: { description: 'État changé', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
409: { description: 'Transition interdite ou clôture bloquée (garde)' },
},
},
{
operationId: 'commentWorkOrder',
method: 'post',
path: '/work-orders/{id}/comments',
summary: 'Commenter (activité chronologique)',
tags: ['work-orders'],
pathParams: ['id'],
request: { name: 'CommentCreate', schema: CommentCreateSchema },
responses: {
201: { description: 'Commentaire ajouté', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
},
},
{
operationId: 'setWorkOrderAssignees',
method: 'put',
path: '/work-orders/{id}/assignees',
summary: 'Assigner (remplace la liste)',
tags: ['work-orders'],
pathParams: ['id'],
request: { name: 'AssigneesUpdate', schema: AssigneesUpdateSchema },
responses: {
200: { description: 'Assignés', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
},
},
{
operationId: 'upsertWorkOrderReport',
method: 'put',
path: '/work-orders/{id}/report',
summary: 'Renseigner le bilan codé (null efface un champ)',
tags: ['work-orders'],
pathParams: ['id'],
request: { name: 'ReportUpsert', schema: ReportUpsertSchema },
responses: {
200: { description: 'Bilan enregistré', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
400: { description: 'Valeur hors référentiel du champ' },
},
},
{
operationId: 'patchChecklistItem',
method: 'patch',
path: '/work-orders/{id}/checklist/{itemId}',
summary: 'Régler une tâche de la grille (Fait / N-A / à faire)',
tags: ['work-orders'],
pathParams: ['id', 'itemId'],
request: { name: 'ChecklistPatch', schema: ChecklistPatchSchema },
responses: {
200: { description: 'Tâche réglée', name: 'ChecklistItem', schema: ChecklistItemSchema },
404: { description: 'Tâche inconnue' },
},
},
{
operationId: 'listRequests',
method: 'get',
path: '/requests',
summary: 'Demandes (sans « voir autre » : seulement les siennes)',
tags: ['requests'],
responses: {
200: { description: 'Liste', name: 'RequestsResponse', schema: RequestsResponseSchema },
},
},
{
operationId: 'createRequest',
method: 'post',
path: '/requests',
summary: 'Signaler (interne — le portail public QR arrive en R2.4)',
tags: ['requests'],
request: { name: 'RequestCreate', schema: RequestCreateSchema },
responses: {
201: { description: 'Demande créée', name: 'RequestSummary', schema: RequestSummarySchema },
},
},
{
operationId: 'approveRequest',
method: 'post',
path: '/requests/{id}/approve',
summary: 'Approuver → crée lOT lié (1-1, jamais de doublon)',
tags: ['requests'],
pathParams: ['id'],
request: { name: 'RequestApprove', schema: RequestApproveSchema },
responses: {
201: { description: 'OT créé et lié', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema },
409: { description: 'Demande déjà traitée' },
},
},
{
operationId: 'rejectRequest',
method: 'post',
path: '/requests/{id}/reject',
summary: 'Rejeter — motif obligatoire, lisible côté demandeur',
tags: ['requests'],
pathParams: ['id'],
request: { name: 'RequestReject', schema: RequestRejectSchema },
responses: {
200: { description: 'Rejetée', name: 'RequestSummary', schema: RequestSummarySchema },
409: { description: 'Demande déjà traitée' },
},
},
{
operationId: 'listReferenceValues',
method: 'get',
path: '/reference-values',
summary: 'Référentiels du bilan codé (6 champs)',
tags: ['reference-values'],
responses: {
200: {
description: 'Liste',
name: 'ReferenceValuesResponse',
schema: ReferenceValuesResponseSchema,
},
},
},
{
operationId: 'createReferenceValue',
method: 'post',
path: '/reference-values',
summary: 'Ajouter une valeur de référentiel',
tags: ['reference-values'],
request: { name: 'ReferenceValueCreate', schema: ReferenceValueCreateSchema },
responses: {
201: { description: 'Créée', name: 'ReferenceValue', schema: ReferenceValueSchema },
409: { description: 'Libellé déjà présent pour ce champ' },
},
},
{
operationId: 'updateReferenceValue',
method: 'patch',
path: '/reference-values/{id}',
summary: 'Renommer ou (dés)activer — jamais de suppression si utilisée',
tags: ['reference-values'],
pathParams: ['id'],
request: { name: 'ReferenceValueUpdate', schema: ReferenceValueUpdateSchema },
responses: {
200: { description: 'Mise à jour', name: 'ReferenceValue', schema: ReferenceValueSchema },
404: { description: 'Inconnue' },
},
},
{
operationId: 'getHealth',
method: 'get',
path: '/health',
summary: 'État des dépendances (base, Redis, stockage)',
tags: ['health'],
isPublic: true,
responses: {
200: {
description: 'État agrégé',
name: 'HealthResponse',
schema: HealthResponseSchema,
},
},
},
];