openapi: 3.1.0
info:
  title: Spartadoc Partner API
  version: 1.0.0
  description: |
    API partenaire Spartadoc. Intégrez la vérification d'intégrité cryptographique,
    les résumés de souscription et la gestion de partages dans vos systèmes.

    **Authentification** : clé API via l'en-tête `X-API-Key`.
    Les endpoints de gestion de clés (`/api-keys`) utilisent JWT (session INTEGRATOR).
  contact:
    name: Support Spartadoc API
    email: api@spartadoc.com
  license:
    name: Propriétaire — Spartadoc Q, Spartadoc Inc.

servers:
  - url: https://spartadoc.com/api
    description: Production

tags:
  - name: Verification
    description: Vérification d'intégrité et complétude
  - name: Shares
    description: Création et révocation de partages
  - name: Dossiers
    description: Accès complet aux dossiers (scope admin)
  - name: Email Gateway
    description: Réception de documents par courriel
  - name: Reminders
    description: Rappels SMS automatiques
  - name: API Keys
    description: Gestion des clés API (JWT requis)
  - name: Capsule
    description: Sceau RFC 3161 des capsules de /declare (public, sans compte, sans état)

security:
  - ApiKeyAuth: []

paths:
  /partner/verify/{merkleRoot}:
    get:
      tags: [Verification]
      summary: Vérifier l'intégrité d'un dossier
      description: Vérifie l'intégrité cryptographique via la racine Merkle.
      parameters:
        - name: merkleRoot
          in: path
          required: true
          schema: { type: string }
          description: Racine Merkle hexadécimale du dossier.
      responses:
        '200':
          description: Dossier vérifié
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VerifyResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /partner/shares/{shareId}/completeness:
    get:
      tags: [Verification]
      summary: Score de complétude d'un partage
      parameters:
        - name: shareId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Score de complétude
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CompletenessResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /partner/shares/{shareId}/summary:
    get:
      tags: [Verification]
      summary: Résumé de souscription (ABD/ATD/RPV)
      description: Ratios ABD/ATD/RPV pré-calculés à partir du dossier partagé.
      parameters:
        - name: shareId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Résumé de souscription
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SummaryResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /partner/shares:
    post:
      tags: [Shares]
      summary: Créer un partage révocable
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateShareRequest' }
      responses:
        '201':
          description: Partage créé
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ShareResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /partner/shares/{shareId}:
    delete:
      tags: [Shares]
      summary: Révoquer un partage immédiatement
      parameters:
        - name: shareId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Partage révoqué
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked: { type: boolean, example: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /partner/dossiers:
    get:
      tags: [Dossiers]
      summary: Lister tous les dossiers hypothécaires
      description: Nécessite le scope `admin`.
      responses:
        '200':
          description: Liste des dossiers
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/DossierSummary' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /partner/dossiers/{id}:
    get:
      tags: [Dossiers]
      summary: Détail complet d'un dossier
      description: Nécessite le scope `admin`. Inclut documents, données IA et partages.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Dossier complet
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DossierDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /partner/inbox/{fileId}:
    get:
      tags: [Email Gateway]
      summary: Obtenir l'adresse courriel d'un dossier
      security:
        - BearerAuth: []
      parameters:
        - name: fileId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Inbox et courriels reçus
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InboxResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Email Gateway]
      summary: Activer l'inbox et définir les expéditeurs autorisés
      security:
        - BearerAuth: []
      parameters:
        - name: fileId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [allowedEmails]
              properties:
                allowedEmails:
                  type: array
                  items: { type: string, format: email }
      responses:
        '200':
          description: Inbox activée
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InboxResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /partner/reminders/{fileId}:
    get:
      tags: [Reminders]
      summary: Lister les rappels d'un dossier
      security:
        - BearerAuth: []
      parameters:
        - name: fileId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Liste des rappels
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Reminder' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /partner/reminders/{fileId}/stop:
    post:
      tags: [Reminders]
      summary: Arrêter tous les rappels actifs
      security:
        - BearerAuth: []
      parameters:
        - name: fileId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Rappels arrêtés
          content:
            application/json:
              schema:
                type: object
                properties:
                  stopped: { type: integer, example: 1 }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /partner/api-keys:
    get:
      tags: [API Keys]
      summary: Lister les clés API (préfixe seulement)
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Liste des clés
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ApiKeySummary' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [API Keys]
      summary: Générer une nouvelle clé API
      description: La clé complète est retournée UNE SEULE FOIS. Stockez-la immédiatement.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, scope]
              properties:
                name: { type: string, example: "Production CRM" }
                scope:
                  type: string
                  enum: [read, write, admin]
                expiresInDays: { type: integer, example: 365 }
      responses:
        '201':
          description: Clé créée
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiKeyCreated' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /partner/api-keys/{keyId}:
    delete:
      tags: [API Keys]
      summary: Révoquer une clé API
      description: Effet immédiat. La clé ne peut plus être utilisée.
      security:
        - BearerAuth: []
      parameters:
        - name: keyId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Clé révoquée
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked: { type: boolean, example: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /capsules/sceau:
    post:
      tags: [Capsule]
      summary: Sceller l'empreinte d'un manifeste de capsule
      description: |
        Fait sceller par une autorité d'horodatage tierce (RFC 3161 ; DigiCert,
        repli GlobalSign) l'empreinte SHA-256 du `manifeste.json` d'une capsule
        composée sur /declare. Le corps n'a qu'un champ : ni l'œuvre, ni les
        échanges, ni la clé ne sont transmis. Le jeton est vérifié avant d'être
        rendu. Sans état : rien n'est conservé. 10 requêtes par heure et par IP.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [empreinte]
              properties:
                empreinte: { type: string, pattern: '^[0-9a-f]{64}$', description: SHA-256 hexadécimal (minuscules) des octets de manifeste.json. }
      responses:
        '200':
          description: Jeton RFC 3161 (DER, base64), autorité signataire et date du sceau.
          content:
            application/json:
              schema:
                type: object
                properties:
                  jeton: { type: string, description: TimeStampResp DER en base64 (contenu de sceau.tsr). }
                  autorite: { type: [string, 'null'], example: 'DigiCert, Inc.' }
                  scelleLe: { type: [string, 'null'], format: date-time }
        '400': { description: 'EMPREINTE_INVALIDE : autre chose que 64 caractères hexadécimaux, ou un champ en trop.' }
        '429': { description: 'TROP_DE_SCEAUX : plus de 10 requêtes par heure depuis cette adresse.' }
        '503': { description: 'SCEAU_INDISPONIBLE : aucune autorité n''a rendu un jeton valide. Aucun jeton n''est émis.' }

  /capsules/sceau/verifier:
    post:
      tags: [Capsule]
      summary: Vérifier la signature d'un sceau
      description: |
        Vérifie un jeton RFC 3161 présenté avec l'empreinte qu'il doit couvrir
        (`openssl ts -verify -digest`, chaîne jusqu'à la racine de l'autorité).
        Le même contrôle se fait hors ligne avec OpenSSL et la racine publiée
        par l'autorité (voir LISEZ-MOI.txt dans la capsule). 60 requêtes par
        heure et par IP.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [empreinte, jeton]
              properties:
                empreinte: { type: string, pattern: '^[0-9a-f]{64}$' }
                jeton: { type: string, maxLength: 21848, description: Contenu de sceau.tsr en base64 (16 Ko au plus). }
      responses:
        '200':
          description: 'Verdict. valide:false porte une raison : JETON_ILLISIBLE, EMPREINTE_DISCORDANTE ou SIGNATURE_INVALIDE.'
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    properties:
                      valide: { const: true }
                      autorite: { type: [string, 'null'] }
                      scelleLe: { type: [string, 'null'], format: date-time }
                  - type: object
                    properties:
                      valide: { const: false }
                      raison: { type: string, enum: [JETON_ILLISIBLE, EMPREINTE_DISCORDANTE, SIGNATURE_INVALIDE] }
        '400': { description: 'REQUETE_INVALIDE' }
        '429': { description: 'TROP_DE_VERIFICATIONS' }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Clé API partenaire (format `sdoc_...`)
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT d'un compte INTEGRATOR (pour la gestion des clés API)

  schemas:
    SecurityLevel:
      type: string
      enum: [LEVEL_1, LEVEL_2, LEVEL_3, LEVEL_4, LEVEL_5]
      description: |
        - `LEVEL_1` : Enveloppe simple (AES-256)
        - `LEVEL_2` : Enveloppe scellée (expiration + limite d'accès)
        - `LEVEL_3` : Coffre à code (TOTP requis)
        - `LEVEL_4` : Coffre quantique (ML-KEM-768)
        - `LEVEL_5` : Chambre forte (SLH-DSA, archivage 25+ ans)

    Scope:
      type: string
      enum: [read, write, admin]

    VerifyResponse:
      type: object
      properties:
        verified: { type: boolean }
        merkleRoot: { type: string }
        completenessScore: { type: number, format: float, minimum: 0, maximum: 1 }
        documentCount: { type: integer }
        documents:
          type: array
          items:
            type: object
            properties:
              type: { type: string, example: "T4" }
              checksum: { type: string }
              securityLevel: { $ref: '#/components/schemas/SecurityLevel' }

    CompletenessResponse:
      type: object
      properties:
        shareId: { type: string }
        completenessScore: { type: number, format: float }
        documentCount: { type: integer }
        documents:
          type: array
          items:
            type: object
            properties:
              type: { type: string }
              confidence: { type: number, format: float }
        flags:
          type: array
          items: { type: string }

    SummaryResponse:
      type: object
      properties:
        shareId: { type: string }
        completenessScore: { type: number, format: float }
        summary:
          type: object
          properties:
            income:
              type: object
              properties:
                grossAnnual: { type: number }
            ratios:
              type: object
              properties:
                gds: { type: number, format: float, description: "ABD" }
                tds: { type: number, format: float, description: "ATD" }
                ltv: { type: number, format: float, description: "RPV" }
        flags:
          type: array
          items: { type: string }

    CreateShareRequest:
      type: object
      required: [mortgageFileId, grantedToEmail]
      properties:
        mortgageFileId: { type: string }
        grantedToEmail: { type: string, format: email }
        expiresInDays: { type: integer, example: 90 }

    ShareResponse:
      type: object
      properties:
        shareId: { type: string }
        grantedToEmail: { type: string, format: email }
        expiresAt: { type: string, format: date-time }

    DossierSummary:
      type: object
      properties:
        id: { type: string }
        clientFirstName: { type: string }
        completenessScore: { type: number, format: float }
        _count:
          type: object
          properties:
            documents: { type: integer }
            shares: { type: integer }

    DossierDetail:
      type: object
      properties:
        id: { type: string }
        consolidatedData:
          type: object
          properties:
            ratios:
              type: object
              properties:
                gds: { type: number, format: float }
                tds: { type: number, format: float }
                ltv: { type: number, format: float }
        documents:
          type: array
          items:
            type: object
            properties:
              mortgageDocType: { type: string }
              aiExtractedData:
                type: object
                additionalProperties: true

    InboxResponse:
      type: object
      properties:
        active: { type: boolean }
        address: { type: string, format: email, example: "dossier-a7b3c9@spartadoc.com" }
        allowedSenders:
          type: array
          items: { type: string, format: email }
        receivedEmails:
          type: array
          items:
            type: object
            properties:
              attachmentCount: { type: integer }
              status: { type: string, enum: [pending, done, failed] }

    Reminder:
      type: object
      properties:
        id: { type: string }
        borrowerEmail: { type: string, format: email }
        status: { type: string, enum: [active, stopped, completed] }
        reminderCount: { type: integer }
        maxReminders: { type: integer }
        logs:
          type: array
          items:
            type: object
            properties:
              channel: { type: string, enum: [sms, email] }
              status: { type: string }

    ApiKeySummary:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        prefix: { type: string, example: "sdoc_a1b2c3" }
        scope: { $ref: '#/components/schemas/Scope' }
        requestCount: { type: integer }

    ApiKeyCreated:
      type: object
      properties:
        id: { type: string }
        key:
          type: string
          description: Clé complète. Affichée UNE SEULE FOIS.
          example: "sdoc_a1b2c3d4e5f6..."
        prefix: { type: string }
        scope: { $ref: '#/components/schemas/Scope' }

    Error:
      type: object
      properties:
        error: { type: string }
        required:
          type: array
          items: { type: string }
        current: { type: string }

  responses:
    Unauthorized:
      description: Clé API manquante, invalide ou révoquée
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Scope insuffisant pour cet endpoint
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Ressource non trouvée ou partage expiré/révoqué
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
