openapi: 3.0.3
info:
  title: SATF CONGO FLEET API
  description: |
    Passerelle SaaS multi-client entre vos systèmes et Wialon.

    Authentification par token Bearer (`Authorization: Bearer {token}`).
    Préfixe des tokens : `sc_` (configurable via `FLEET_API_TOKEN_PREFIX`).

    **Abilities :**
    - `fleet:read` — véhicules, positions, groupes, événements, modèles de rapports
    - `fleet:reports` — exécution et suivi de rapports async
    - `fleet:webhooks` — gestion des webhooks
    - `*` — accès complet
  version: "1.0.0"
  contact:
    name: SATF Congo Fleet Support

servers:
  - url: http://localhost:8000/api/v1
    description: Serveur API (configurable via FLEET_API_PUBLIC_URL)

tags:
  - name: Vehicles
    description: Véhicules du client
  - name: Positions
    description: Positions GPS synchronisées
  - name: Groups
    description: Groupes Wialon mappés
  - name: History
    description: Historique GPS en cache
  - name: Trips & Fuel
    description: Données issues des rapports Wialon
  - name: Sensors
    description: Capteurs véhicule
  - name: Events
    description: Événements flotte
  - name: ReportTemplates
    description: Modèles de rapports Wialon affectés aux groupes du client
  - name: Reports
    description: Rapports asynchrones
  - name: Webhooks
    description: Endpoints webhook client

security:
  - bearerAuth: []

