openapi: "3.0.3"
info:
  title: API Suppliers E-VTC v2
  version: "2.0"
  description: |
    API para proveedores (suppliers): recepción, gestión y ejecución de reservas
    subcontratadas. Modelo pull (polling). Ver GUIA-INTEGRACION.md para flujos,
    máquina de estados y política de reintentos.

    Los errores se discriminan por el campo `error` (código estable); `message` y
    `description` son texto para humanos y pueden cambiar sin aviso.
servers:
  - url: https://{dominio}/api-ext/v2
    variables:
      dominio:
        default: sandbox.example.com
        description: Dominio facilitado por la plataforma (sandbox o producción)

security:
  - bearerAuth: []

paths:
  /oauth/token:
    post:
      summary: Obtener token de acceso (OAuth2 client credentials)
      security:
        - basicAuth: []
      parameters:
        - name: grant_type
          in: query
          required: true
          schema: { type: string, enum: [client_credentials] }
      responses:
        "200":
          description: Token emitido
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token: { type: string }
                  token_type: { type: string, example: Bearer }
                  expires_in: { type: integer, example: 900, description: Segundos de validez }
        "401":
          $ref: "#/components/responses/AuthError"

  /suppliers/bookings:
    get:
      summary: Listar reservas asignadas (polling)
      parameters:
        - name: status
          in: query
          required: false
          description: CSV de estados a filtrar
          schema:
            type: string
            example: NEW,ACCEPTED,DRIVER_ASSIGNED,PENDING_AMENDMENT,PENDING_CANCELLATION,CANCELLED
        - name: pickUpDateFrom
          in: query
          schema: { type: string, format: date-time, description: "ISO-8601 UTC. Defecto: ayer 00:00" }
        - name: pickUpDateTo
          in: query
          schema: { type: string, format: date-time }
        - name: size
          in: query
          schema: { type: integer, default: 400, maximum: 500, minimum: 1 }
        - name: after
          in: query
          description: Cursor opaco de paginación. No construir a mano — usar el link `rel=next`.
          schema: { type: string }
      responses:
        "200":
          description: Página de reservas
          content:
            application/json:
              schema:
                type: object
                properties:
                  links:
                    type: array
                    items: { $ref: "#/components/schemas/Link" }
                    description: Seguir SIEMPRE el link con rel=next; su ausencia indica última página.
                  bookings:
                    type: array
                    items: { $ref: "#/components/schemas/Booking" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/AuthError" }

  /suppliers/bookings/{bookingReference}:
    get:
      summary: Detalle de una reserva
      parameters:
        - $ref: "#/components/parameters/bookingReference"
      responses:
        "200":
          description: |
            Reserva actual (incluye `state_hash` fresco). Una cancelación PENDIENTE cuya
            fila viva ya no existe también responde 200, con `status: PENDING_CANCELLATION`
            y `cancellationOrigin`, para que pueda confirmarse.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Booking" }
        "404":
          description: |
            Dos variantes distinguibles por `error`:
            - `BOOKING_NOT_FOUND`: referencia inexistente o ajena.
            - `BOOKING_NO_LONGER_AVAILABLE`: existió pero ya no está asignada; incluye
              `lastKnownStatus` y `cancellationOrigin` para el cierre idempotente.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              examples:
                noLongerAvailable:
                  value:
                    error: BOOKING_NO_LONGER_AVAILABLE
                    lastKnownStatus: CANCELLED
                    cancellationOrigin: SUPPLIER_REJECT
        "401": { $ref: "#/components/responses/AuthError" }

  /suppliers/bookings/{bookingReference}/responses:
    post:
      summary: Aceptar / rechazar (reserva, modificación o cancelación, según estado)
      description: |
        El significado de ACCEPT/REJECT depende del estado actual de la reserva
        (ver GUIA-INTEGRACION.md §6.1). `state_hash` se valida de forma estricta.
      parameters:
        - $ref: "#/components/parameters/bookingReference"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [state_hash, supplierResponse]
              properties:
                state_hash: { type: string, description: Hash del último GET }
                supplierResponse: { type: string, enum: [ACCEPT, REJECT] }
      responses:
        "201":
          description: Operación aplicada
          content:
            application/json:
              schema: { type: array, maxItems: 0, description: "Body vacío: []" }
        "400": { $ref: "#/components/responses/BadRequestConHash" }
        "401": { $ref: "#/components/responses/AuthError" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /suppliers/bookings/{bookingReference}/assign-driver:
    post:
      summary: Asignar conductor
      parameters:
        - $ref: "#/components/parameters/bookingReference"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [state_hash, driver]
              properties:
                state_hash: { type: string }
                driver:
                  type: object
                  required: [firstName, lastName, telephoneNumber]
                  properties:
                    firstName: { type: string }
                    lastName: { type: string }
                    telephoneNumber: { type: string, example: "+34600000000" }
                    photoURL: { type: string, format: uri }
      responses:
        "200":
          description: Conductor asignado
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  bookingReference: { type: integer }
                  message: { type: string, example: Driver assigned successfully. }
        "400": { $ref: "#/components/responses/BadRequestConHash" }
        "401": { $ref: "#/components/responses/AuthError" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /suppliers/bookings/{bookingReference}/assign-vehicle:
    post:
      summary: Asignar vehículo
      parameters:
        - $ref: "#/components/parameters/bookingReference"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [state_hash, vehicle]
              properties:
                state_hash: { type: string }
                vehicle:
                  type: object
                  required: [registration]
                  properties:
                    registration: { type: string, example: "1234ABC" }
                    make: { type: string }
                    model: { type: string }
                    colour: { type: string }
      responses:
        "200":
          description: Vehículo asignado
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  bookingReference: { type: integer }
                  message: { type: string, example: Vehicle assigned successfully. }
        "400": { $ref: "#/components/responses/BadRequestConHash" }
        "401": { $ref: "#/components/responses/AuthError" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /suppliers/bookings/{bookingReference}/driver/events:
    post:
      summary: Reportar evento de ejecución del conductor (incluye GPS)
      description: |
        Orden de la cadena: DEPARTED_TO_PICKUP → ARRIVED_AT_PICKUP → DEPARTED_TO_DROPOFF
        → ARRIVED_AT_DROPOFF. DRIVER_LIVE_LOCATION es repetible (recomendado cada 5-20 s
        entre DEPARTED y ARRIVED_AT_DROPOFF). El servidor avanza al estado más reciente
        reportado (los intermedios perdidos no bloquean) y nunca retrocede. `state_hash`
        se acepta con validación laxa: la progresión no se bloquea por hash desfasado.
      parameters:
        - $ref: "#/components/parameters/bookingReference"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event_type, occurred_at]
              properties:
                state_hash: { type: string }
                event_type:
                  type: string
                  enum:
                    - DRIVER_DEPARTED_TO_PICKUP
                    - DRIVER_ARRIVED_AT_PICKUP
                    - DRIVER_DEPARTED_TO_DROPOFF
                    - DRIVER_ARRIVED_AT_DROPOFF
                    - DRIVER_LIVE_LOCATION
                occurred_at:
                  type: string
                  description: "UTC estricto: YYYY-MM-DDTHH:MM:SSZ. No futuro. Incremental."
                  example: "2026-08-01T09:12:00Z"
                latitude: { type: number, example: 41.3874 }
                longitude: { type: number, example: 2.1686 }
      responses:
        "200":
          description: Evento registrado (o no-op idempotente si es repetido/inferior)
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string, example: Event sent successfully. }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/AuthError" }
        "404": { $ref: "#/components/responses/NotFound" }
        "405":
          description: Evento bloqueado por API (error EVENT_NOT_ALLOWED)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409": { $ref: "#/components/responses/Conflict" }

components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
    basicAuth: { type: http, scheme: basic, description: client_id / client_secret }

  parameters:
    bookingReference:
      name: bookingReference
      in: path
      required: true
      schema: { type: integer }

  schemas:
    Link:
      type: object
      properties:
        rel: { type: string, enum: [self, accept, reject, previous, next] }
        href: { type: string }
        type: { type: string, enum: [GET, POST] }

    Location:
      type: object
      properties:
        latitude: { type: string, example: "41.304459" }
        longitude: { type: string, example: "2.079780" }
        address: { type: string }
        type: { type: string, enum: [AIRPORT, STREET_ADDRESS, PORT, TRAIN_STATION] }

    Passenger:
      type: object
      properties:
        name: { type: string }
        telephone_number: { type: string }
        client_reference: { type: string }
        client_email: { type: string, nullable: true }

    Booking:
      type: object
      properties:
        links:
          type: array
          items: { $ref: "#/components/schemas/Link" }
        bookingReference: { type: integer }
        state_hash:
          type: string
          description: |
            Huella del CONTENIDO de la reserva. Cambia si y solo si cambian los datos;
            las transiciones de estado que inicia el proveedor no lo alteran.
            Obligatorio en todos los POST.
        status:
          type: string
          enum: [NEW, ACCEPTED, DRIVER_ASSIGNED, PENDING_AMENDMENT, PENDING_CANCELLATION, CANCELLED, COMPLETE]
        cancellationOrigin:
          type: string
          enum: [SUPPLIER_REJECT, SENDER_CANCELLED]
          description: Solo presente en cancelaciones. Ver GUIA-INTEGRACION.md §8.
        booked_date: { type: string, format: date-time }
        pickup_date_time: { type: string, format: date-time }
        pickup_date_time_zone: { type: string, example: Europe/Madrid }
        vehicle_type: { type: string, description: Código del tipo de servicio pactado }
        num_adults: { type: integer }
        num_children: { type: integer }
        num_babies: { type: integer }
        flight_number: { type: string, nullable: true }
        meet_and_greet: { type: boolean }
        meet_and_greet_message: { type: string }
        price:
          type: object
          properties:
            amount: { type: number }
            amountFormatted: { type: string }
            currency: { type: string, example: EUR }
        passenger: { $ref: "#/components/schemas/Passenger" }
        additional_comments: { type: string }
        pickup: { $ref: "#/components/schemas/Location" }
        dropoff: { $ref: "#/components/schemas/Location" }

    Error:
      type: object
      properties:
        message: { type: string, description: Texto para humanos — puede cambiar sin aviso }
        description: { type: string, description: Texto para humanos — puede cambiar sin aviso }
        error:
          type: string
          description: Código ESTABLE — discriminar siempre por este campo
          enum:
            - AUTHENTICATION_ERROR
            - INVALID_INPUT
            - INVALID_STATE_HASH
            - INVALID_PAGE_CURSOR
            - BOOKING_NOT_FOUND
            - BOOKING_NO_LONGER_AVAILABLE
            - BOOKING_STATUS_INCOMPATIBLE
            - AMENDMENT_PENDING
            - CANCELLATION_PENDING
            - DRIVER_NOT_ASSIGNED
            - INVALID_EVENT_DATE
            - EVENT_NOT_ALLOWED
            - BOOKING_ALREADY_COMPLETED
            - BOOKING_ALREADY_CANCELLED
            - CANCELLATION_FAILED
            - CONCURRENT_MODIFICATION
            - USER_NOT_CONFIGURED
            - SERVER_ERROR
        lastKnownStatus: { type: string, description: Solo en BOOKING_NO_LONGER_AVAILABLE }
        cancellationOrigin: { type: string, description: Solo en cancelaciones }
        estadoAnterior: { type: string, description: Solo en AMENDMENT_PENDING/CANCELLATION_PENDING }

  responses:
    AuthError:
      description: Token inválido o caducado → renovar y reintentar una vez
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            expired:
              value: { type: authentication_error, error: AUTHENTICATION_ERROR, errors: ["Expired JWT Token"] }
    BadRequest:
      description: Petición inválida (ver campo `error`)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BadRequestConHash:
      description: |
        Petición inválida. Caso especial `INVALID_STATE_HASH`: la reserva cambió después
        del último GET — re-GET, procesar cambios y reintentar con el hash nuevo.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            staleHash:
              value:
                message: Invalid Input parameter.
                description: Invalid state_hash for booking status.
                error: INVALID_STATE_HASH
    NotFound:
      description: BOOKING_NOT_FOUND o BOOKING_NO_LONGER_AVAILABLE (distinguir por `error`)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: Conflicto de estado (AMENDMENT_PENDING, CANCELLATION_PENDING, DRIVER_NOT_ASSIGNED, CONCURRENT_MODIFICATION…)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
