openapi: 3.1.0
info:
  title: BrokerOne — API Partenaires
  version: 0.1.0-preview
  description: |
    API d'intégration des **agents IA conformes** de BrokerOne pour les éditeurs CRM,
    plateformes courtier et insurtech. **CRM-agnostique**, **multi-tenant strict**,
    **propose-décide** (aucun envoi client automatique), conforme FSMA / ACPR / IDD / DDA /
    AML / RGPD by design.

    ## Authentification

    Chaque cabinet (tenant) reçoit une clé API opaque `bo_live_…`. Toute requête porte&nbsp;:

    ```
    Authorization: Bearer bo_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ```

    - La clé n'est affichée **qu'une seule fois** à sa génération (seul son hash est stocké côté serveur).
    - Le `broker_id` / `courtier` fourni **doit** correspondre au cabinet de la clé, sinon **403**.
    - L'accès par service est **modélisé par les scopes** de la clé (une clé sans le scope `checkup`
      reçoit **403** sur les routes Checkup).

    ## Démarrage rapide (sandbox)

    Les capacités **« sans état »** (vous fournissez la donnée) se testent immédiatement avec une clé
    sandbox scopée, **sans pré-alimentation**. Exemple — audit AML d'un contrat&nbsp;:

    ```bash
    # export BROKERONE_API_KEY="bo_live_…"   (votre clé sandbox)
    curl -X POST https://api-dev.brokerone.insure/compliance/api/v1/compliance/aml/audit-contrat \
      -H "Authorization: Bearer $BROKERONE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
            "broker_id": "assurfinance",
            "contract": {
              "ref_dossier": "DEMO-001", "produit": "vie_ibip",
              "prime_unique": 250000, "date_effet": "2025-06-01",
              "pays_residence": "BE", "ppe": true
            }
          }'
    ```

    Réponse (extrait réel)&nbsp;:

    ```json
    {
      "ok": true,
      "audit": {
        "niveauPropose": 3, "risqueClient": "eleve", "niveauVigilance": "accrue",
        "clignotants": ["cl-S2-01", "cl-S2-09"],
        "clignotantsDetail": [
          { "id": "cl-S2-09", "libelle": "Personne politiquement exposée (PPE)",
            "tier": "accrue", "evidence": "indicateur PPE présent dans la fiche client" }
        ],
        "regle2sur3": { "declenche": true },
        "ctif": false, "embargo": false,
        "rapportMarkdown": "# Rapport AML intermédiaire — contrat DEMO-001 …"
      }
    }
    ```

    **Capacités testables à froid** (vous fournissez le payload)&nbsp;: audit AML
    (`/compliance/.../aml/audit-contrat`, `/aml/audit-portfolio`, `/aml/batch-audit`) et qualification
    sinistre (`/claims/.../qualify`). Les routes de **lecture** (bilans, dossiers, détections) nécessitent
    un **cabinet de test pré-alimenté** — fourni avec votre accès sandbox.

    ## Conventions

    - Tous les corps sont en `application/json` (sauf upload de pièces&nbsp;: `multipart/form-data`).
    - `Accept-Language: fr | nl` choisit la langue des livrables générés.
    - **Propose-décide**&nbsp;: les capacités produisent des **brouillons/propositions** ; aucune action
      vers le client n'est déclenchée automatiquement — la validation reste un acte du courtier.

    > **Statut v0.1 (preview, DEV)** — base URL DEV, schémas susceptibles d'évoluer avant publication.
    > La référence est publique ; l'accès bac-à-sable et les clés sont délivrés aux partenaires sous accord.
  contact:
    name: BrokerOne — Partenaires intégrateurs
    url: https://brokerone.insure/partners.html
servers:
  - url: https://api-dev.brokerone.insure
    description: Passerelle DEV (preview)
security:
  - bearerAuth: []
tags:
  - name: Compliance — AML
    description: "Audit AML / i-AMLCO (LCB-FT). Scope requis&nbsp;: `compliance`."
  - name: Checkup — Revue 360
    description: "Revue de portefeuille, bilans, lacunes & opportunités. Scope&nbsp;: `checkup`."
  - name: Augmented — Devoir de conseil
    description: "Dossiers de conseil IDD / DDA. Scope&nbsp;: `augmented`."
  - name: Transcript — Analyse d'appels
    description: "Analyse de transcription d'appels (alertes, recommandations, actes). Scope&nbsp;: `transcript`."
  - name: Claims — Sinistres
    description: "Qualification & gestion de dossiers sinistre. Scope&nbsp;: `claims`."
  - name: Sentinel — Détections conformité
    description: "Réclamations & droits RGPD détectés sur les emails entrants. Scope&nbsp;: `sentinel`."

