openapi: 3.0.3
info:
  title: QuoteFox API
  version: 1.0.0
  description: |
    REST API for Fleet Management Organizations (FMOs) to discover dealers,
    browse inventory, and request quotes.

    Authentication uses `Authorization: Bearer qfx_...` on protected endpoints.
servers:
  - url: https://app.quotefox.au
    description: Production
  - url: https://api-gltyc7uedq-km.a.run.app
    description: Development (direct Cloud Run function URL)
tags:
  - name: Auth
  - name: Dealers
  - name: Inventory
  - name: Quotes

paths:
  /api/v1/register:
    post:
      tags: [Auth]
      summary: Register a new FMO API client
      description: Creates a new API client and returns the plaintext API key once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterRequest"
            examples:
              default:
                value:
                  companyName: Fleet Corp Pty Ltd
                  contactEmail: api@fleetcorp.com.au
                  contactPhone: "+61400000000"
                  companyAbn: "12345678901"
      responses:
        "201":
          description: API client created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegisterResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/dealers:
    get:
      tags: [Dealers]
      summary: List approved dealerships
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/LimitParam"
        - $ref: "#/components/parameters/OffsetParam"
      responses:
        "200":
          description: Dealership list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DealersResponse"
        "401":
          $ref: "#/components/responses/InvalidApiKey"
        "429":
          $ref: "#/components/responses/RateLimitExceeded"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/dealers/{id}/inventory:
    get:
      tags: [Dealers]
      summary: List inventory for a dealership
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Dealership ID (UUID canonical or compact input accepted)
          schema:
            $ref: "#/components/schemas/UuidOrCompact"
        - $ref: "#/components/parameters/LimitParam"
        - $ref: "#/components/parameters/OffsetParam"
      responses:
        "200":
          description: Dealership inventory
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DealerInventoryResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/InvalidApiKey"
        "429":
          $ref: "#/components/responses/RateLimitExceeded"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/inventory:
    get:
      tags: [Inventory]
      summary: Search cross-dealer inventory
      security:
        - BearerAuth: []
      parameters:
        - name: model
          in: query
          required: true
          description: Vehicle model search text
          schema:
            type: string
            minLength: 1
            maxLength: 200
        - $ref: "#/components/parameters/LimitParam"
        - $ref: "#/components/parameters/OffsetParam"
      responses:
        "200":
          description: Matching vehicles
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InventorySearchResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/InvalidApiKey"
        "429":
          $ref: "#/components/responses/RateLimitExceeded"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/quotes:
    post:
      tags: [Quotes]
      summary: Create a quote request
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateQuoteRequest"
            examples:
              withGlobalCarId:
                value:
                  dealershipId: "550e8400-e29b-41d4-a716-446655440000"
                  globalCarId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
                  clientName: Fleet Corp Pty Ltd
                  clientEmail: fleet@corp.com.au
                  clientPhone: "+61400000000"
                  postcode: "3000"
                  callbackUrl: https://your-system.com/webhooks/quotefox
              withModelFallback:
                value:
                  dealershipId: "550e8400-e29b-41d4-a716-446655440000"
                  model: MG ZS
                  version: Excite 1.5L Auto
                  clientName: Fleet Corp Pty Ltd
                  clientEmail: fleet@corp.com.au
      responses:
        "201":
          description: Quote created (pending or auto-completed)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/CreateQuotePendingResponse"
                  - $ref: "#/components/schemas/CreateQuoteCompletedResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/InvalidApiKey"
        "402":
          description: Dealer has insufficient credits
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "404":
          description: Dealer or vehicle not found/unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          $ref: "#/components/responses/RateLimitExceeded"
        "500":
          $ref: "#/components/responses/InternalError"
    get:
      tags: [Quotes]
      summary: List quotes for the authenticated FMO
      security:
        - BearerAuth: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Optional status filter (exact match)
        - $ref: "#/components/parameters/LimitParam"
        - $ref: "#/components/parameters/OffsetParam"
      responses:
        "200":
          description: Quote list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QuoteListResponse"
        "401":
          $ref: "#/components/responses/InvalidApiKey"
        "429":
          $ref: "#/components/responses/RateLimitExceeded"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/quotes/{id}:
    get:
      tags: [Quotes]
      summary: Get quote details
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Quote ID (UUID canonical or compact input accepted)
          schema:
            $ref: "#/components/schemas/UuidOrCompact"
      responses:
        "200":
          description: Quote detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QuoteDetailResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/InvalidApiKey"
        "404":
          description: Quote not found for this API client
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          $ref: "#/components/responses/RateLimitExceeded"
        "500":
          $ref: "#/components/responses/InternalError"

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: 'Use `Authorization: Bearer qfx_...`'

  parameters:
    LimitParam:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    OffsetParam:
      name: offset
      in: query
      required: false
      schema:
        type: integer
        minimum: 0
        default: 0

  responses:
    InvalidRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    InvalidApiKey:
      description: Missing/malformed/invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    RateLimitExceeded:
      description: Request rate limit exceeded
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"

  schemas:
    Uuid:
      type: string
      format: uuid
      pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
      example: "550e8400-e29b-41d4-a716-446655440000"

    CompactUuid:
      type: string
      pattern: '^[0-9a-fA-F]{32}$'
      example: "550e8400e29b41d4a716446655440000"

    UuidOrCompact:
      oneOf:
        - $ref: "#/components/schemas/Uuid"
        - $ref: "#/components/schemas/CompactUuid"

    ErrorObject:
      type: object
      required: [code, message, status]
      properties:
        code:
          type: string
          example: INVALID_REQUEST
        message:
          type: string
          example: Query parameter 'model' is required.
        status:
          type: integer
          example: 400
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          $ref: "#/components/schemas/ErrorObject"

    Pagination:
      type: object
      required: [limit, offset, count]
      properties:
        limit:
          type: integer
        offset:
          type: integer
        count:
          type: integer

    RegisterRequest:
      type: object
      required: [companyName, contactEmail]
      properties:
        companyName:
          type: string
          minLength: 1
          maxLength: 200
        contactEmail:
          type: string
          format: email
        contactPhone:
          type: string
          maxLength: 30
        companyAbn:
          type: string
          maxLength: 20
    RegisterResponse:
      type: object
      required: [apiKey, message, rateLimitPerHour]
      properties:
        apiKey:
          type: string
          pattern: '^qfx_[0-9a-f]{60}$'
        message:
          type: string
        rateLimitPerHour:
          type: integer
          example: 100

    Dealer:
      type: object
      required: [id, name, suburb, state, postcode, autoRespond]
      properties:
        id:
          $ref: "#/components/schemas/Uuid"
        name:
          type: string
        suburb:
          type: string
          nullable: true
        state:
          type: string
          nullable: true
        postcode:
          type: string
          nullable: true
        autoRespond:
          type: boolean
    DealersResponse:
      type: object
      required: [dealers, pagination]
      properties:
        dealers:
          type: array
          items:
            $ref: "#/components/schemas/Dealer"
        pagination:
          $ref: "#/components/schemas/Pagination"

    DealerInventoryItem:
      type: object
      required: [globalCarId, model, version, specs, imageUrl]
      properties:
        globalCarId:
          $ref: "#/components/schemas/Uuid"
        model:
          type: string
        version:
          type: string
        specs:
          nullable: true
        imageUrl:
          type: string
          nullable: true
    DealerInventoryResponse:
      type: object
      required: [dealershipId, inventory, pagination]
      properties:
        dealershipId:
          $ref: "#/components/schemas/Uuid"
        inventory:
          type: array
          items:
            $ref: "#/components/schemas/DealerInventoryItem"
        pagination:
          $ref: "#/components/schemas/Pagination"

    VehicleSummary:
      type: object
      required: [id, model, version, specs, imageUrl]
      properties:
        id:
          $ref: "#/components/schemas/Uuid"
        model:
          type: string
        version:
          type: string
        specs:
          nullable: true
        imageUrl:
          type: string
          nullable: true
    InventorySearchResponse:
      type: object
      required: [vehicles, pagination]
      properties:
        vehicles:
          type: array
          items:
            $ref: "#/components/schemas/VehicleSummary"
        pagination:
          $ref: "#/components/schemas/Pagination"

    CreateQuoteRequest:
      type: object
      required: [dealershipId, clientName, clientEmail]
      properties:
        dealershipId:
          $ref: "#/components/schemas/UuidOrCompact"
        globalCarId:
          $ref: "#/components/schemas/UuidOrCompact"
        model:
          type: string
          maxLength: 200
        version:
          type: string
          maxLength: 200
        clientName:
          type: string
          minLength: 1
          maxLength: 200
        clientEmail:
          type: string
          format: email
        clientPhone:
          type: string
          maxLength: 30
        postcode:
          type: string
          maxLength: 10
        quantity:
          type: integer
          minimum: 1
          maximum: 1000
        accessoryIds:
          type: array
          maxItems: 50
          items:
            $ref: "#/components/schemas/UuidOrCompact"
        notes:
          type: string
          maxLength: 2000
        callbackUrl:
          type: string
          format: uri
          maxLength: 500
      description: Either `globalCarId` or `model` is required.

    AccessoryPrice:
      type: object
      required: [name, price]
      properties:
        name:
          type: string
        price:
          type: number

    CreateQuoteCompletedResponse:
      type: object
      required: [status, quoteId, dealership, vehicle, pricing, createdAt]
      properties:
        status:
          type: string
          enum: [completed]
        quoteId:
          $ref: "#/components/schemas/Uuid"
        dealership:
          type: object
          required: [name, email]
          properties:
            name:
              type: string
            email:
              type: string
              nullable: true
        vehicle:
          type: object
          required: [model, version, imageUrl]
          properties:
            model:
              type: string
            version:
              type: string
            imageUrl:
              type: string
              nullable: true
        pricing:
          type: object
          required: [dealerDelivery, accessories, totalDriveAway]
          properties:
            dealerDelivery:
              type: number
            accessories:
              type: array
              items:
                $ref: "#/components/schemas/AccessoryPrice"
            totalDriveAway:
              type: number
        createdAt:
          type: string
          format: date-time

    CreateQuotePendingResponse:
      type: object
      required: [status, quoteId, message, createdAt]
      properties:
        status:
          type: string
          enum: [pending]
        quoteId:
          $ref: "#/components/schemas/Uuid"
        message:
          type: string
        createdAt:
          type: string
          format: date-time

    QuoteListItem:
      type: object
      required: [id, dealership, vehicle, clientName, clientEmail, status, channel, createdAt, sentAt]
      properties:
        id:
          $ref: "#/components/schemas/Uuid"
        dealership:
          type: object
          required: [id, name, suburb, state]
          properties:
            id:
              $ref: "#/components/schemas/Uuid"
            name:
              type: string
            suburb:
              type: string
              nullable: true
            state:
              type: string
              nullable: true
        vehicle:
          type: object
          nullable: true
          properties:
            model:
              type: string
            version:
              type: string
        clientName:
          type: string
        clientEmail:
          type: string
          nullable: true
        status:
          type: string
        channel:
          type: string
        createdAt:
          type: string
          format: date-time
        sentAt:
          type: string
          format: date-time
          nullable: true
    QuoteListResponse:
      type: object
      required: [quotes, pagination]
      properties:
        quotes:
          type: array
          items:
            $ref: "#/components/schemas/QuoteListItem"
        pagination:
          $ref: "#/components/schemas/Pagination"

    QuoteDetailResponse:
      type: object
      required:
        - id
        - status
        - channel
        - dealership
        - vehicle
        - clientName
        - clientEmail
        - clientPhone
        - createdAt
        - sentAt
        - callbackUrl
      properties:
        id:
          $ref: "#/components/schemas/Uuid"
        status:
          type: string
        channel:
          type: string
        dealership:
          type: object
          required: [id, name, email, suburb, state]
          properties:
            id:
              $ref: "#/components/schemas/Uuid"
            name:
              type: string
            email:
              type: string
              nullable: true
            suburb:
              type: string
              nullable: true
            state:
              type: string
              nullable: true
        vehicle:
          type: object
          nullable: true
          properties:
            model:
              type: string
            version:
              type: string
            imageUrl:
              type: string
              nullable: true
        clientName:
          type: string
        clientEmail:
          type: string
          nullable: true
        clientPhone:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        sentAt:
          type: string
          format: date-time
          nullable: true
        callbackUrl:
          type: string
          nullable: true
        pricing:
          type: object
          nullable: true
          properties:
            totalDriveAway:
              type: number
            lineItems:
              nullable: true
        dealerNotes:
          type: string
          nullable: true
