openapi: 3.1.0
info:
  title: Edge Journeys API
  version: "1.0"
  description: |
    Tenant-facing booking API for Edge Journeys — catalog, baskets, pricing,
    promotions, payments, and checkout.

    Every request is tenant-scoped: pass `tenantId` as a query parameter on
    GET/DELETE requests and as a body field on POST/PUT/PATCH requests.
    Successful responses wrap payloads in a `data` envelope.

    Human-readable documentation: https://edge-docs.kaptio.com/edge-journeys/api-reference
  contact:
    name: Kaptio Edge Platform
    url: https://edge-docs.kaptio.com
servers:
  - url: https://journeys.edge.staging.kaptioapis.com/api/journeys/v1
    description: Staging
  - url: https://journeys.edge.production.kaptioapis.com/api/journeys/v1
    description: Production
  - url: "{customDomain}/api/journeys/v1"
    description: Tenant custom booking domain (same-origin)
    variables:
      customDomain:
        default: https://book.example.com
tags:
  - name: Catalog
  - name: Configuration
  - name: Advisors
  - name: Baskets
  - name: Promotions
  - name: Pricing & Payments
  - name: Checkout
  - name: Health

paths:
  /config:
    get:
      tags: [Configuration]
      operationId: getJourneyConfig
      summary: Get the tenant's validated journey configuration
      parameters:
        - $ref: "#/components/parameters/tenantIdQuery"
      responses:
        "200":
          description: The tenant's journey.yaml, validated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/JourneyConfig"
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/TenantNotFound" }

  /catalog/voyages:
    get:
      tags: [Catalog]
      operationId: listVoyages
      summary: List voyages in the tenant catalog with departures and from-prices
      parameters:
        - $ref: "#/components/parameters/tenantIdQuery"
      responses:
        "200":
          description: Voyages
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Voyage" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/TenantNotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /catalog/trips:
    get:
      tags: [Catalog]
      operationId: listTrips
      summary: List trips in the tenant catalog with departures and from-prices
      description: >-
        Product-neutral dialect of `/catalog/voyages`: `tripCode`/`tripName`
        instead of `voyageCode`/`voyageName`, and `vesselName` present only
        when the product is operated on a vessel.
      parameters:
        - $ref: "#/components/parameters/tenantIdQuery"
      responses:
        "200":
          description: Trips
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Trip" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/TenantNotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /catalog/categories:
    get:
      tags: [Catalog]
      operationId: listCabinCategories
      summary: List cabin categories grouped into configured tiers
      parameters:
        - $ref: "#/components/parameters/tenantIdQuery"
      responses:
        "200":
          description: Cabin categories
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CabinCategory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/TenantNotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /catalog/cabins:
    get:
      tags: [Catalog]
      operationId: listCabins
      summary: List bookable cabins for a voyage and category with live availability
      parameters:
        - $ref: "#/components/parameters/tenantIdQuery"
        - name: voyageCode
          in: query
          required: true
          schema: { type: string }
        - name: categoryKaptioName
          in: query
          required: true
          schema: { type: string }
        - name: currency
          in: query
          required: true
          schema: { type: string, description: ISO 4217 currency code }
      responses:
        "200":
          description: Cabins with selection context
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Cabin" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/TenantNotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /catalog/rooms:
    get:
      tags: [Catalog]
      operationId: listRooms
      summary: List bookable room options for a trip and category
      description: >-
        Product-neutral dialect of `/catalog/cabins`: `roomCode` instead of
        `cabinNumber`, `deck` present only for vessel products, and a
        `tripCode` query parameter.
      parameters:
        - $ref: "#/components/parameters/tenantIdQuery"
        - name: tripCode
          in: query
          required: true
          schema: { type: string }
        - name: categoryKaptioName
          in: query
          required: true
          schema: { type: string }
        - name: currency
          in: query
          required: true
          schema: { type: string, description: ISO 4217 currency code }
      responses:
        "200":
          description: Room options with selection context
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Room" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/TenantNotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /catalog/activities:
    get:
      tags: [Catalog]
      operationId: listActivities
      summary: List optional add-on activities for a voyage
      parameters:
        - $ref: "#/components/parameters/tenantIdQuery"
        - name: voyageCode
          in: query
          required: true
          schema: { type: string }
        - name: currency
          in: query
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Activities
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Activity" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/TenantNotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /advisors/lookup:
    post:
      tags: [Advisors]
      operationId: lookupAdvisor
      summary: Identify a travel advisor by IATA number and email (B2B channels)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenantId, iata_number, email]
              properties:
                tenantId: { type: string }
                iata_number: { type: string }
                email: { type: string, format: email }
      responses:
        "200":
          description: Lookup result
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/AdvisorLookupResult"
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/TenantNotFound" }

  /baskets:
    post:
      tags: [Baskets]
      operationId: createBasket
      summary: Create a basket for a package departure
      description: |
        Starts a commerce session. Baskets expire 15 minutes after creation;
        inventory holds placed during selection carry their own expiry.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - type: object
                  required: [tenantId]
                  properties:
                    tenantId: { type: string }
                - $ref: "#/components/schemas/CreateBasketRequest"
      responses:
        "201":
          description: Basket created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Basket" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/TenantNotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }
        "503": { $ref: "#/components/responses/LiveConfigError" }

  /baskets/{basketId}:
    get:
      tags: [Baskets]
      operationId: getBasket
      summary: Get the current basket state
      parameters:
        - $ref: "#/components/parameters/basketId"
        - $ref: "#/components/parameters/tenantIdQuery"
      responses:
        "200":
          description: Basket
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Basket" }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Baskets]
      operationId: deleteBasket
      summary: Delete (abandon) a basket
      parameters:
        - $ref: "#/components/parameters/basketId"
        - $ref: "#/components/parameters/tenantIdQuery"
      responses:
        "204": { description: Deleted }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }

  /baskets/{basketId}/components:
    patch:
      tags: [Baskets]
      operationId: updateComponents
      summary: Update option selections on basket components (e.g. select a cabin)
      parameters:
        - $ref: "#/components/parameters/basketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenantId, components]
              properties:
                tenantId: { type: string }
                components:
                  type: array
                  items: { $ref: "#/components/schemas/UpdateComponentsRequest" }
      responses:
        "200":
          description: Updated components
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/BasketComponent" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /baskets/{basketId}/travellers:
    put:
      tags: [Baskets]
      operationId: updateTravellers
      summary: Replace the traveller list
      parameters:
        - $ref: "#/components/parameters/basketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenantId, travellers]
              properties:
                tenantId: { type: string }
                travellers:
                  type: array
                  items: { $ref: "#/components/schemas/Traveller" }
      responses:
        "200":
          description: Updated travellers
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Traveller" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }

  /baskets/{basketId}/promotions:
    post:
      tags: [Promotions]
      operationId: applyPromotion
      summary: Apply a promotion code
      parameters:
        - $ref: "#/components/parameters/basketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenantId, code]
              properties:
                tenantId: { type: string }
                code: { type: string }
      responses:
        "200":
          description: Updated basket
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Basket" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /baskets/{basketId}/promotions/{code}:
    delete:
      tags: [Promotions]
      operationId: removePromotion
      summary: Remove a promotion code
      parameters:
        - $ref: "#/components/parameters/basketId"
        - name: code
          in: path
          required: true
          schema: { type: string }
        - $ref: "#/components/parameters/tenantIdQuery"
      responses:
        "200":
          description: Updated basket
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Basket" }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }

  /baskets/{basketId}/external-ancillaries/{providerId}/quotes:
    post:
      tags: [Baskets]
      operationId: quoteExternalAncillaries
      summary: Quote external services from a configured tenant provider
      parameters:
        - $ref: "#/components/parameters/basketId"
        - $ref: "#/components/parameters/providerId"
        - $ref: "#/components/parameters/tenantIdQuery"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ExternalAncillaryQuoteInput" }
      responses:
        "201":
          description: Normalized, signed external ancillary quote
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ExternalAncillaryQuote" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /baskets/{basketId}/external-ancillaries/{providerId}/selections:
    post:
      tags: [Baskets]
      operationId: selectExternalAncillary
      summary: Select a signed external ancillary quote item
      parameters:
        - $ref: "#/components/parameters/basketId"
        - $ref: "#/components/parameters/providerId"
        - $ref: "#/components/parameters/tenantIdQuery"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ExternalAncillarySelectionInput" }
      responses:
        "201":
          description: Normalized selection and opaque signed selection token
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      selection: { $ref: "#/components/schemas/ExternalAncillarySelection" }
                      selection_token: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }

  /baskets/{basketId}/prices:
    get:
      tags: [Pricing & Payments]
      operationId: getPricing
      summary: Calculate pricing for the current basket state
      parameters:
        - $ref: "#/components/parameters/basketId"
        - $ref: "#/components/parameters/tenantIdQuery"
        - name: externalAncillaryToken
          in: query
          required: false
          description: Repeat for each signed external ancillary selection to include in presentation pricing.
          schema:
            type: array
            items: { type: string }
      responses:
        "200":
          description: Pricing breakdown
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/BasketPricing" }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /baskets/{basketId}/payment-schedules:
    get:
      tags: [Pricing & Payments]
      operationId: getPaymentSchedules
      summary: Get deposit and balance schedule options
      parameters:
        - $ref: "#/components/parameters/basketId"
        - $ref: "#/components/parameters/tenantIdQuery"
      responses:
        "200":
          description: Payment schedules
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/PaymentSchedule" }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /baskets/{basketId}/payment-requests:
    post:
      tags: [Pricing & Payments]
      operationId: createPaymentRequest
      summary: Create a payment request against a chosen schedule
      parameters:
        - $ref: "#/components/parameters/basketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - type: object
                  required: [tenantId]
                  properties:
                    tenantId: { type: string }
                - $ref: "#/components/schemas/PaymentRequestInput"
      responses:
        "201":
          description: Payment request created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PaymentRequest" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /baskets/{basketId}/payment-requests/{paymentRequestId}/embed-session:
    post:
      tags: [Pricing & Payments]
      operationId: createEmbedSession
      summary: Create an Edge Pay hosted checkout session
      parameters:
        - $ref: "#/components/parameters/basketId"
        - $ref: "#/components/parameters/paymentRequestId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenantId, payment_option]
              properties:
                tenantId: { type: string }
                payment_option: { type: string, enum: [deposit, full] }
              additionalProperties: false
      responses:
        "201":
          description: Edge Pay embed session with optional method-specific surcharge previews
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/EmbedSession" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /baskets/{basketId}/payment-requests/{paymentRequestId}/complete:
    put:
      tags: [Pricing & Payments]
      operationId: completePayment
      summary: Mark a payment request complete after provider confirmation
      parameters:
        - $ref: "#/components/parameters/basketId"
        - $ref: "#/components/parameters/paymentRequestId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenantId, session_id]
              properties:
                tenantId: { type: string }
                session_id: { type: string }
              additionalProperties: false
      responses:
        "200":
          description: Payment request completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PaymentRequest" }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /baskets/{basketId}/bookings:
    post:
      tags: [Checkout]
      operationId: createBooking
      summary: Convert the basket into a booking (Salesforce itinerary)
      parameters:
        - $ref: "#/components/parameters/basketId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenantId]
              properties:
                tenantId: { type: string }
                external_ancillary_selection_tokens:
                  type: array
                  maxItems: 50
                  items: { type: string }
              additionalProperties: false
      responses:
        "201":
          description: Booking created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/BookingResult" }
        "403": { $ref: "#/components/responses/TenantMismatch" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Basket already booked, or a required payment is not completed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422":
          description: Basket has no component selections (no cabin chosen)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "501":
          description: External ancillary persistence is not configured
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "502": { $ref: "#/components/responses/UpstreamError" }

  /health-check:
    get:
      tags: [Health]
      operationId: healthCheck
      summary: API-level health check
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  api: { type: string, example: journeys-v1 }

components:
  parameters:
    tenantIdQuery:
      name: tenantId
      in: query
      required: true
      schema: { type: string }
      description: Tenant slug. Required on every request.
    basketId:
      name: basketId
      in: path
      required: true
      schema: { type: string }
    paymentRequestId:
      name: paymentRequestId
      in: path
      required: true
      schema: { type: string }
    providerId:
      name: providerId
      in: path
      required: true
      schema: { type: string, pattern: "^[a-z0-9-]+$" }

  responses:
    BadRequest:
      description: Missing or invalid parameters
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Basket or payment request not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    TenantNotFound:
      description: Tenant not configured
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    TenantMismatch:
      description: Basket belongs to a different tenant
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    UpstreamError:
      description: An upstream platform service returned an error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    LiveConfigError:
      description: The tenant's live configuration is incomplete
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
      required: [error]

    JourneyConfig:
      type: object
      description: Validated journey.yaml. See https://edge-docs.kaptio.com/edge-journeys/configuration
      properties:
        tenant_id: { type: string }
        journey_id: { type: string }
        channels:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              label: { type: string }
              type: { type: string, enum: [b2c, b2b] }
              requires_advisor_id: { type: boolean }
        steps:
          type: array
          items: { type: string }
        branding: { type: object }
        behavior: { type: object }
        catalog: { type: object }
        provider: { type: string, enum: [mock, live] }

    Voyage:
      type: object
      properties:
        voyageCode: { type: string }
        voyageName: { type: string }
        region: { type: string }
        durationDays: { type: number }
        shipName: { type: string }
        departureDates:
          type: array
          items:
            type: object
            properties:
              label: { type: string }
              date: { type: string, format: date }
        fromPrice:
          type: [number, "null"]
          description: Lowest lead-in price. Null when no valid price resolved — never 0.
        currency: { type: string }
        fromPriceUSD: { type: [number, "null"] }
        fromPriceAUD: { type: [number, "null"] }
        fromPriceGBP: { type: [number, "null"] }
        fromPriceEUR: { type: [number, "null"] }
        fromPriceNZD: { type: [number, "null"] }
        fromPriceCAD: { type: [number, "null"] }
        sellingStatus: { type: string }
        voyageSeason: { type: string }

    Trip:
      type: object
      description: >-
        Product-neutral trip row served by /catalog/trips. Same data as
        Voyage with neutral field names; vesselName is present only when the
        product is operated on a vessel.
      properties:
        tripCode: { type: string }
        tripName: { type: string }
        region: { type: string }
        durationDays: { type: number }
        vesselName:
          type: string
          description: Present only for vessel-operated products.
        departureDates:
          type: array
          items:
            type: object
            properties:
              label: { type: string }
              date: { type: string, format: date }
        fromPrice:
          type: [number, "null"]
          description: Lowest lead-in price. Null when no valid price resolved — never 0.
        currency: { type: string }
        fromPriceUSD: { type: [number, "null"] }
        fromPriceAUD: { type: [number, "null"] }
        fromPriceGBP: { type: [number, "null"] }
        fromPriceEUR: { type: [number, "null"] }
        fromPriceNZD: { type: [number, "null"] }
        fromPriceCAD: { type: [number, "null"] }
        sellingStatus: { type: string }
        season: { type: string }

    CabinCategory:
      type: object
      properties:
        kaptioName: { type: string }
        label: { type: string }
        tier: { type: string }
        tierGroup: { type: string }
        isSolo: { type: boolean }
        isPopular: { type: boolean }
        sortOrder: { type: number }

    Cabin:
      type: object
      properties:
        id:
          oneOf: [{ type: number }, { type: string }]
        cabinNumber: { type: string }
        deck: { type: number }
        location: { type: string }
        priceAmount: { type: number }
        priceCurrency: { type: string }
        priceBasis: { type: string }
        berthsAvailable: { type: number }
        componentId:
          type: string
          description: Selection context — pass back verbatim in ComponentSelection
        optionId: { type: number }
        itemOptionId: { type: string }
        cabinTypeId: { type: string }

    Room:
      type: object
      description: >-
        Product-neutral room row served by /catalog/rooms. Same data as Cabin
        with neutral field names; deck is present only for vessel products.
      properties:
        id:
          oneOf: [{ type: number }, { type: string }]
        roomCode: { type: string }
        deck:
          type: number
          description: Present only for vessel-operated products.
        location: { type: string }
        priceAmount: { type: number }
        priceCurrency: { type: string }
        priceBasis: { type: string }
        berthsAvailable: { type: number }
        componentId:
          type: string
          description: Selection context — pass back verbatim in ComponentSelection
        optionId: { type: number }
        itemOptionId: { type: string }
        cabinTypeId: { type: string }

    Activity:
      type: object
      properties:
        activityName: { type: string }
        priceAmount: { type: number }
        priceCurrency: { type: string }
        unitsAvailable: { type: number }

    Occupancy:
      type: object
      required: [adults, children]
      properties:
        adults: { type: integer, minimum: 0 }
        children: { type: integer, minimum: 0 }

    CreateBasketRequest:
      type: object
      required: [package_id, departure_id, occupancy, channel_id, currency_code]
      properties:
        package_id: { type: string }
        departure_id: { type: string }
        occupancy:
          type: array
          items: { $ref: "#/components/schemas/Occupancy" }
          description: One entry per room/cabin
        channel_id: { type: string }
        currency_code: { type: string }
        account_id: { type: string }
        tax_profile_id: { type: string }
        service_level_id: { type: string }

    ComponentSelection:
      type: object
      required: [option_id, item_option_id]
      properties:
        option_id: { type: string }
        item_option_id: { type: string }
        cabin_id: { type: string }
        travellers:
          type: array
          items: { type: integer }
        rooms:
          type: array
          items: { type: integer }
        units:
          type: array
          items: { type: integer }

    UpdateComponentsRequest:
      type: object
      required: [component_id, selections]
      properties:
        component_id: { type: string }
        selections:
          type: array
          items: { $ref: "#/components/schemas/ComponentSelection" }

    Traveller:
      type: object
      required: [id, first_name, last_name, email]
      properties:
        id: { type: string }
        first_name: { type: string }
        last_name: { type: string }
        email: { type: string, format: email }
        phone: { type: string }
        date_of_birth: { type: string, format: date }
        contact:
          type: boolean
          description: Marks the lead passenger

    BasketComponent:
      type: object
      properties:
        component_id: { type: string }
        selections:
          type: array
          items: { $ref: "#/components/schemas/ComponentSelection" }

    Basket:
      type: object
      properties:
        id: { type: string }
        tenant_id: { type: string }
        channel_id: { type: string }
        package_id: { type: string }
        departure_id: { type: string }
        currency_code: { type: string }
        occupancy:
          type: array
          items: { $ref: "#/components/schemas/Occupancy" }
        components:
          type: array
          items: { $ref: "#/components/schemas/BasketComponent" }
        travellers:
          type: array
          items: { $ref: "#/components/schemas/Traveller" }
        promotions:
          type: array
          items: { type: string }
        status: { type: string, enum: [open, priced, booked] }
        created_at: { type: string, format: date-time }

    BasketPricing:
      type: object
      properties:
        subtotal: { type: number }
        taxes: { type: number }
        total: { type: number }
        currency: { type: string }
        deposit_amount: { type: number }
        deposit_percentage: { type: number }
        balance_due: { type: number }
        balance_due_date: { type: string, format: date }
        commission_amount:
          type: number
          description: Optional reseller commission aggregate; does not change customer totals.
        commission_lines:
          type: array
          items: { $ref: "#/components/schemas/PricingLine" }
        external_ancillary_lines:
          type: array
          items: { $ref: "#/components/schemas/ExternalAncillaryPricingLine" }
        external_ancillary_total: { type: number }
        presentation_total:
          type: number
          description: Display-only basket total plus validated external selections.
        line_items:
          type: array
          items: { $ref: "#/components/schemas/PricingLine" }

    PricingLine:
      type: object
      required: [label, amount, currency]
      properties:
        label: { type: string }
        amount: { type: number }
        currency: { type: string, pattern: "^[A-Z]{3}$" }

    ScalarMetadata:
      type: object
      maxProperties: 50
      additionalProperties:
        type: [string, number, boolean, "null"]

    ExternalAncillaryQuoteInput:
      type: object
      properties:
        traveller_ids:
          type: array
          maxItems: 50
          uniqueItems: true
          items: { type: string, maxLength: 200 }
        metadata: { $ref: "#/components/schemas/ScalarMetadata" }
      additionalProperties: false

    ExternalAncillaryQuoteItem:
      type: object
      required: [id, provider_ref, label, unit_amount, min_quantity, max_quantity]
      properties:
        id: { type: string }
        provider_ref: { type: string }
        label: { type: string }
        description: { type: string }
        unit_amount: { type: number, minimum: 0 }
        min_quantity: { type: integer, minimum: 1 }
        max_quantity: { type: integer, minimum: 1, maximum: 99 }
        metadata: { $ref: "#/components/schemas/ScalarMetadata" }

    ExternalAncillaryQuote:
      type: object
      required: [provider_id, quote_id, expires_at, currency, items, quote_token]
      properties:
        provider_id: { type: string }
        quote_id: { type: string }
        expires_at: { type: string, format: date-time }
        currency: { type: string, pattern: "^[A-Z]{3}$" }
        items:
          type: array
          items: { $ref: "#/components/schemas/ExternalAncillaryQuoteItem" }
        quote_token: { type: string }

    ExternalAncillarySelectionInput:
      type: object
      required: [quote_token, item_id, quantity]
      properties:
        quote_token: { type: string }
        item_id: { type: string }
        quantity: { type: integer, minimum: 1 }
        traveller_ids:
          type: array
          maxItems: 50
          uniqueItems: true
          items: { type: string, maxLength: 200 }
      additionalProperties: false

    ExternalAncillarySelection:
      type: object
      required:
        - provider_id
        - source
        - quote_id
        - item_id
        - provider_ref
        - label
        - quantity
        - unit_amount
        - total_amount
        - currency
        - expires_at
        - traveller_ids
      properties:
        provider_id: { type: string }
        source: { type: string }
        quote_id: { type: string }
        item_id: { type: string }
        provider_ref: { type: string }
        label: { type: string }
        quantity: { type: integer, minimum: 1 }
        unit_amount: { type: number, minimum: 0 }
        total_amount: { type: number, minimum: 0 }
        currency: { type: string, pattern: "^[A-Z]{3}$" }
        expires_at: { type: string, format: date-time }
        traveller_ids:
          type: array
          items: { type: string }
        metadata: { $ref: "#/components/schemas/ScalarMetadata" }

    ExternalAncillaryPricingLine:
      type: object
      required:
        - provider_id
        - source
        - quote_id
        - item_id
        - provider_ref
        - label
        - quantity
        - unit_amount
        - amount
        - currency
        - traveller_ids
        - selection_token
      properties:
        provider_id: { type: string }
        source: { type: string }
        quote_id: { type: string }
        item_id: { type: string }
        provider_ref: { type: string }
        label: { type: string }
        quantity: { type: integer, minimum: 1 }
        unit_amount: { type: number, minimum: 0 }
        amount: { type: number, minimum: 0 }
        currency: { type: string, pattern: "^[A-Z]{3}$" }
        traveller_ids:
          type: array
          items: { type: string }
        metadata: { $ref: "#/components/schemas/ScalarMetadata" }
        selection_token: { type: string }

    PaymentSchedule:
      type: object
      properties:
        id: { type: string }
        label: { type: string }
        amount: { type: number }
        due_date: { type: string, format: date }
        type: { type: string, enum: [deposit, balance] }

    PaymentRequestInput:
      type: object
      required: [passenger_payments, pay_per_person]
      properties:
        passenger_payments:
          type: array
          items:
            type: object
            required: [payment_schedule_rule_id]
            properties:
              passenger_id: { type: string }
              payment_schedule_rule_id: { type: string }
        pay_per_person: { type: boolean }

    PaymentRequest:
      type: object
      properties:
        id: { type: string }
        basket_id: { type: string }
        amount: { type: number }
        currency: { type: string }
        status: { type: string, enum: [pending, completed, failed] }
        payment_url: { type: string }
        surcharge_breakdown: { $ref: "#/components/schemas/PaymentSurchargeBreakdown" }

    PaymentSurchargeBreakdown:
      type: object
      required: [amount, currency, source, authoritative]
      properties:
        amount: { type: number, minimum: 0 }
        currency: { type: string, pattern: "^[A-Z]{3}$" }
        percentage: { type: number, minimum: 0 }
        method_id: { type: string }
        method_label: { type: string }
        source: { type: string, const: edge_pay }
        authoritative: { type: boolean }

    EmbedSession:
      type: object
      required: [session_id, embed_url, expires_at]
      properties:
        session_id: { type: string }
        embed_url: { type: string, format: uri }
        expires_at: { type: string, format: date-time }
        surcharge_breakdown: { $ref: "#/components/schemas/PaymentSurchargeBreakdown" }
        surcharge_options:
          type: array
          items: { $ref: "#/components/schemas/PaymentSurchargeBreakdown" }

    AdvisorAccount:
      type: object
      properties:
        account_id: { type: string }
        agency_name: { type: string }
        iata_number: { type: string }
        email: { type: string }
        country: { type: string }

    AdvisorLookupResult:
      type: object
      required: [status]
      properties:
        status: { type: string, enum: [matched, not_found, ambiguous] }
        account: { $ref: "#/components/schemas/AdvisorAccount" }

    BookingResult:
      type: object
      properties:
        itinerary_id: { type: string }
        booking_reference: { type: string }
        status: { type: string }
