# public/api/openapi.yaml — copie de docs/api-publique.yaml (dépôt voxpop) ; ne pas modifier ici, recopier la source.
# docs/api-publique.yaml
#
# US-B30 — Spécification OpenAPI de l'API publique VoxPop (agrégats uniquement).
# Source de vérité : functions/api_publique.js. Le test functions/api_publique.test.js
# vérifie que chaque route et chaque champ de réponse écrits ici existent dans le code,
# et réciproquement. Le portail (page « Développeurs ») publie ce fichier tel quel.
openapi: 3.0.3
info:
  title: API publique VoxPop
  version: 1.0.0
  description: |
    Des résultats AGRÉGÉS de la plateforme d'opinion VoxPop, pour les outils de veille et de BI.

    Engagements, vérifiés par des tests et par le pré-vol du dépôt :
    - aucune donnée de membre : ni vote, ni profil, ni identifiant de personne ;
    - plancher d'anonymat de 5 : un sondage de moins de 5 voix n'est pas servi ; une
      option de moins de 5 voix est fusionnée dans « autres » (publiée seulement si elle
      atteint 5) ; tous les comptes sont arrondis au multiple de 5 ; les parts sont
      calculées sur les comptes arrondis ;
    - la marge d'erreur est calculée sur l'effectif arrondi ;
    - un sondage non public, sensible, en quarantaine ou retiré par la modération est
      introuvable (404), comme s'il n'existait pas ;
    - ces chiffres décrivent le panel VoxPop, jamais une population : ne pas les présenter
      comme « ce que pense la France ».

    Accès : une clé d'API dans l'en-tête `Authorization: Bearer vp_…` (ou `X-API-Key`).
    Jamais dans l'adresse. La clé est remise une seule fois par VoxPop et n'est jamais
    stockée en clair. Quotas : 60 requêtes par minute, puis un quota par jour UTC selon le
    plan (Pro : 1 000, Annonceur : 10 000). Dépassement : 429 avec `Retry-After`.
    Tant que les offres payantes ne sont pas ouvertes, l'API répond 403 `a_venir`
    (hors clés d'essai).

    Conditions d'usage : réutilisation des agrégats avec mention de VoxPop ; toute tentative
    de ré-identification de personnes est interdite (texte à valider avec les CGU).
servers:
  - url: https://voxpopapp.app/api
security:
  - cleApi: []
paths:
  /v1/sondages:
    get:
      summary: Les sondages publics qui ont au moins 5 voix, du plus récent au plus ancien
      operationId: listerLesSondages
      parameters:
        - name: limite
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
        - name: curseur
          in: query
          description: La valeur `curseur` de la page précédente.
          schema: { type: string }
        - name: categorie
          in: query
          description: Identifiant de thème (ex. « politique »).
          schema: { type: string }
      responses:
        "200":
          description: Une page de sondages
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ListeDeSondages" }
        "401": { $ref: "#/components/responses/NonAuthentifie" }
        "403": { $ref: "#/components/responses/AVenir" }
        "429": { $ref: "#/components/responses/TropDeRequetes" }
  /v1/sondages/{id}/resultats:
    get:
      summary: Les résultats agrégés d'un sondage public
      operationId: resultatsDUnSondage
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Parts, effectif arrondi et marge d'erreur
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResultatsDeSondage" }
        "401": { $ref: "#/components/responses/NonAuthentifie" }
        "403": { $ref: "#/components/responses/AVenir" }
        "404": { $ref: "#/components/responses/Introuvable" }
        "429": { $ref: "#/components/responses/TropDeRequetes" }
  /v1/tendances:
    get:
      summary: La comparaison par thème et les sujets qui montent
      operationId: tendances
      parameters:
        - name: categorie
          in: query
          description: Se limiter à un thème.
          schema: { type: string }
      responses:
        "200":
          description: Les thèmes (agrégats horaires) et les sujets en montée (marqueurs de 15 minutes)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Tendances" }
        "401": { $ref: "#/components/responses/NonAuthentifie" }
        "403": { $ref: "#/components/responses/AVenir" }
        "429": { $ref: "#/components/responses/TropDeRequetes" }
  /v1/ancrages:
    get:
      summary: Les racines quotidiennes du journal des VOX, avec leur état d'ancrage
      operationId: ancrages
      parameters:
        - name: limite
          in: query
          schema: { type: integer, minimum: 1, maximum: 90, default: 30 }
      responses:
        "200":
          description: Les racines, de la plus récente à la plus ancienne
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Ancrages" }
        "401": { $ref: "#/components/responses/NonAuthentifie" }
        "403": { $ref: "#/components/responses/AVenir" }
        "429": { $ref: "#/components/responses/TropDeRequetes" }
components:
  securitySchemes:
    cleApi:
      type: http
      scheme: bearer
      description: Clé d'API VoxPop (`vp_…`), aussi acceptée dans l'en-tête `X-API-Key`.
  responses:
    NonAuthentifie:
      description: Clé absente (`cle_manquante`), inconnue (`cle_invalide`) ou révoquée (`cle_revoquee`)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erreur" }
    AVenir:
      description: Offres payantes pas encore ouvertes (`a_venir`)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erreur" }
    Introuvable:
      description: Sondage inexistant, non public, sensible, retiré, ou sous 5 voix (`introuvable`)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erreur" }
    TropDeRequetes:
      description: Quota par minute (`trop_de_requetes`) ou par jour (`quota_epuise`) dépassé
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erreur" }
  schemas:
    Erreur:
      type: object
      required: [erreur, message]
      properties:
        erreur: { type: string }
        message: { type: string }
    ListeDeSondages:
      type: object
      required: [donnees, curseur, k]
      properties:
        donnees:
          type: array
          items: { $ref: "#/components/schemas/Sondage" }
        curseur: { type: string, nullable: true, description: "À repasser pour la page suivante ; null à la dernière page." }
        k: { type: integer, description: Plancher d'anonymat (5). }
    Sondage:
      type: object
      required: [id, titre, categorie, creeLe, clos, sponsorise, voix]
      properties:
        id: { type: string }
        titre: { type: string }
        categorie: { type: string }
        creeLe: { type: string, format: date-time, nullable: true }
        clos: { type: boolean }
        sponsorise: { type: boolean }
        voix: { type: integer, description: Nombre de voix, arrondi au multiple de 5. }
    ResultatsDeSondage:
      type: object
      required: [id, titre, categorie, creeLe, clos, voix, margeDErreur, k, questions]
      properties:
        id: { type: string }
        titre: { type: string }
        categorie: { type: string }
        creeLe: { type: string, format: date-time, nullable: true }
        clos: { type: boolean }
        voix: { type: integer }
        margeDErreur: { type: number, nullable: true, description: "Points de pourcentage, à 95 %, sur l'effectif arrondi." }
        k: { type: integer }
        questions:
          type: array
          items: { $ref: "#/components/schemas/Question" }
    Question:
      type: object
      required: [id, texte, voix, options]
      properties:
        id: { type: string }
        texte: { type: string }
        voix: { type: integer }
        options:
          type: array
          items: { $ref: "#/components/schemas/Option" }
        autres: { $ref: "#/components/schemas/Option" }
    Option:
      type: object
      properties:
        libelle: { type: string }
        voix: { type: integer }
        part: { type: integer, description: "Pourcentage entier, sur les comptes arrondis." }
    Tendances:
      type: object
      required: [categories, sujetsEnMontee, k]
      properties:
        categories:
          type: array
          items: { $ref: "#/components/schemas/Theme" }
        sujetsEnMontee:
          type: array
          items: { $ref: "#/components/schemas/SujetEnMontee" }
        k: { type: integer }
    Theme:
      type: object
      required: [categorie, disponible]
      properties:
        categorie: { type: string }
        disponible: { type: boolean }
        raison: { type: string, description: "Quand `disponible` est faux : pas_assez_de_sondages." }
        nbSondages: { type: integer }
        nettete: { type: object, description: "Médiane et quartiles de la part de l'option de tête (en %)." }
        partOui: { type: object }
        calculeLe: { type: string, format: date-time, nullable: true }
    SujetEnMontee:
      type: object
      required: [id, titre, categorie, voix, fenetreHeures]
      properties:
        id: { type: string }
        titre: { type: string }
        categorie: { type: string }
        voix: { type: integer }
        fenetreHeures: { type: integer }
    Ancrages:
      type: object
      required: [donnees]
      properties:
        donnees:
          type: array
          items: { $ref: "#/components/schemas/Ancrage" }
    Ancrage:
      type: object
      required: [jour, racine, racinePrecedente, ancrage]
      properties:
        jour: { type: string }
        racine: { type: string, nullable: true }
        racinePrecedente: { type: string, nullable: true }
        ancrage:
          type: object
          required: [etat]
          properties:
            etat: { type: string, description: "non_ancre tant que l'ancrage sur la chaîne n'est pas fait ; ancre avec son txHash ensuite." }
            txHash: { type: string }