paths:
  # ───────────────────────── COMPLIANCE / AML ─────────────────────────
  /compliance/api/v1/compliance/aml/batch-audit:
    post:
      tags: [Compliance — AML]
      summary: Audit AML rétroactif par lot
      description: >
        Audite un portefeuille de contrats **vie/IBIP** pour un exercice (criblage AML déterministe).
        Par défaut, dépose des brouillons « À valider » côté courtier (`deposit:true`). **Aucun registre
        n'est écrit** : la validation reste un acte humain (propose-décide). Un niveau 4 (CTIF/embargo)
        n'est jamais déposé en lot.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [broker_id, contracts]
              properties:
                broker_id: { type: string, description: Slug ou UUID du cabinet (== cabinet de la clé). }
                annee: { type: integer, description: "Exercice audité (défaut : année courante)." }
                contracts:
                  type: array
                  description: Portefeuille fourni par l'appelant (CRM-agnostique).
                  items: { $ref: '#/components/schemas/AmlContract' }
                deposit: { type: boolean, default: true, description: "true = dépose les brouillons ; false = dry-run." }
                include_cancelled: { type: boolean, default: false }
      responses:
        '200':
          description: Audit effectué.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  capability: { type: string, example: aml_batch_audit }
                  exercice: { type: integer }
                  broker_id: { type: string }
                  portfolio:
                    type: object
                    properties:
                      received: { type: integer }
                      audited: { type: integer }
                      skipped: { type: integer }
                      skipped_by_reason: { type: object, additionalProperties: { type: integer } }
                  deposit:
                    type: object
                    properties:
                      applied: { type: boolean }
                      deposited: { type: integer }
                      escalated_skipped: { type: integer, description: Niveau 4 jamais déposé en lot (escalade confidentielle). }
                  audits:
                    type: array
                    items: { $ref: '#/components/schemas/AmlAuditResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /compliance/api/v1/compliance/aml/audit-contrat:
    post:
      tags: [Compliance — AML]
      summary: Audit AML d'un contrat
      description: Analyse AML déterministe d'un seul contrat (données CRM structurées). **Pure** — aucun registre écrit.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [broker_id, contract]
              properties:
                broker_id: { type: string }
                annee: { type: integer }
                contract: { $ref: '#/components/schemas/AmlContract' }
      responses:
        '200':
          description: Audit du contrat.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  audit: { $ref: '#/components/schemas/AmlAuditResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /compliance/api/v1/compliance/aml/audit-portfolio:
    post:
      tags: [Compliance — AML]
      summary: Audit AML d'un portefeuille (sans dépôt)
      description: Comme `batch-audit` mais **dry-run par défaut** (analyse sans dépôt de brouillons). Filtre vie/IBIP + exercice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [broker_id, contracts]
              properties:
                broker_id: { type: string }
                annee: { type: integer }
                contracts: { type: array, items: { $ref: '#/components/schemas/AmlContract' } }
                include_cancelled: { type: boolean, default: false }
                deposit: { type: boolean, default: false }
      responses:
        '200':
          description: Audit du portefeuille.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  total_contracts: { type: integer }
                  audited_count: { type: integer }
                  skipped_count: { type: integer }
                  audits: { type: array, items: { $ref: '#/components/schemas/AmlAuditResult' } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ───────────────────────── CHECKUP / REVUE 360 ─────────────────────────
  /checkup/api/v1/checkup:
    post:
      tags: [Checkup — Revue 360]
      summary: Générer un bilan 360°
      description: Lance une revue 360° à partir de données CRM (texte) ou d'un PDF. Renvoie le score de couverture, le profil et les lacunes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [client_id, client_name]
              properties:
                courtier: { type: string, description: Slug du cabinet (== cabinet de la clé). }
                client_id: { type: string }
                client_name: { type: string }
                crm_text: { type: string, description: Données CRM structurées (texte). Requis si pas de PDF. }
                pdf_base64: { type: string, description: Alternative — document PDF encodé base64. }
                client_email: { type: string }
                langue: { type: string, enum: [fr, nl] }
      responses:
        '201':
          description: Bilan généré.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ref: { type: string, example: ICU-BILAN-assurfinance-v1 }
                  score_couverture: { type: integer }
                  profil: { type: string }
                  contrats: { type: integer }
                  lacunes_critiques: { type: integer }
                  html_file: { type: string }
                  docx_file: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /checkup/api/v1/checkup/list:
    get:
      tags: [Checkup — Revue 360]
      summary: Lister les bilans
      parameters:
        - { name: courtier, in: query, required: true, schema: { type: string } }
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: per_page, in: query, schema: { type: integer, default: 30 } }
        - { name: profil, in: query, schema: { type: string } }
        - { name: score_min, in: query, schema: { type: integer } }
        - { name: has_lacune_critique, in: query, schema: { type: boolean } }
        - { name: q, in: query, description: Recherche par nom client., schema: { type: string } }
      responses:
        '200':
          description: Liste paginée.
          content:
            application/json:
              schema:
                type: object
                properties:
                  bilans: { type: array, items: { $ref: '#/components/schemas/CheckupBilanListItem' } }
                  meta:
                    type: object
                    properties:
                      total: { type: integer }
                      page: { type: integer }
                      per_page: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /checkup/api/v1/checkup/{ref}/detail:
    get:
      tags: [Checkup — Revue 360]
      summary: Détail d'un bilan
      parameters:
        - { name: ref, in: path, required: true, schema: { type: string } }
        - { name: courtier, in: query, required: true, schema: { type: string } }
      responses:
        '200':
          description: Analyse complète du bilan (axes de couverture, lacunes, contrats, recommandations).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CheckupBilanDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /checkup/api/v1/checkup/{ref}/docx:
    get:
      tags: [Checkup — Revue 360]
      summary: Télécharger le bilan (DOCX)
      parameters:
        - { name: ref, in: path, required: true, schema: { type: string } }
        - { name: courtier, in: query, required: true, schema: { type: string } }
      responses:
        '200':
          description: Document Word.
          content:
            application/vnd.openxmlformats-officedocument.wordprocessingml.document:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── AUGMENTED / CONSEIL ─────────────────────────
  /augmented/api/v1/dossiers:
    post:
      tags: [Augmented — Devoir de conseil]
      summary: Créer un dossier de conseil
      description: Initie un dossier de devoir de conseil IDD/DDA (profil client + pièces optionnelles) ; déclenche l'analyse IA.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [courtier_id, client_nom, client_prenom]
              properties:
                courtier_id: { type: string }
                client_nom: { type: string }
                client_prenom: { type: string }
                client_email: { type: string }
                branche: { type: string }
                type_operation: { type: string }
                canal_vente: { type: string }
                message_courtier: { type: string, description: Contexte courtier pour l'analyse. }
                mode: { type: string, enum: [standard, fast_track], default: standard }
                langue: { type: string, enum: [fr, nl], default: fr }
      responses:
        '201':
          description: Dossier créé.
          content:
            application/json:
              schema:
                type: object
                properties:
                  dossier_id: { type: string, format: uuid }
                  reference: { type: string }
                  statut: { type: string }
                  branche_detectee: { type: string }
                  diagnostic: { type: string, description: Conseil (markdown). }
                  created_at: { type: string, format: date-time }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
    get:
      tags: [Augmented — Devoir de conseil]
      summary: Lister les dossiers
      parameters:
        - { name: courtier, in: query, required: true, schema: { type: string } }
        - { name: statut, in: query, schema: { type: string, enum: [BROUILLON, EN_ATTENTE_VALIDATION, VALIDE, TRANSMIS, ARCHIVE] } }
        - { name: branche, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 20, maximum: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        '200':
          description: Liste paginée.
          content:
            application/json:
              schema:
                type: object
                properties:
                  dossiers: { type: array, items: { $ref: '#/components/schemas/AugmentedDossierListItem' } }
                  total: { type: integer }
                  limit: { type: integer }
                  offset: { type: integer }
                  has_more: { type: boolean }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /augmented/api/v1/dossiers/{id}:
    get:
      tags: [Augmented — Devoir de conseil]
      summary: Consulter un dossier
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Dossier complet (conseil, client, pièces, piste d'audit IDD).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AugmentedDossierDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /augmented/api/v1/dossiers/{id}/validate:
    post:
      tags: [Augmented — Devoir de conseil]
      summary: Valider un dossier (acte du courtier)
      description: Validation humaine du conseil (accomplissement du devoir IDD). Verrou IDD.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [courtier_id]
              properties:
                courtier_id: { type: string }
                commentaire: { type: string }
      responses:
        '200':
          description: Dossier validé.
          content:
            application/json:
              schema:
                type: object
                properties:
                  dossier_id: { type: string, format: uuid }
                  statut: { type: string, example: VALIDE }
                  valide_at: { type: string, format: date-time }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /augmented/api/v1/dossiers/{id}/rapport.docx:
    get:
      tags: [Augmented — Devoir de conseil]
      summary: Exporter la fiche conseil (DOCX)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Fiche conseil Word.
          content:
            application/vnd.openxmlformats-officedocument.wordprocessingml.document:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Aucune fiche générée pour ce dossier. }

  # ───────────────────────── TRANSCRIPT / APPELS ─────────────────────────
  /transcript/api/v1/calls:
    get:
      tags: [Transcript — Analyse d'appels]
      summary: Lister les appels analysés
      parameters:
        - { name: courtier, in: query, required: true, schema: { type: string } }
        - { name: cas, in: query, description: "Codes de cas réglementaires (A,B,C,D,E), séparés par des virgules.", schema: { type: string } }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: page, in: query, schema: { type: integer } }
        - { name: per_page, in: query, schema: { type: integer } }
      responses:
        '200':
          description: Liste paginée des appels.
          content:
            application/json:
              schema:
                type: object
                properties:
                  calls: { type: array, items: { $ref: '#/components/schemas/TranscriptCallListItem' } }
                  meta:
                    type: object
                    properties:
                      total: { type: integer }
                      page: { type: integer }
                      per_page: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /transcript/api/v1/calls/{sessionId}:
    get:
      tags: [Transcript — Analyse d'appels]
      summary: Détail d'un appel
      parameters:
        - { name: sessionId, in: path, required: true, schema: { type: string } }
        - { name: courtier, in: query, schema: { type: string } }
      responses:
        '200':
          description: Analyse complète (cas détectés, alertes, recommandations 360, manques).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TranscriptCallDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /transcript/api/v1/calls/{sessionId}/rapport.docx:
    get:
      tags: [Transcript — Analyse d'appels]
      summary: Rapport courtier de l'appel (DOCX)
      parameters:
        - { name: sessionId, in: path, required: true, schema: { type: string } }
        - { name: type, in: query, schema: { type: string, enum: [rapport_courtier, 3e_pilier], default: rapport_courtier } }
      responses:
        '200':
          description: Rapport Word.
          content:
            application/vnd.openxmlformats-officedocument.wordprocessingml.document:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /transcript/api/v1/stats/summary:
    get:
      tags: [Transcript — Analyse d'appels]
      summary: Synthèse (tableau de bord)
      parameters:
        - { name: courtier, in: query, required: true, schema: { type: string } }
      responses:
        '200':
          description: Métriques (appels traités, dernière activité).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TranscriptStatsSummary' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  # ───────────────────────── CLAIMS / SINISTRES ─────────────────────────
  /claims/api/v1/qualify:
    post:
      tags: [Claims — Sinistres]
      summary: Qualifier un péril sinistre
      description: Classe le péril d'un sinistre à partir d'un email (objet + corps). Sans effet de bord.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subject, body]
              properties:
                broker_id: { type: string }
                subject: { type: string }
                body: { type: string }
                attachments_texts: { type: array, items: { type: string } }
                pays: { type: string, enum: [BE, FR, LU] }
                language: { type: string, enum: [fr, nl, en, de], default: fr }
              example:
                broker_id: assurfinance
                subject: "Dégât des eaux dans la cuisine"
                body: "Bonjour, suite à une fuite sous l'évier hier soir, le sol et les meubles bas de la cuisine sont endommagés. Que dois-je faire ?"
                pays: BE
                language: fr
      responses:
        '200':
          description: Péril qualifié.
          content:
            application/json:
              schema:
                type: object
                properties:
                  peril: { type: string }
                  confidence: { type: number }
                  secondary_peril: { type: [string, 'null'] }
                  urgency: { type: string }
                  pays: { type: [string, 'null'] }
              example:
                peril: degat_des_eaux
                confidence: 0.96
                secondary_peril: null
                urgency: normale
                pays: be
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /claims/api/v1/claims/dossiers:
    get:
      tags: [Claims — Sinistres]
      summary: Lister les dossiers sinistre
      parameters:
        - { name: broker_id, in: query, schema: { type: string } }
        - { name: validation_state, in: query, schema: { type: string, enum: [draft, validated, edited, rejected, sent] } }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        '200':
          description: Liste des dossiers.
          content:
            application/json:
              schema:
                type: object
                properties:
                  dossiers: { type: array, items: { $ref: '#/components/schemas/ClaimsDossierListItem' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /claims/api/v1/claims/dossiers/{id}:
    get:
      tags: [Claims — Sinistres]
      summary: Consulter un dossier sinistre
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Dossier complet (qualification, identification contrat, analyse couverture, pièces).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ClaimsDossierDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Claims — Sinistres]
      summary: Workflow d'un dossier (valider / éditer / rejeter)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action: { type: string, enum: [validate, edit, reject, mark_sent] }
                validated_by: { type: string }
                notes: { type: string }
      responses:
        '200':
          description: Dossier mis à jour.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ClaimsDossierDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ───────────────────────── SENTINEL / DÉTECTIONS ─────────────────────────
  /sentinel/api/v1/detections:
    get:
      tags: [Sentinel — Détections conformité]
      summary: Lister les détections
      description: Réclamations / droits RGPD détectés sur les emails entrants d'un cabinet.
      parameters:
        - { name: courtier, in: query, required: true, description: UUID du cabinet., schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string, enum: [pending, validated, rejected] } }
        - { name: category, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 20, maximum: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        '200':
          description: Détections paginées.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detections: { type: array, items: { $ref: '#/components/schemas/SentinelDetection' } }
                  count: { type: integer, description: Total avant pagination. }
                  limit: { type: integer }
                  offset: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /sentinel/api/v1/detections/stats:
    get:
      tags: [Sentinel — Détections conformité]
      summary: Statistiques de détections
      parameters:
        - { name: courtier, in: query, required: true, schema: { type: string, format: uuid } }
        - { name: days, in: query, schema: { type: integer, default: 30 } }
      responses:
        '200':
          description: Agrégats par catégorie × statut.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SentinelDetectionStats' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /sentinel/api/v1/detections/{id}/validate:
    post:
      tags: [Sentinel — Détections conformité]
      summary: Valider une détection (acte du courtier)
      description: Valide une détection en attente → crée le dossier conformité correspondant (réclamation / RGPD).
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                validated_by: { type: string, format: uuid }
      responses:
        '200':
          description: Détection validée.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  detection: { $ref: '#/components/schemas/SentinelDetection' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /sentinel/api/v1/detections/{id}/reject:
    post:
      tags: [Sentinel — Détections conformité]
      summary: Rejeter une détection (faux positif)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, maxLength: 500 }
      responses:
        '200':
          description: Détection rejetée.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  detection: { $ref: '#/components/schemas/SentinelDetection' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: bo_live_…
      description: "Clé API du cabinet. `Authorization: Bearer bo_live_…`. Scope requis selon le service appelé."
  schemas:
    AmlContract:
      type: object
      description: Contrat fourni par l'appelant (CRM-agnostique).
      properties:
        ref_dossier: { type: string }
        client_ref: { type: string }
        party_id: { type: string }
        policy_id: { type: string }
        type_dossier: { type: string }
        branche: { type: string }
        produit: { type: string, description: "Type de produit (vie/IBIP filtré pour l'audit lot)." }
        prime_annuelle: { type: number }
        prime_unique: { type: number }
        cumul_12_mois: { type: number }
        date_effet: { type: string, format: date }
        pays_residence: { type: string }
        origine_fonds_declaree: { type: string }
        ppe: { type: boolean, description: Personne politiquement exposée. }
        adresse_renseignee: { type: boolean }
        ctif: { type: boolean }
        embargo: { type: boolean }
    AmlAuditResult:
      type: object
      description: Résultat d'audit AML déterministe d'un contrat (proposition à valider/signer par l'AMLCO — G2).
      properties:
        brokerId: { type: string }
        annee: { type: integer }
        enExercice: { type: boolean, description: Le contrat tombe-t-il dans l'exercice audité. }
        refDossier: { type: [string, 'null'] }
        clientRef: { type: [string, 'null'] }
        typeDossier: { type: [string, 'null'] }
        produit: { type: [string, 'null'] }
        classification: { type: string, description: "Classe Annexe (ex. standard, accrue)." }
        classificationJustification: { type: string }
        niveauVigilance: { type: string, enum: [standard, accrue], description: Niveau de vigilance LCB-FT. }
        niveauPropose: { type: integer, description: "Niveau proposé 1–4 (4 = CTIF/embargo, escalade confidentielle jamais déposée en lot)." }
        risqueClient: { type: string, enum: [faible, moyen, eleve] }
        clignotants:
          type: array
          description: Identifiants des clignotants (red flags) déclenchés.
          items: { type: string }
        clignotantsDetail:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              libelle: { type: string }
              tier: { type: string, enum: [standard, accrue] }
              evidence: { type: string, description: Justification non-PII du déclenchement. }
        regle2sur3:
          type: object
          description: Règle « 2 indices sur 3 » de présomption de vigilance accrue.
          properties:
            applicable: { type: boolean }
            nbReunies: { type: integer }
            declenche: { type: boolean }
        ctif: { type: boolean, description: Déclaration CTIF/CRF requise (escalade confidentielle). }
        embargo: { type: boolean }
        rapportMarkdown: { type: string, description: "Rapport AML intermédiaire rédigé (markdown, document interne non divulgable au client)." }
    # ───────────────────────── CHECKUP ─────────────────────────
    CheckupBilanListItem:
      type: object
      description: Ligne de liste d'un bilan 360° (sans l'analyse détaillée).
      properties:
        id: { type: string, format: uuid }
        ref: { type: string, example: ICU-BILAN-206200-202604 }
        courtier_id: { type: string, description: Slug du cabinet. }
        client_id: { type: string }
        client_nom: { type: string }
        client_prenom: { type: string }
        profil: { type: string, enum: [PRIVE, PRO, MIXTE, BUSINESS, NON_DEFINI] }
        score_couverture: { type: integer, description: Score de couverture 0–100. }
        lacunes_critiques_count: { type: integer }
        axes_complets_count: { type: integer }
        axes_absents_count: { type: integer }
        html_path: { type: [string, 'null'] }
        docx_path: { type: [string, 'null'] }
        source: { type: string, enum: [manual, brio, backfill] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        statut: { type: string, enum: [a_traiter, en_cours, cloture, archive], description: "Statut workflow (champ optionnel)." }
        note_interne: { type: [string, 'null'] }
        dossier_augmented_ref: { type: [string, 'null'], description: Référence du dossier Augmented lié (cross-sell décidé). }
        email_envoye_at: { type: [string, 'null'], format: date-time }
        email_envoye_to: { type: [string, 'null'] }
        version_n: { type: integer, default: 1 }
        is_archived: { type: boolean, default: false }
        event_trigger: { type: [string, 'null'], description: Événement de vie ayant déclenché une nouvelle version. }
    CheckupAxe:
      type: object
      description: Axe (privé) ou zone (business) de couverture analysé.
      properties:
        axe: { type: string }
        zone: { type: string, description: Présent à la place de `axe` pour les zones business. }
        statut: { type: string, enum: [complet, partiel, absent, ambigu] }
        priorite: { type: string, enum: [H, M, B, OK] }
        lacune_critique: { type: boolean }
        detail_couverture: { type: string }
        lacune: { type: [string, 'null'] }
        recommandation: { type: string }
        garanties_reference: { type: array, items: { type: string } }
        garanties_presentes: { type: array, items: { type: string } }
        garanties_manquantes: { type: array, items: { type: string } }
        couverture_ipid: { type: string }
    CheckupContrat:
      type: object
      properties:
        branche_famille: { type: string }
        assureur: { type: string }
        numero_police: { type: string }
        produit: { type: [string, 'null'] }
        etat: { type: string, example: En cours }
        date_debut: { type: string }
        prime_annuelle: { type: [number, 'null'], description: Prime annuelle en EUR. }
        garanties: { type: array, items: { type: string } }
    CheckupBilanDetail:
      type: object
      description: Bilan 360° complet incluant l'analyse détaillée (`data_full`) et son miroir typé (`data_typed`).
      properties:
        id: { type: string, format: uuid }
        ref: { type: string }
        courtier_id: { type: string }
        client_id: { type: string }
        client_nom: { type: string }
        client_prenom: { type: string }
        profil: { type: string, enum: [PRIVE, PRO, MIXTE, BUSINESS, NON_DEFINI] }
        score_couverture: { type: integer }
        lacunes_critiques_count: { type: integer }
        axes_complets_count: { type: integer }
        axes_absents_count: { type: integer }
        data_full:
          type: object
          description: Snapshot complet de l'analyse.
          properties:
            profil: { type: string }
            client:
              type: object
              properties:
                id: { type: string }
                nom: { type: string }
                prenom: { type: string }
                bce: { type: [string, 'null'] }
                tva: { type: [string, 'null'] }
                adresse: { type: string }
                gsm: { type: [string, 'null'] }
                email: { type: [string, 'null'] }
                forme_juridique: { type: [string, 'null'] }
                secteur_activite: { type: [string, 'null'] }
                personnes:
                  type: array
                  items:
                    type: object
                    properties:
                      prenom: { type: string }
                      nom: { type: string }
                      lien: { type: string, example: Conjoint }
            contrats: { type: array, items: { $ref: '#/components/schemas/CheckupContrat' } }
            axes_prive: { type: array, items: { $ref: '#/components/schemas/CheckupAxe' } }
            zones_business: { type: array, items: { $ref: '#/components/schemas/CheckupAxe' } }
            zone_croisee:
              type: array
              items:
                type: object
                properties:
                  question: { type: string }
                  constat: { type: string }
            signaux_business: { type: array, items: { type: string } }
            signaux_prive: { type: array, items: { type: string } }
            signaux_absents: { type: array, items: { type: string } }
            incertitudes: { type: array, items: { type: string } }
            donnees_manquantes: { type: array, items: { type: string } }
            anomalies: { type: array, items: { type: string } }
            score_couverture: { type: number }
            top3:
              type: array
              items:
                type: object
                properties:
                  titre: { type: string }
                  description: { type: string }
                  priorite: { type: string, enum: [H, M, B] }
            prime_totale: { type: [number, 'null'] }
            declaration_type: { type: string, enum: [NONE, EB, IBIP] }
            ibip_motif: { type: [string, 'null'] }
            declaration_adequation_requise: { type: boolean }
            declaration_adequation_motif: { type: [string, 'null'] }
        data_typed:
          type: object
          additionalProperties: true
          description: Miroir « cellules typées » de `data_full` (chaque champ = `{ type, value, unit? }`). Schéma `tc-1`.
        typed_schema_version: { type: string, example: tc-1 }
        html_path: { type: [string, 'null'] }
        docx_path: { type: [string, 'null'] }
        source: { type: string, enum: [manual, brio, backfill] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        statut: { type: string, enum: [a_traiter, en_cours, cloture, archive] }
        note_interne: { type: [string, 'null'] }
        dossier_augmented_ref: { type: [string, 'null'] }
        email_envoye_at: { type: [string, 'null'], format: date-time }
        email_envoye_to: { type: [string, 'null'] }
        version_n: { type: integer }
        is_archived: { type: boolean }
        event_trigger: { type: [string, 'null'] }

    # ───────────────────────── AUGMENTED ─────────────────────────
    AugmentedDossierListItem:
      type: object
      properties:
        dossier_id: { type: string, format: uuid }
        reference: { type: string }
        statut: { type: string, enum: [BROUILLON, EN_ATTENTE_VALIDATION, VALIDE, TRANSMIS, ARCHIVE] }
        branche: { type: [string, 'null'] }
        langue: { type: string, enum: [fr, nl] }
        client:
          type: object
          properties:
            nom: { type: string }
            prenom: { type: string }
        type_operation: { type: [string, 'null'] }
        canal_vente: { type: [string, 'null'] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        valide_at: { type: [string, 'null'], format: date-time }
        archived_at: { type: [string, 'null'], format: date-time }
    AugmentedDossierDetail:
      type: object
      description: Dossier de conseil complet (conseil rédigé, pièces, vérification, piste d'audit).
      properties:
        dossier_id: { type: string, format: uuid }
        reference: { type: string }
        statut: { type: string, enum: [BROUILLON, EN_ATTENTE_VALIDATION, VALIDE, TRANSMIS, ARCHIVE] }
        branche: { type: [string, 'null'] }
        langue: { type: string, enum: [fr, nl] }
        client:
          type: object
          properties:
            nom: { type: string }
            prenom: { type: string }
            email: { type: [string, 'null'] }
        type_operation: { type: [string, 'null'] }
        canal_vente: { type: [string, 'null'] }
        rapport_genere: { type: [string, 'null'], description: "Conseil rédigé (markdown, nettoyé pour rendu)." }
        rapport_json: { type: [object, 'null'], additionalProperties: true, description: "Structure du conseil (besoins / marché / justification, métadonnées de validation)." }
        valide_par: { type: [string, 'null'] }
        valide_at: { type: [string, 'null'], format: date-time }
        pieces:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              type: { type: string, example: contrat }
              nom: { type: string }
              taille: { type: integer, description: Taille en octets. }
        audit_trail:
          type: object
          properties:
            document_hash: { type: [string, 'null'], description: "SHA-256 (hex), posé à la transmission." }
            timestamp_certifie: { type: [string, 'null'], format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        analysis_started_at: { type: [string, 'null'], format: date-time }
        analysis_completed_at: { type: [string, 'null'], format: date-time }
        analysis_error: { type: [string, 'null'] }
        verif:
          type: [object, 'null']
          description: Résultat du pipeline de vérification du devoir de conseil.
          properties:
            verdict: { type: string, example: PASS }
            score: { type: number }
            config_id: { type: string }
            passes:
              type: array
              items:
                type: object
                properties:
                  id: { type: string }
                  severity: { type: string }
                  passed: { type: boolean }
                  skipped: { type: boolean }
        cg_grounding:
          type: [object, 'null']
          description: Vérification d'ancrage des citations CG.
          properties:
            checked: { type: integer }
            verified: { type: integer }
            failed: { type: integer }
            allGrounded: { type: boolean }
            sourceEmpty: { type: boolean }

    # ───────────────────────── TRANSCRIPT ─────────────────────────
    TranscriptAlerte:
      type: object
      properties:
        niveau: { type: string, enum: [CRITIQUE, IMPORTANT, INFO] }
        message: { type: string }
        module: { type: [string, 'null'] }
        champ: { type: [string, 'null'] }
    TranscriptCallListItem:
      type: object
      properties:
        session_id: { type: string, example: IDD-1700000000000-DEADBEEF }
        call_uuid: { type: [string, 'null'], description: UUID de l'appel Leexi. }
        title: { type: [string, 'null'] }
        duration_sec: { type: [integer, 'null'] }
        cabinet_id: { type: [string, 'null'] }
        cabinet_name: { type: [string, 'null'] }
        langue: { type: [string, 'null'], enum: [FR, NL, null] }
        cas_detectes: { type: array, items: { type: string, enum: [A, B, C, D, E] } }
        alertes_critiques_count: { type: integer }
        alertes_importantes_count: { type: integer }
        recommandations_count: { type: integer }
        manques_count: { type: integer }
        has_docx: { type: boolean }
        created_at: { type: string, format: date-time }
    TranscriptCallDetail:
      type: object
      description: Analyse complète d'un appel. Le sous-objet `session` agrège les extractions par module (forme variable selon le cas).
      properties:
        session_id: { type: string }
        session:
          type: object
          description: Données brutes et extractions de l'appel.
          properties:
            session_id: { type: string }
            timestamp: { type: string, format: date-time }
            langue: { type: [string, 'null'] }
            cas_detectes: { type: array, items: { type: string, enum: [A, B, C, D, E] } }
            leexi: { type: object, additionalProperties: true, description: "Métadonnées Leexi (cabinet_id, call_uuid, title, duration_seconds…)." }
            transcript: { type: string, description: Transcription normalisée. }
            cabinet_id: { type: [string, 'null'] }
            client: { type: object, additionalProperties: true, description: "Champs client extraits (email, phone…)." }
            modules: { type: object, additionalProperties: true, description: "Résultats d'extraction par module (clés A–E), ou `{ _erreur }` en cas d'échec." }
            aml_data: { type: object, additionalProperties: true }
            aml_balises:
              type: array
              items:
                type: object
                properties:
                  tag: { type: string, example: PEP_CHECK }
                  priorite: { type: integer }
                  message: { type: string }
                  verbatim: { type: [string, 'null'] }
            aml_evaluation: { type: object, additionalProperties: true }
            aml_grounding:
              type: object
              properties:
                checked: { type: integer }
                verified: { type: integer }
                failed: { type: integer }
                skipped: { type: integer }
                allGrounded: { type: boolean }
                failedVerbatims: { type: array, items: { type: object, additionalProperties: true } }
            alertes: { type: array, items: { $ref: '#/components/schemas/TranscriptAlerte' } }
            recommandations_360: { type: array, items: { type: object, additionalProperties: true } }
            manques: { type: array, items: { type: object, additionalProperties: true } }
            penbox_plan: { type: [object, 'null'], additionalProperties: true }
            audit:
              type: object
              properties:
                payload_hash: { type: string, description: SHA-256 du payload. }
                model: { type: string, description: Identifiant du modèle Claude. }
                router_result: { type: object, additionalProperties: true }
        alertes_critiques: { type: array, items: { $ref: '#/components/schemas/TranscriptAlerte' } }
        alertes_importantes: { type: array, items: { $ref: '#/components/schemas/TranscriptAlerte' } }
        recommandations_360: { type: array, items: { type: object, additionalProperties: true } }
        manques: { type: array, items: { type: object, additionalProperties: true } }
        documents:
          type: array
          items:
            type: object
            properties:
              filename: { type: string }
              type: { type: string, example: rapport_courtier_aml }
              variante: { type: [string, 'null'] }
              cas: { type: [string, 'null'], description: Code de cas A–E pour une fiche IDD. }
              gabarit: { type: [string, 'null'] }
              label_cas: { type: [string, 'null'] }
              size: { type: [integer, 'null'], description: Taille en octets. }
        email_notified_at: { type: [string, 'null'], format: date-time }
        email_notified_to: { type: [string, 'null'] }
    TranscriptStatsSummary:
      type: object
      properties:
        status: { type: string, enum: [online, degraded, offline] }
        cabinet_id: { type: [string, 'null'] }
        metrics:
          type: array
          description: Jusqu'à 4 métriques de tableau de bord.
          items:
            type: object
            properties:
              label: { type: string, example: Appels traités }
              value: { description: Nombre ou chaîne. }
              trend: { type: string, enum: [up, down, flat] }
              delta: { type: number }
        actions_attendues: { type: integer }
        last_activity_at: { type: [string, 'null'], format: date-time }
        meta:
          type: object
          properties:
            module: { type: string, example: transcript }
            source: { type: string, enum: [scoped, legacy_flat, empty] }

    # ───────────────────────── CLAIMS ─────────────────────────
    ClaimsDossierListItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        created_at: { type: string, format: date-time }
        broker_id: { type: string }
        status: { type: string, description: "État du dossier (ex. draft_ready, info_missing, not_found)." }
        validation_state: { type: string, enum: [pending, validated, edited, rejected, sent] }
        input_message_id: { type: string, description: Identifiant du message email source (provider). }
        typology: { type: string, description: "Typologie de péril (ex. degat_eaux, incendie)." }
        status_input:
          type: object
          properties:
            has_critical_alert: { type: boolean }
            contract_candidates: { type: integer }
            contract_selected: { type: boolean }
            has_actionable_pieces: { type: boolean }
            evidence_canal: { type: [string, 'null'] }
    ClaimsDossierDetail:
      type: object
      description: Dossier sinistre complet (qualification, identification contrat, alertes, validation).
      properties:
        id: { type: string, format: uuid }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        broker_id: { type: string }
        status: { type: string }
        validation_state: { type: string, enum: [pending, validated, edited, rejected, sent] }
        payload:
          type: object
          description: Entrée du dossier (email + contexte courtier).
          properties:
            mail:
              type: object
              properties:
                from:
                  type: object
                  properties:
                    name: { type: string }
                    email: { type: string }
                subject: { type: string }
                body_text: { type: string }
            broker:
              type: object
              properties:
                id: { type: string }
                country: { type: string, enum: [BE, FR, LU] }
                agency_email: { type: string }
            version: { type: string }
            intention:
              type: object
              properties:
                label: { type: string, example: sinistre }
                confidence: { type: number }
            message_id: { type: string }
            received_at: { type: string, format: date-time }
            extracted_indices: { type: object, additionalProperties: true }
            language_detected: { type: string }
        output:
          type: object
          description: Sortie d'analyse du sinistre.
          properties:
            alerts:
              type: array
              items:
                type: object
                properties:
                  code: { type: string }
                  level: { type: string, enum: [info, warning, critical] }
                  message: { type: string }
            status: { type: string }
            version: { type: string }
            session_id: { type: string, format: uuid }
            generated_at: { type: string, format: date-time }
            traceability:
              type: object
              properties:
                flux_ids: { type: array, items: { type: string } }
                latency_ms: { type: integer }
                models_used: { type: array, items: { type: string } }
                meta_version: { type: string }
                skills_invoked: { type: array, items: { type: string } }
            input_message_id: { type: string }
            event_qualification:
              type: object
              properties:
                urgency: { type: string, example: 48h }
                typology: { type: string }
                key_facts: { type: array, items: { type: string } }
                bodily_injury: { type: string, enum: [yes, no, unknown] }
                quoted_excerpts:
                  type: array
                  items:
                    type: object
                    properties:
                      text: { type: string }
                      source: { type: string }
                estimated_severity: { type: string, enum: [low, medium, high] }
                secondary_typology: { type: [string, 'null'] }
                typology_confidence: { type: number }
                event_date_extracted: { type: [string, 'null'] }
                third_party_involved: { type: string, enum: [yes, no, unknown] }
            contract_identification:
              type: object
              properties:
                status: { type: string, enum: [identified, ambiguous, not_found] }
                candidates:
                  type: array
                  items:
                    type: object
                    properties:
                      branch: { type: string }
                      client:
                        type: object
                        properties:
                          name: { type: string }
                          email: { type: string }
                          phone: { type: string }
                          address: { type: string }
                      status: { type: string }
                      company: { type: string }
                      product: { type: string }
                      policy_id: { type: string }
                      policy_number: { type: string }
                      match_score: { type: number }
                      match_reasons: { type: array, items: { type: string } }
                      date_effet_fin: { type: [string, 'null'] }
            human_validation_required: { type: boolean }
        input_message_id: { type: string }
        validated_at: { type: [string, 'null'], format: date-time }
        pieces_status: { type: object, additionalProperties: true, description: Statut par pièce attendue (routage Penbox). }

    # ───────────────────────── SENTINEL ─────────────────────────
    SentinelDetection:
      type: object
      description: >
        Détection (réclamation / droit RGPD) issue d'un email entrant. Les champs PII
        (client_name, client_email, subject, summary, known_client_name) ne sont renvoyés
        que pour le statut `pending` (minimisation) ; ils sont null après traitement.
      properties:
        id: { type: string, format: uuid }
        courtier_id: { type: string, format: uuid }
        category: { type: string, enum: [RECLAMATION, DEMANDE_DROITS_RGPD, BESOIN_ASSURANCE, AMBASSADEUR, SINISTRE, NEUTRE, HORS_SCOPE] }
        priority: { type: [string, 'null'], enum: [CRITIQUE, HAUTE, MOYENNE, BASSE, null] }
        confidence: { type: [number, 'null'] }
        proposed_actions:
          type: array
          items:
            type: object
            properties:
              target: { type: string, example: advice }
              endpoint: { type: string, example: /api/v1/advice/reclamation }
              label: { type: string }
        status: { type: string, enum: [pending, validated, rejected, expired, auto_forwarded] }
        detected_at: { type: string, format: date-time }
        email_received_at: { type: string, format: date-time }
        expires_at: { type: [string, 'null'], format: date-time }
        validated_at: { type: [string, 'null'], format: date-time }
        rejected_at: { type: [string, 'null'], format: date-time }
        forwarded_at: { type: [string, 'null'], format: date-time }
        validated_by: { type: [string, 'null'], format: uuid }
        rejected_reason: { type: [string, 'null'] }
        forwarded_to: { type: [array, 'null'], items: { type: string } }
        forwarded_status: { type: [object, 'null'], additionalProperties: true, description: Résultat du forward par module cible. }
        email_provider: { type: string, enum: [ms365, gmail] }
        email_provider_message_id: { type: string }
        created_at: { type: string, format: date-time }
        client_name: { type: [string, 'null'] }
        client_email: { type: [string, 'null'] }
        subject: { type: [string, 'null'] }
        summary: { type: [string, 'null'] }
        known_client: { type: [boolean, 'null'] }
        known_client_name: { type: [string, 'null'] }
        client_recognized: { type: boolean }
    SentinelDetectionStats:
      type: object
      properties:
        by_category: { type: object, additionalProperties: { type: integer }, description: Compte par catégorie. }
        by_status: { type: object, additionalProperties: { type: integer }, description: Compte par statut. }
        total: { type: integer }
        last_activity_at: { type: [string, 'null'], format: date-time }
        period_days: { type: integer }

    Error:
      type: object
      properties:
        error: { type: string }
        reason: { type: string, description: "Motif court non sensible (ex. tenant_mismatch, scope_denied)." }
  responses:
    BadRequest:
      description: Requête invalide (paramètre requis manquant).
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: Clé absente, inconnue ou révoquée.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: "Scope insuffisant (`scope_denied`) ou tenant différent du cabinet de la clé (`tenant_mismatch`)."
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: Ressource introuvable (ou hors du périmètre du cabinet).
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    RateLimited:
      description: Quota dépassé (rate-limit). Réessayer après un délai.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