paths:
  /vehicles:
    get:
      tags: [Vehicles]
      summary: Liste des véhicules
      description: Retourne les véhicules du client authentifié.
      operationId: listVehicles
      parameters:
        - name: active_only
          in: query
          schema:
            type: boolean
            default: true
          description: Filtrer uniquement les véhicules actifs
      responses:
        "200":
          description: Liste des véhicules
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Vehicle"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /vehicles/{vehicle}:
    get:
      tags: [Vehicles]
      summary: Détail d'un véhicule
      operationId: showVehicle
      parameters:
        - $ref: "#/components/parameters/VehicleId"
      responses:
        "200":
          description: Véhicule
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Vehicle"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /vehicles/positions:
    get:
      tags: [Positions]
      summary: Dernières positions de tous les véhicules actifs
      operationId: listVehiclePositions
      responses:
        "200":
          description: Positions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Position"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /vehicles/{vehicle}/position:
    get:
      tags: [Positions]
      summary: Dernière position d'un véhicule
      operationId: showVehiclePosition
      parameters:
        - $ref: "#/components/parameters/VehicleId"
      responses:
        "200":
          description: Position
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Position"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Véhicule introuvable ou position indisponible
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /groups:
    get:
      tags: [Groups]
      summary: Groupes Wialon mappés au client
      operationId: listGroups
      responses:
        "200":
          description: Groupes
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Group"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /groups/{group}/vehicles:
    get:
      tags: [Groups]
      summary: Véhicules d'un groupe
      operationId: listGroupVehicles
      parameters:
        - name: group
          in: path
          required: true
          schema:
            type: integer
          description: ID du mapping groupe Wialon
      responses:
        "200":
          description: Véhicules du groupe
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Vehicle"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /vehicles/{vehicle}/history:
    get:
      tags: [History]
      summary: Historique GPS en cache
      operationId: vehicleHistory
      parameters:
        - $ref: "#/components/parameters/VehicleId"
        - $ref: "#/components/parameters/FromDate"
        - $ref: "#/components/parameters/ToDate"
      responses:
        "200":
          description: Historique
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Position"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /vehicles/{vehicle}/trips:
    get:
      tags: [Trips & Fuel]
      summary: Trajets (rapport Wialon)
      operationId: vehicleTrips
      parameters:
        - $ref: "#/components/parameters/VehicleId"
        - $ref: "#/components/parameters/FromDate"
        - $ref: "#/components/parameters/ToDate"
        - $ref: "#/components/parameters/ReportTemplateId"
      responses:
        "200":
          description: Données trajets
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReportDataEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "502":
          description: Erreur Wialon
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WialonError"

  /vehicles/{vehicle}/fuel:
    get:
      tags: [Trips & Fuel]
      summary: Consommation carburant (rapport Wialon)
      operationId: vehicleFuel
      parameters:
        - $ref: "#/components/parameters/VehicleId"
        - $ref: "#/components/parameters/FromDate"
        - $ref: "#/components/parameters/ToDate"
        - $ref: "#/components/parameters/ReportTemplateId"
      responses:
        "200":
          description: Données carburant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReportDataEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "502":
          description: Erreur Wialon
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WialonError"

  /vehicles/{vehicle}/sensors:
    get:
      tags: [Sensors]
      summary: Capteurs d'un véhicule
      operationId: vehicleSensors
      parameters:
        - $ref: "#/components/parameters/VehicleId"
      responses:
        "200":
          description: Capteurs
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Sensor"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /events:
    get:
      tags: [Events]
      summary: Événements flotte
      operationId: listEvents
      parameters:
        - name: vehicle_id
          in: query
          schema:
            type: integer
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
        - name: per_page
          in: query
          schema:
            type: integer
            default: 50
      responses:
        "200":
          description: Événements paginés
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEvents"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /report-templates:
    get:
      tags: [ReportTemplates]
      summary: Catalogue des templates de rapports du client
      description: >
        Retourne les templates Wialon affectés aux groupes du client.
        Nécessite l'ability `fleet:read`.
        Utilisez `since` pour une synchronisation incrémentale locale.
      operationId: listReportTemplates
      parameters:
        - $ref: "#/components/parameters/SinceDate"
        - name: include
          in: query
          schema:
            type: string
          description: >
            Valeurs supportées : `wialon_data` (payload JSON Wialon complet, volumineux).
      responses:
        "200":
          description: Templates disponibles pour le client
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ReportTemplate"
                  meta:
                    $ref: "#/components/schemas/ReportTemplateCatalogMeta"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /report-templates/{template}:
    get:
      tags: [ReportTemplates]
      summary: Détail d'un template de rapport
      description: >
        Retourne un template Wialon accessible au client via une affectation groupe.
        Nécessite l'ability `fleet:read`.
      operationId: showReportTemplate
      parameters:
        - name: template
          in: path
          required: true
          schema:
            type: integer
          description: ID local du template (`wialon_report_templates.id`)
        - name: include
          in: query
          schema:
            type: string
          description: >
            Valeurs supportées : `wialon_data`.
      responses:
        "200":
          description: Template accessible au client
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ReportTemplate"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /groups/{group}/report-templates:
    get:
      tags: [ReportTemplates]
      summary: Templates de rapports d'un groupe
      description: >
        Retourne les affectations template ↔ groupe pour un mapping groupe client.
        Nécessite l'ability `fleet:read`.
      operationId: listGroupReportTemplates
      parameters:
        - name: group
          in: path
          required: true
          schema:
            type: integer
          description: ID du mapping groupe Wialon
        - $ref: "#/components/parameters/SinceDate"
        - name: include
          in: query
          schema:
            type: string
          description: >
            Valeurs supportées : `wialon_data`.
      responses:
        "200":
          description: Affectations templates du groupe
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/GroupReportTemplateBinding"
                  meta:
                    $ref: "#/components/schemas/GroupReportTemplateCatalogMeta"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /groups/{group}/reports:
    get:
      tags: [Reports]
      summary: Rapports stockés d'un groupe
      description: >
        Liste les rapports générés et persistés pour un groupe.
        Le payload complet (`result`) est disponible via `GET /reports/{id}`.
        Nécessite l'ability `fleet:reports`.
      operationId: listGroupReports
      parameters:
        - name: group
          in: path
          required: true
          schema:
            type: integer
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, running, completed, failed]
        - name: report_template_id
          in: query
          schema:
            type: integer
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
        - name: per_page
          in: query
          schema:
            type: integer
            default: 30
      responses:
        "200":
          description: Rapports paginés
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedReportJobs"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
    post:
      tags: [Reports]
      summary: Générer un rapport de groupe
      description: >
        Lance un rapport Wialon avec le `resource_id` / template Wialon du template local choisi.
        Pour un template de type `avl_unit` (`requires_unit: true`), fournir `wialon_unit_id` :
        `reportObjectId` sera alors l'ID Wialon de l'unité. Sinon `reportObjectId` = `wialon_group_id`.
        Le résultat est stocké côté serveur et rechargeable via `GET /reports/{id}`.
        Nécessite l'ability `fleet:reports`.
      operationId: executeGroupReport
      parameters:
        - name: group
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExecuteGroupReportRequest"
      responses:
        "202":
          description: Job rapport créé
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ReportJob"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /reports:
    get:
      tags: [Reports]
      summary: Rapports stockés du client
      description: >
        Liste paginée des rapports persistés. Utilisez `GET /reports/{id}` pour
        recharger le résultat complet.
        Nécessite l'ability `fleet:reports`.
      operationId: listReports
      parameters:
        - name: group_id
          in: query
          schema:
            type: integer
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, running, completed, failed]
        - name: report_template_id
          in: query
          schema:
            type: integer
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
        - name: per_page
          in: query
          schema:
            type: integer
            default: 30
      responses:
        "200":
          description: Rapports paginés
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedReportJobs"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /groups/{group}/reports/gateway:
    post:
      tags: [Reports]
      summary: Générer un rapport de groupe (IDs gateway)
      description: >
        Lance un rapport avec les IDs locaux gateway (`gateway_template_id`, `gateway_vehicle_id`).
        Le groupe est fourni dans l'URL (`wialon_group_mappings.id`).
        La réponse inclut `meta.resolved_wialon` avec les identifiants Wialon déduits.
        Nécessite l'ability `fleet:reports`.
      operationId: executeGroupReportViaGateway
      parameters:
        - name: group
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExecuteGroupGatewayReportRequest"
      responses:
        "202":
          description: Job rapport créé
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GatewayReportJobResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /reports/execute:
    post:
      tags: [Reports]
      summary: Lancer un rapport asynchrone
      description: >
        Deux modes : explicite (`report_template_id` + `group_id` + période + ressource unité ou groupe)
        ou legacy (`type` + `vehicle_id`). Nécessite l'ability `fleet:reports`.
      operationId: executeReport
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExecuteReportRequest"
      responses:
        "202":
          description: Job créé
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ReportJob"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /reports/execute/gateway:
    post:
      tags: [Reports]
      summary: Lancer un rapport asynchrone (IDs gateway)
      description: >
        Lance un rapport avec les IDs locaux gateway. La plateforme résout
        `gateway_group_id`, `gateway_template_id` et `gateway_vehicle_id`
        vers les identifiants Wialon correspondants.
        La réponse inclut `meta.resolved_wialon`.
        Nécessite l'ability `fleet:reports`.
      operationId: executeReportViaGateway
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExecuteGatewayReportRequest"
      responses:
        "202":
          description: Job créé
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GatewayReportJobResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /reports/{reportJob}:
    get:
      tags: [Reports]
      summary: Statut d'un rapport asynchrone
      description: Nécessite l'ability `fleet:reports`.
      operationId: showReportJob
      parameters:
        - $ref: "#/components/parameters/ReportJobId"
      responses:
        "200":
          description: Job rapport
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ReportJob"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /reports/{reportJob}/wialon-responses:
    get:
      tags: [Reports]
      summary: Archives JSON Wialon d'un rapport
      description: >
        Liste les réponses brutes Wialon archivées lors de l'exécution du job.
        Nécessite l'ability `fleet:reports`.
      operationId: listReportJobWialonResponses
      parameters:
        - $ref: "#/components/parameters/ReportJobId"
      responses:
        "200":
          description: Archives disponibles
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WialonResponseArchiveSummary"
                  meta:
                    type: object
                    properties:
                      report_job_id:
                        type: integer
                      count:
                        type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /reports/{reportJob}/wialon-responses/{archive}:
    get:
      tags: [Reports]
      summary: Contenu d'une archive Wialon
      description: >
        Retourne le résumé de l'archive et le payload JSON décodé.
        Nécessite l'ability `fleet:reports`.
      operationId: showReportJobWialonResponse
      parameters:
        - $ref: "#/components/parameters/ReportJobId"
        - name: archive
          in: path
          required: true
          schema:
            type: integer
          description: ID de l'archive Wialon
      responses:
        "200":
          description: Archive et payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      archive:
                        $ref: "#/components/schemas/WialonResponseArchiveSummary"
                      payload:
                        description: Payload JSON Wialon décodé
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /webhooks:
    get:
      tags: [Webhooks]
      summary: Liste des webhooks
      description: Nécessite l'ability `fleet:webhooks`.
      operationId: listWebhooks
      responses:
        "200":
          description: Endpoints webhook
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookEndpoint"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
    post:
      tags: [Webhooks]
      summary: Créer un webhook
      description: |
        Nécessite l'ability `fleet:webhooks`.
        Le champ `secret` n'est retourné qu'à la création.
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StoreWebhookRequest"
      responses:
        "201":
          description: Webhook créé
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/WebhookEndpoint"
                  secret:
                    type: string
                  message:
                    type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /webhooks/{webhookEndpoint}:
    delete:
      tags: [Webhooks]
      summary: Supprimer un webhook
      description: Nécessite l'ability `fleet:webhooks`.
      operationId: deleteWebhook
      parameters:
        - name: webhookEndpoint
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Supprimé
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Token
      description: Token client (préfixe sc_)

  parameters:
    VehicleId:
      name: vehicle
      in: path
      required: true
      schema:
        type: integer
      description: ID véhicule local
    FromDate:
      name: from
      in: query
      required: true
      schema:
        type: string
        format: date-time
      description: Début de période (ISO 8601)
    ToDate:
      name: to
      in: query
      required: true
      schema:
        type: string
        format: date-time
      description: Fin de période (ISO 8601, max 31 jours)
    ReportTemplateId:
      name: report_template_id
      in: query
      schema:
        type: integer
      description: >
        ID local du template catalogue (wialon_report_templates.id).
        Priorité sur l'affectation groupe / fallback .env.
    SinceDate:
      name: since
      in: query
      schema:
        type: string
        format: date-time
      description: >
        Synchronisation incrémentale : ne retourne que les templates
        ou affectations modifiés/synchronisés après cette date (ISO 8601).
    ReportJobId:
      name: reportJob
      in: path
      required: true
      schema:
        type: integer
      description: ID local du job rapport (`report_jobs.id`)

  schemas:
    Vehicle:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        plate_number:
          type: string
          nullable: true
        imei:
          type: string
          nullable: true
        is_active:
          type: boolean
        last_position_at:
          type: string
          format: date-time
          nullable: true
        metadata:
          type: object

    Position:
      type: object
      properties:
        vehicle_id:
          type: integer
        latitude:
          type: number
          format: double
        longitude:
          type: number
          format: double
        speed:
          type: number
          nullable: true
        course:
          type: number
          nullable: true
        altitude:
          type: number
          nullable: true
        satellites:
          type: integer
          nullable: true
        gps_time:
          type: string
          format: date-time
        received_at:
          type: string
          format: date-time

    Group:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        synced_at:
          type: string
          format: date-time
          nullable: true

    Sensor:
      type: object
      properties:
        id:
          type: integer
        vehicle_id:
          type: integer
        wialon_sensor_id:
          type: integer
        name:
          type: string
        type:
          type: string
          nullable: true
        value:
          type: string
          nullable: true
        unit:
          type: string
          nullable: true
        measured_at:
          type: string
          format: date-time
          nullable: true

    Event:
      type: object
      properties:
        id:
          type: integer
        vehicle_id:
          type: integer
        event_type:
          type: string
        severity:
          type: string
          nullable: true
        occurred_at:
          type: string
          format: date-time
        payload:
          type: object

    PaginatedEvents:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Event"
        links:
          type: object
        meta:
          type: object

    ReportDataEnvelope:
      type: object
      properties:
        vehicle_id:
          type: integer
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        data:
          type: array
          items:
            type: object

    ExecuteReportRequest:
      oneOf:
        - $ref: "#/components/schemas/ExecuteExplicitReportRequest"
        - $ref: "#/components/schemas/ExecuteLegacyReportRequest"

    ExecuteExplicitReportRequest:
      type: object
      required: [wialon_group_id, wialon_resource_id, wialon_template_id, from, to]
      properties:
        wialon_group_id:
          type: integer
          description: ID groupe Wialon (`GET /report-templates` → `groups[].wialon_group_id`)
        wialon_resource_id:
          type: integer
          description: ID ressource Wialon du template
        wialon_template_id:
          type: integer
          description: ID template Wialon dans la ressource
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        format:
          type: string
          enum: [json, pdf, csv]
          default: json
        label:
          type: string
          maxLength: 128
        wialon_unit_id:
          type: integer
          minimum: 1
          description: >
            Requis si le template a `requires_unit: true`. ID unité Wialon dans le groupe.

    ExecuteLegacyReportRequest:
      type: object
      required: [type, wialon_unit_id, from, to]
      properties:
        type:
          type: string
          enum: [trips, fuel, general]
        wialon_unit_id:
          type: integer
          minimum: 1
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        format:
          type: string
          enum: [json, pdf, csv]
          default: json

    ReportTemplateResource:
      type: object
      properties:
        wialon_resource_id:
          type: integer
          nullable: true
        name:
          type: string
          nullable: true

    ReportTemplateGroupBinding:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        wialon_group_id:
          type: integer
        report_purpose:
          type: string
          nullable: true
          enum: [trips, fuel, general]
        is_default:
          type: boolean

    ReportTemplate:
      type: object
      properties:
        id:
          type: integer
          description: ID local à passer dans `report_template_id` lors de `/reports/execute`
        name:
          type: string
        purpose:
          type: string
          nullable: true
          enum: [trips, fuel, general]
        wialon_template_id:
          type: integer
        template_type:
          type: string
          nullable: true
        requires_unit:
          type: boolean
          description: Si true, exécuter avec `wialon_unit_id` (template unité, ex. `avl_unit`)
        parameters:
          type: string
          nullable: true
        checksum:
          type: string
          nullable: true
        tables_count:
          type: integer
          nullable: true
        synced_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
        resource:
          $ref: "#/components/schemas/ReportTemplateResource"
        groups:
          type: array
          items:
            $ref: "#/components/schemas/ReportTemplateGroupBinding"
        wialon_data:
          type: object
          nullable: true
          description: Présent uniquement si `include=wialon_data`

    GroupReportTemplateBinding:
      type: object
      properties:
        binding_id:
          type: integer
        report_purpose:
          type: string
          nullable: true
          enum: [trips, fuel, general]
        is_default:
          type: boolean
        template:
          $ref: "#/components/schemas/ReportTemplate"

    ReportTemplateCatalogMeta:
      type: object
      properties:
        catalog_synced_at:
          type: string
          format: date-time
          nullable: true
        since:
          type: string
          format: date-time
          nullable: true
        count:
          type: integer

    GroupReportTemplateCatalogMeta:
      allOf:
        - $ref: "#/components/schemas/ReportTemplateCatalogMeta"
        - type: object
          properties:
            group_id:
              type: integer
            group_name:
              type: string
            wialon_group_id:
              type: integer

    ExecuteGroupReportRequest:
      type: object
      required: [wialon_resource_id, wialon_template_id, from, to]
      properties:
        wialon_resource_id:
          type: integer
          description: ID ressource Wialon du template
        wialon_template_id:
          type: integer
          description: ID template Wialon dans la ressource
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        format:
          type: string
          enum: [json, pdf, csv]
          default: json
        label:
          type: string
          maxLength: 128
          description: Libellé libre pour retrouver le rapport stocké
        wialon_unit_id:
          type: integer
          minimum: 1
          description: >
            Requis si le template a `requires_unit: true`. ID unité Wialon dans le groupe.

    ExecuteGatewayReportRequest:
      type: object
      required: [gateway_group_id, gateway_template_id, from, to]
      properties:
        gateway_group_id:
          type: integer
          description: ID mapping groupe local (`GET /groups` → `id`)
        gateway_template_id:
          type: integer
          description: ID template catalogue local (`GET /report-templates` → `id`)
        gateway_vehicle_id:
          type: integer
          minimum: 1
          description: >
            Requis si le template a `requires_unit: true`. ID véhicule local (`GET /vehicles` → `id`).
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        format:
          type: string
          enum: [json, pdf, csv]
          default: json
        label:
          type: string
          maxLength: 128

    ExecuteGroupGatewayReportRequest:
      type: object
      required: [gateway_template_id, from, to]
      properties:
        gateway_template_id:
          type: integer
          description: ID template catalogue local (`GET /report-templates` → `id`)
        gateway_vehicle_id:
          type: integer
          minimum: 1
          description: >
            Requis si le template a `requires_unit: true`. ID véhicule local.
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        format:
          type: string
          enum: [json, pdf, csv]
          default: json
        label:
          type: string
          maxLength: 128

    ResolvedWialonContext:
      type: object
      properties:
        wialon_group_id:
          type: integer
          nullable: true
        wialon_resource_id:
          type: integer
          nullable: true
        wialon_template_id:
          type: integer
          nullable: true
        wialon_unit_id:
          type: integer
          nullable: true

    GatewayReportJobResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/ReportJob"
        meta:
          type: object
          properties:
            resolved_wialon:
              $ref: "#/components/schemas/ResolvedWialonContext"

    PaginatedReportJobs:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ReportJob"
        links:
          type: object
        meta:
          type: object

    ReportJob:
      type: object
      properties:
        id:
          type: integer
        report_type:
          type: string
        status:
          type: string
          enum: [pending, running, completed, failed]
        label:
          type: string
          nullable: true
        vehicle_id:
          type: integer
          nullable: true
        wialon_unit_id:
          type: integer
          nullable: true
          description: ID Wialon de l'unité ciblée (rapports template unité)
        group_id:
          type: integer
          nullable: true
          description: ID mapping groupe local
        group_name:
          type: string
          nullable: true
        wialon_group_id:
          type: integer
          nullable: true
        report_template_id:
          type: integer
          nullable: true
        template_name:
          type: string
          nullable: true
        wialon_template_id:
          type: integer
          nullable: true
        wialon_resource_id:
          type: integer
          nullable: true
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        format:
          type: string
        result_available:
          type: boolean
        row_count:
          type: integer
          nullable: true
        result:
          nullable: true
          description: Présent sur `GET /reports/{id}` une fois terminé
        error_message:
          type: string
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true

    WialonResponseArchiveSummary:
      type: object
      properties:
        id:
          type: integer
        filename:
          type: string
        api_call:
          type: string
          nullable: true
        wialon_resource_id:
          type: integer
          nullable: true
        wialon_template_id:
          type: integer
          nullable: true
        byte_size:
          type: integer
        created_at:
          type: string
          format: date-time
          nullable: true

    StoreWebhookRequest:
      type: object
      required: [url, events]
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
        events:
          type: array
          minItems: 1
          items:
            type: string
            maxLength: 100

    WebhookEndpoint:
      type: object
      properties:
        id:
          type: integer
        url:
          type: string
        events:
          type: array
          items:
            type: string
        is_active:
          type: boolean
        created_at:
          type: string
          format: date-time

    Message:
      type: object
      properties:
        message:
          type: string

    ValidationError:
      type: object
      properties:
        message:
          type: string
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string

    WialonError:
      type: object
      properties:
        message:
          type: string
        error:
          type: string

  responses:
    Unauthorized:
      description: Token absent, invalide ou expiré
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Message"
    Forbidden:
      description: Compte inactif ou ability insuffisante
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Message"
    NotFound:
      description: Ressource introuvable ou hors périmètre client
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Message"
    ValidationError:
      description: Erreur de validation
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ValidationError"
    TooManyRequests:
      description: Rate limit dépassé
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Message"
