openapi: "3.0.3"
info:
  title: API Customers E-VTC v2
  version: "2.0"
  description: |
    API para agencias y clientes: tarificación, creación y gestión de sus propias
    reservas — traslados (transfer) y disposiciones por horas (disposal). Ver
    GUIA-INTEGRACION.md para flujos, reglas por tipo de reserva y manejo de errores.

    Los errores se discriminan por el código HTTP y el campo `message` (estable);
    `description` es texto para humanos y puede 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"

  /customers/clients:
    get:
      summary: Clientes asociados a la credencial
      description: |
        Con varios clientes, el campo `clientCode` pasa a ser obligatorio en la
        tarificación, los extras y la creación de reservas.
      responses:
        "200":
          description: Lista de clientes
          content:
            application/json:
              schema:
                type: object
                properties:
                  clients:
                    type: array
                    items:
                      type: object
                      properties:
                        code: { type: string, example: AGEN01 }
                        name: { type: string, example: Agencia Ejemplo }
                        taxId: { type: string, example: B12345678 }
        "401": { $ref: "#/components/responses/AuthError" }

  /customers/zones:
    get:
      summary: Todas las zonas de cobertura (solo aplican a traslados)
      responses:
        "200":
          description: Lista de zonas
          content:
            application/json:
              schema:
                type: object
                properties:
                  zones:
                    type: array
                    items: { $ref: "#/components/schemas/Zone" }
        "401": { $ref: "#/components/responses/AuthError" }

  /customers/zones/by-coordinates:
    get:
      summary: Zona que contiene unas coordenadas
      parameters:
        - name: latitude
          in: query
          required: true
          schema: { type: number, example: 38.2822 }
        - name: longitude
          in: query
          required: true
          schema: { type: number, example: -0.5581 }
      responses:
        "200":
          description: Zona encontrada
          content:
            application/json:
              schema:
                type: object
                properties:
                  zone: { $ref: "#/components/schemas/Zone" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "401": { $ref: "#/components/responses/AuthError" }

  /customers/zones/{code}:
    get:
      summary: Zona por código
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string, example: ALC }
      responses:
        "200":
          description: Zona encontrada
          content:
            application/json:
              schema:
                type: object
                properties:
                  zone: { $ref: "#/components/schemas/Zone" }
        "404": { $ref: "#/components/responses/NotFound" }
        "401": { $ref: "#/components/responses/AuthError" }

  /customers/search-results:
    post:
      summary: Tarificar un traslado
      description: |
        Devuelve un resultado por tipo de servicio con tarifa configurada entre las zonas
        de las coordenadas dadas. El `code` del resultado es el `serviceType` que se envía
        después al crear la reserva.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [origin, destination, passengers, pickupDateTime, pickupTimezone]
              properties:
                clientCode: { type: string, description: "Ver regla multi-cliente (obligatorio con varios clientes)" }
                origin: { $ref: "#/components/schemas/CoordinatesInput" }
                destination: { $ref: "#/components/schemas/CoordinatesInput" }
                passengers: { type: integer, example: 4 }
                pickupDateTime: { type: string, format: date-time, description: "UTC estricto", example: "2026-09-01T10:00:00Z" }
                pickupTimezone: { type: string, description: Zona IANA, example: Europe/Madrid }
      responses:
        "200":
          description: Tarifas disponibles
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items: { $ref: "#/components/schemas/TransferSearchResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/AuthError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404":
          description: "`Price not found.` — sin tarifa entre esas zonas (sin cobertura)"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /customers/search-results/disposal:
    post:
      summary: Tarificar una disposición (por horas)
      description: |
        El tipo va en la URL y no en el cuerpo: la disposición no pide origen ni destino
        porque su precio no depende de las zonas — solo del cliente, el tipo de servicio,
        la fecha y las horas contratadas.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [serviceType, disposal, pickupDateTime]
              properties:
                clientCode: { type: string }
                serviceType: { type: string, description: Código del tipo de servicio, example: "001" }
                disposal:
                  type: object
                  required: [contractedHours]
                  properties:
                    contractedHours: { type: integer, minimum: 1, example: 6 }
                pickupDateTime: { type: string, format: date-time, description: "UTC estricto", example: "2026-09-01T10:00:00Z" }
                pickupTimezone: { type: string, description: "Opcional y sin efecto (se admite por simetría)" }
      responses:
        "200":
          description: Precio con desglose
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DisposalPriceResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/AuthError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404":
          description: "`Price not found.` — sin tarifa de disposición para ese tipo de servicio y esas horas"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /customers/extras/by-service-type:
    post:
      summary: Extras compatibles con un tipo de servicio
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [serviceTypeId]
              properties:
                serviceTypeId: { type: integer, example: 9 }
                clientCode: { type: string }
      responses:
        "200":
          description: Extras disponibles (precio del cliente o precio base)
          content:
            application/json:
              schema:
                type: object
                properties:
                  extras:
                    type: array
                    items: { $ref: "#/components/schemas/ExtraCatalogItem" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/AuthError" }
        "404": { $ref: "#/components/responses/NotFound" }

  /customers/bookings:
    post:
      summary: Crear una reserva (traslado o disposición)
      description: |
        `journey.bookingType` ausente equivale a `transfer`. Reglas por tipo:
        - `transfer`: zonas por `zoneCode` o coordenadas (404 si el punto no cae en
          ninguna); admite `journey_return` (crea dos reservas asociadas).
        - `disposal`: requiere `disposal.contractedHours`; no resuelve zonas; rechaza
          `journey_return` con 400.
        La reserva se crea en estado `PRER`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [journey]
              properties:
                clientCode: { type: string }
                journey: { $ref: "#/components/schemas/JourneyInput" }
                journey_return:
                  allOf: [{ $ref: "#/components/schemas/JourneyInput" }]
                  description: Solo traslados. Crea una segunda reserva asociada.
      responses:
        "200":
          description: Reserva creada
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer, example: 12345, description: Referencia para leer/editar/cancelar }
                  return_id: { type: integer, description: Solo con journey_return }
                  message: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/AuthError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404":
          description: "`Locations not found.` — coordenadas de un traslado fuera de todas las zonas"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    get:
      summary: Listar reservas (paginación por cursor)
      parameters:
        - name: bookingType
          in: query
          required: false
          description: "CSV: transfer, disposal (minúsculas). Defecto: ambos."
          schema: { type: string, example: "transfer,disposal" }
        - name: status
          in: query
          required: false
          description: "CSV de códigos de estado. Defecto: todos los filtrables."
          schema: { type: string, example: "PRER,RESE,ASIG" }
        - 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/BookingEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/AuthError" }

  /customers/bookings/{bookingReference}:
    get:
      summary: Detalle de una reserva
      parameters:
        - $ref: "#/components/parameters/bookingReference"
      responses:
        "200":
          description: Reserva actual
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BookingEnvelope" }
        "404": { $ref: "#/components/responses/NotFound" }
        "401": { $ref: "#/components/responses/AuthError" }
    put:
      summary: Editar una reserva
      description: |
        Mismo payload que la creación. Restricciones:
        - Ventana de edición del cliente → 409 con la fecha límite en la description.
        - El tipo no cambia: `bookingType` distinto del guardado → 400. Sin `bookingType`
          se asume `transfer` — para editar una disposición hay que enviarlo explícito.
        - `journey_return` no se procesa: la vuelta se edita por su propio id.
      parameters:
        - $ref: "#/components/parameters/bookingReference"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [journey]
              properties:
                journey: { $ref: "#/components/schemas/JourneyInput" }
      responses:
        "200":
          description: Reserva editada
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  message: { type: string, example: Booking 12345 edited successfully. }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/AuthError" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
    delete:
      summary: Cancelar una reserva
      parameters:
        - $ref: "#/components/parameters/bookingReference"
      responses:
        "200":
          description: Cancelada
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  message: { type: string, example: The booking has been cancelled. }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: |
            Ya realizada, ya cancelada (tratable como éxito idempotente) o cancelación
            no aplicable — distinguible por la description.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/AuthError" }

  /customers/payments/initiate-data:
    post:
      summary: Iniciar un pago Stripe (PaymentIntent) para una reserva
      description: Requiere credencial con configuración de Stripe activa.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, journeyId]
              properties:
                amount: { type: number, example: 110.0 }
                journeyId: { type: integer, example: 12345 }
                description: { type: string }
      responses:
        "200":
          description: Datos del PaymentIntent (incluye public_key) para el SDK de Stripe
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  public_key: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/AuthError" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Authorization: Bearer <access_token> (TTL 900 s; ante 401, renovar y reintentar una vez)"
    basicAuth:
      type: http
      scheme: basic
      description: Solo para /oauth/token (client_id:client_secret)

  parameters:
    bookingReference:
      name: bookingReference
      in: path
      required: true
      description: El `id` devuelto al crear la reserva
      schema: { type: integer, example: 12345 }

  responses:
    AuthError:
      description: Token inválido o caducado — renovar y reintentar una vez
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BadRequest:
      description: "`Invalid Input parameter.` — la description enumera cada campo erróneo"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: "`Forbidden` — clientCode o recurso que no pertenece a la credencial"
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Recurso inexistente o ajeno a la credencial
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: Operación no aplicable al estado actual (la description da el detalle)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      properties:
        message: { type: string, description: Código estable para discriminar, example: Invalid Input parameter. }
        description: { type: string, description: Texto para humanos — puede cambiar sin aviso }

    Link:
      type: object
      properties:
        rel: { type: string, example: next }
        href: { type: string }
        type: { type: string, example: GET }

    Zone:
      type: object
      properties:
        code: { type: string, example: ALC }
        name: { type: string, example: Aeropuerto de Alicante }
        type: { type: string, enum: [AIRPORT, STREET_ADDRESS] }
        airportCode: { type: string, description: Solo aeropuertos, example: ALC }
        area:
          type: object
          nullable: true
          description: Geometría (polígono / círculo / rectángulo / multiárea)

    CoordinatesInput:
      type: object
      required: [latitude, longitude]
      properties:
        latitude: { type: number, example: 38.2822 }
        longitude: { type: number, example: -0.5581 }

    PriceBlock:
      type: object
      description: |
        Bloque de precio de las tarificaciones. salePrice conserva hasta 4 decimales (la
        precisión de las tarifas); salePriceWithVat se redondea a 2 (lo que se cobra).
        Cliente exento de IVA → vatPercentage 0 y ambos importes coinciden.
      properties:
        salePrice: { type: number, example: 100.0 }
        priceFormattedWithSymbol: { type: string, example: "100,00 €" }
        currency: { type: string, example: EUR }
        vatPercentage: { type: number, example: 10 }
        salePriceWithVat: { type: number, example: 110.0 }
        salePriceWithVatFormatted: { type: string, example: "110,00 €" }

    TransferSearchResult:
      type: object
      properties:
        id: { type: integer, example: 9 }
        code: { type: string, example: "001", description: Es el serviceType para crear la reserva }
        transportCategory: { type: string, example: Sedan }
        transportDescription: { type: string, nullable: true }
        transportImage: { type: string, nullable: true }
        price: { $ref: "#/components/schemas/PriceBlock" }
        maxPassengers: { type: integer, example: 4 }
        maxSuitcases: { type: integer, example: 4 }
        features:
          type: array
          items:
            type: object
            properties:
              name: { type: string, example: Silla de bebé }
              value: { type: integer, example: 2, description: Cantidad máxima aceptada }

    DisposalPriceResult:
      type: object
      properties:
        serviceType:
          type: object
          properties:
            id: { type: integer, example: 9 }
            code: { type: string, example: "001" }
            transportCategory: { type: string, example: Sedan }
        disposal:
          type: object
          description: |
            Desglose del precio anunciado. Con tarifa plana sin paquete, packageHours y
            extraHours son null. includedKm es el TOTAL de km incluidos — no multiplicar
            por horas.
          properties:
            contractedHours: { type: integer, example: 6 }
            packageHours: { type: integer, nullable: true, example: 4 }
            packagePrice: { type: number, nullable: true, example: 80.0 }
            extraHours: { type: integer, nullable: true, example: 2 }
            extraHourPrice: { type: number, nullable: true, example: 10.0 }
            includedKm: { type: number, nullable: true, example: 120.0 }
            extraKmPrice: { type: number, nullable: true, example: 0.5 }
        price: { $ref: "#/components/schemas/PriceBlock" }

    ExtraCatalogItem:
      type: object
      properties:
        id: { type: integer, example: 3 }
        name: { type: string, example: Silla de bebé }
        description: { type: string, nullable: true }
        icon: { type: string, nullable: true, description: SVG inline }
        price:
          type: object
          properties:
            amount: { type: number, example: 6.05, description: Con IVA }
            amountFormatted: { type: string, example: "6,05 €" }
            amountWithoutVat: { type: number, example: 5.5 }
            amountWithoutVatFormatted: { type: string, example: "5,50 €" }
            currency: { type: string, example: EUR }
            vatPercentage: { type: number, example: 10 }
        maxQuantity: { type: integer, example: 2 }

    JourneyInput:
      type: object
      required: [reference, serviceType, dateTime, timezone, price, passengers, origin, destination]
      properties:
        bookingType:
          type: string
          enum: [transfer, disposal]
          default: transfer
          description: Ausente equivale a transfer. En la edición debe coincidir con el tipo guardado.
        reference: { type: string, example: MI-REF-001, description: Vuestra referencia (viaja con la reserva; no sirve para buscarla) }
        serviceType: { type: string, example: "001", description: Código del tipo de servicio (el `code` de la tarificación) }
        dateTime: { type: string, format: date-time, description: Recogida en UTC estricto }
        timezone: { type: string, example: Europe/Madrid, description: Zona IANA del punto de recogida }
        price:
          type: object
          required: [amount]
          properties:
            amount: { type: number, example: 45.0, description: Importe sin IVA — usad el salePrice de la tarificación }
            currency: { type: string, example: EUR }
        passengers:
          type: object
          required: [totalNumber]
          properties:
            totalNumber: { type: integer, example: 4 }
            numberOfAdults: { type: integer }
            numberOfChildren: { type: integer }
            numberOfInfants: { type: integer }
            childrenAges:
              type: array
              items: { type: integer }
              description: Se anexa a las observaciones del servicio
        origin: { $ref: "#/components/schemas/EndpointInput" }
        destination:
          allOf: [{ $ref: "#/components/schemas/EndpointInput" }]
          description: Obligatorio también en disposiciones (usad el punto de recogida si no hay otro)
        disposal:
          type: object
          description: Obligatorio con bookingType disposal
          properties:
            contractedHours: { type: integer, minimum: 1, example: 6 }
        flight:
          type: object
          properties:
            flightNumber: { type: string, nullable: true, example: FR2835 }
            flightDateTime: { type: string, format: date-time, nullable: true, description: UTC estricto }
        traveler:
          type: object
          properties:
            name: { type: string }
            phone: { type: string }
            email: { type: string }
            documentNumber: { type: string }
        extras:
          type: array
          description: Solo id y quantity — el precio lo pone la plataforma
          items:
            type: object
            required: [id]
            properties:
              id: { type: integer, example: 3 }
              quantity: { type: integer, minimum: 1, default: 1 }
        numberOfSuitcases: { type: integer, default: 0 }
        observations: { type: string }
        travelerPays: { type: boolean, default: false }
        amountToChargeDriver: { type: number, default: 0 }
        usesHighway: { type: boolean, default: false, description: Solo traslados }

    EndpointInput:
      type: object
      required: [address, latitude, longitude]
      properties:
        address: { type: string, example: Aeropuerto de Alicante (ALC) }
        latitude: { type: number, example: 38.2822 }
        longitude: { type: number, example: -0.5581 }
        zoneCode:
          type: string
          description: |
            Solo traslados. Si se envía, la zona se resuelve por código (404 si no
            existe); si no, por las coordenadas (404 si no caen en ninguna zona).

    BookingEnvelope:
      type: object
      properties:
        journey: { $ref: "#/components/schemas/Booking" }

    Booking:
      type: object
      description: |
        El bloque `disposal` solo aparece en disposiciones; `usesHighway` es null en
        ellas y `zone` en origen/destino solo aparece en traslados. `vehicle` y `driver`
        se rellenan cuando la plataforma asigna el servicio, también cuando lo ejecuta
        un proveedor colaborador.
      properties:
        links:
          type: array
          items: { $ref: "#/components/schemas/Link" }
          description: self (GET) y cancel (DELETE)
        id: { type: integer, example: 12345 }
        reference: { type: string, nullable: true, example: MI-REF-001 }
        status:
          type: string
          enum: [PRER, RESE, ASIG, ENTR, ENPO, ABOR, REAL, ANUL]
          description: |
            PRER pre-reservada (inicial por API) · RESE reservada · ASIG asignada ·
            ENTR conductor en camino · ENPO en posición · ABOR pasajero a bordo ·
            REAL realizada (final) · ANUL anulada (final)
        bookingType: { type: string, example: Traslado, description: Nombre del tipo de reserva }
        disposal:
          type: object
          description: Solo disposiciones. Los *Completed los fija la plataforma al cerrar el servicio.
          properties:
            contractedHours: { type: integer, example: 6 }
            hoursCompleted: { type: integer, nullable: true }
            minutesCompleted: { type: integer, nullable: true }
            kmCompleted: { type: integer, nullable: true }
        serviceType: { type: integer, example: 9, description: Id del tipo de servicio }
        dateTime: { type: string, format: date-time }
        timeZone: { type: string, example: Europe/Madrid }
        origin: { $ref: "#/components/schemas/BookingEndpoint" }
        destination: { $ref: "#/components/schemas/BookingEndpoint" }
        price:
          type: object
          properties:
            amount: { type: number, nullable: true, example: 45.0 }
            amountFormatted: { type: string, nullable: true, example: "45,00 €" }
            currency: { type: string, example: EUR }
        flight:
          type: object
          properties:
            flightNumber: { type: string, nullable: true }
            flightDateTime: { type: string, nullable: true }
        traveler:
          type: object
          properties:
            name: { type: string, nullable: true }
            phone: { type: string, nullable: true }
            email: { type: string, nullable: true }
        passengers:
          type: object
          properties:
            totalNumber: { type: integer }
            numberOfAdults: { type: integer }
            numberOfChildren: { type: integer }
            numberOfInfants: { type: integer }
        numberOfSuitcases: { type: integer, nullable: true }
        travelerPays: { type: boolean, nullable: true }
        amountToChargeDriver: { type: number, nullable: true }
        usesHighway: { type: boolean, nullable: true, description: null en disposiciones }
        distanceMeters:
          type: integer
          nullable: true
          description: |
            Distancia estimada del trayecto. En el listado puede ser null si aún no está
            calculada; el detalle la calcula siempre que sea posible.
        durationSeconds:
          type: integer
          nullable: true
          description: |
            Duración estimada. En una disposición, las horas contratadas en segundos.
            En el listado puede ser null; el detalle la calcula siempre que sea posible.
        trackingUrl:
          type: string
          nullable: true
          description: |
            Enlace de seguimiento en vivo para el viajero, si el servicio dispone de él.
            En el detalle se incluye siempre que el seguimiento esté disponible; en el
            listado solo si ya se había generado. Puede caducar al terminar el servicio.
        operator:
          type: object
          description: |
            Proveedor colaborador que ejecuta el servicio. Presente SOLO si la credencial
            tiene habilitada la compartición de datos del colaborador y el servicio lo
            ejecuta uno: con operativa directa la clave no aparece. Sin datos fiscales
            ni económicos.
          properties:
            name: { type: string, example: Transportes Ejemplo SL }
            phone: { type: string, nullable: true, example: "+34600111222" }
            email: { type: string, nullable: true, example: operaciones@ejemplo.com }
        observations: { type: string }
        extras:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              name: { type: string }
              quantity: { type: integer }
              unitPrice: { type: number }
              unitPriceFormatted: { type: string }
              totalPrice: { type: number }
              totalPriceFormatted: { type: string }
              currency: { type: string }
        vehicle:
          type: object
          properties:
            code: { type: string, nullable: true }
            registrationNumber: { type: string, nullable: true }
        driver:
          type: object
          properties:
            name: { type: string, nullable: true }
            phone: { type: string, nullable: true }

    BookingEndpoint:
      type: object
      properties:
        latitude: { type: string, example: "38.2822" }
        longitude: { type: string, example: "-0.5581" }
        address: { type: string }
        type: { type: string, enum: [AIRPORT, STREET_ADDRESS], description: Solo traslados }
        zone:
          type: object
          description: Solo traslados
          properties:
            code: { type: string, example: ALC }
            name: { type: string }
