openapi: 3.0.3
info:
  title: CR Bee REST API
  version: "1.0.0"
  description: |
    The CR Bee REST API powers car rental booking integrations — WordPress plugins,
    custom front-ends, and partner systems. All endpoints are scoped to a single
    tenant by the API key used.

    **Base URL:** `https://{your-slug}.crbeeapp.com/api/v1` (or `https://{your-domain}/api/v1`).

    **Authentication:** Bearer token. Create keys in **Admin → Settings → Developer**.
    Live keys are prefixed `crb_live_` followed by 32 hex characters.

    **Errors** are JSON `{ "error": "<message>", "details"?: <object> }`. See the
    Error Handling guide for the full catalog.

    **Rate limits** are returned in the `X-RateLimit-*` response headers. A `429`
    includes a `Retry-After` header in seconds.

    **CORS** is open for all origins on the public endpoints — browser integrations
    work without a proxy.
  contact:
    name: CR Bee Support
    url: https://crbeeapp.com
servers:
  - url: https://{slug}.crbeeapp.com/api/v1
    description: Tenant subdomain
    variables:
      slug:
        default: your-slug
        description: Your tenant's subdomain
  - url: https://{domain}/api/v1
    description: Custom domain
    variables:
      domain:
        default: example.com
        description: Your tenant's custom domain

tags:
  - name: Catalog
    description: Public, read-only catalog data — cars, categories, locations, addons, settings.
  - name: Availability & Quotes
    description: Check availability and price single or bulk quotes.
  - name: Sessions
    description: Server-side quote sessions that persist a customer's selection through the booking flow.
  - name: Bookings
    description: Create bookings, retrieve them by reference, cancel, and message.
  - name: Admin
    description: Tenant-admin endpoints for managing API keys, Stripe, and webhooks. Session auth (cookie), not Bearer.
  - name: Webhooks
    description: Webhook auto-registration for plugins, plus inbound Stripe webhook receivers.

security:
  - ApiKeyAuth: []

paths:
  # ── Catalog ────────────────────────────────────────────────────
  /cars:
    get:
      tags: [Catalog]
      summary: List cars
      description: Returns all published cars, with category, images, and feature labels. Optionally filter by category or featured flag.
      parameters:
        - { in: query, name: categoryId, schema: { type: string }, description: Filter by category ID. }
        - { in: query, name: featured,  schema: { type: boolean, default: false }, description: Return only featured cars. }
        - { in: query, name: withPrices, schema: { type: boolean, default: false }, description: Include `startingFromPrice` for every car (heavier query). }
      responses:
        "200":
          description: Cars list
          content:
            application/json:
              schema:
                type: object
                properties:
                  cars:
                    type: array
                    items: { $ref: "#/components/schemas/Car" }
              example:
                cars:
                  - id: "car_01HXY..."
                    name: "Toyota Yaris"
                    slug: "toyota-yaris"
                    description: "Compact, fuel-efficient hatchback"
                    quantity: 4
                    transmission: "manual"
                    fuelType: "petrol"
                    seats: 5
                    doors: 5
                    luggage: 2
                    engine: "1.0L"
                    licence: "B"
                    maxSpeed: 170
                    featured: true
                    images: ["https://cdn.example.com/yaris-1.jpg"]
                    specs: {}
                    features: []
                    orSimilarEnabled: true
                    orSimilarLabel: "or similar"
                    sortOrder: 1
                    category: { id: "cat_01...", name: "Economy", slug: "economy" }
                    startingFromPrice: 25.00
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /cars/{slug}:
    get:
      tags: [Catalog]
      summary: Get a car by slug
      parameters:
        - { in: path, name: slug, required: true, schema: { type: string }, description: Car slug. }
      responses:
        "200":
          description: Car details
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Car" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /categories:
    get:
      tags: [Catalog]
      summary: List categories
      responses:
        "200":
          description: Categories list
          content:
            application/json:
              schema:
                type: object
                properties:
                  categories:
                    type: array
                    items: { $ref: "#/components/schemas/Category" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /features:
    get:
      tags: [Catalog]
      summary: List feature labels
      description: Reusable feature tags (icons + labels) used across cars.
      responses:
        "200":
          description: Features list
          content:
            application/json:
              schema:
                type: object
                properties:
                  features:
                    type: array
                    items: { $ref: "#/components/schemas/Feature" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /locations:
    get:
      tags: [Catalog]
      summary: List locations
      description: Active pickup and drop-off locations with per-location operating hours (`null` = inherits global).
      responses:
        "200":
          description: Locations list
          content:
            application/json:
              schema:
                type: object
                properties:
                  locations:
                    type: array
                    items: { $ref: "#/components/schemas/Location" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /addons:
    get:
      tags: [Catalog]
      summary: List addons (extras + insurance)
      description: |
        Returns addons applicable to the tenant. When `carId` is provided, results are
        filtered by the addon's scope (`all` / `categories` / `vehicles`) and deduped
        by slug — most specific scope wins.
      parameters:
        - { in: query, name: carId, schema: { type: string }, description: Filter by car. Returns the resolved set of addons applicable to that vehicle. }
      responses:
        "200":
          description: Addons list
          content:
            application/json:
              schema:
                type: object
                properties:
                  addons:
                    type: array
                    items: { $ref: "#/components/schemas/Addon" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /settings:
    get:
      tags: [Catalog]
      summary: Get tenant settings
      description: |
        Returns the tenant's public-facing configuration: currency, tax mode, booking modes,
        operating hours, branding, and locale-resolved labels.
      parameters:
        - in: query
          name: locale
          schema: { type: string }
          description: BCP-47 locale code (e.g. `en`, `el`). Defaults to the tenant's `default_locale`.
      responses:
        "200":
          description: Tenant settings
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Settings" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Availability & Quotes ──────────────────────────────────────
  /availability/check:
    post:
      tags: [Availability & Quotes]
      summary: Check car availability
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [carId, pickupDate, dropoffDate]
              properties:
                carId:       { type: string, minLength: 1, maxLength: 100 }
                pickupDate:  { type: string, format: date, description: "ISO date `YYYY-MM-DD`." }
                dropoffDate: { type: string, format: date }
      responses:
        "200":
          description: Availability result
          content:
            application/json:
              schema:
                type: object
                properties:
                  available:    { type: boolean }
                  availableQty: { type: integer }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /quotes:
    post:
      tags: [Availability & Quotes]
      summary: Quote a single car
      description: Runs the pricing engine for one car and returns a full breakdown.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/QuoteRequest" }
      responses:
        "200":
          description: |
            Quote result. Always inspect `available` — when `false`, the response
            describes why the car cannot be quoted (no payload `error` is *not* an HTTP error).
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/QuoteSuccess" }
                  - { $ref: "#/components/schemas/QuoteUnavailable" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /quotes/bulk:
    post:
      tags: [Availability & Quotes]
      summary: Quote multiple cars
      description: |
        Returns a quote for each requested `carId` (or every published car if `carIds` is omitted).
        Per-car failures are reported inline — the response is always `200`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [pickupDate, dropoffDate]
              properties:
                pickupDate:         { type: string, format: date }
                dropoffDate:        { type: string, format: date }
                pickupTime:         { type: string, example: "10:00" }
                dropoffTime:        { type: string, example: "10:00" }
                pickupLocationId:   { type: string }
                dropoffLocationId:  { type: string }
                driverAge:          { type: integer, minimum: 16, maximum: 120 }
                carIds:
                  type: array
                  items: { type: string }
                  maxItems: 50
                  description: Omit to quote every published car.
      responses:
        "200":
          description: Quote results
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        carId:     { type: string }
                        available: { type: boolean }
                        quote:
                          type: object
                          nullable: true
                          properties:
                            pricePerDay:     { type: number }
                            baseTotal:       { type: number }
                            modifiersTotal:  { type: number }
                            discountTotal:   { type: number }
                            taxTotal:        { type: number }
                            grandTotal:      { type: number }
                            currency:        { type: string }
                            rentalDays:      { type: integer }
                            specialOfferId:  { type: string, nullable: true }
                        error: { type: string, nullable: true }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Sessions ───────────────────────────────────────────────────
  /sessions:
    post:
      tags: [Sessions]
      summary: Create a quote session
      description: |
        Persists a customer's car + dates + locations selection server-side and returns
        a `sessionId` to use through addon selection, coupon, and booking submission.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SessionRequest" }
      responses:
        "201":
          description: Session created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Session" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /sessions/{id}:
    get:
      tags: [Sessions]
      summary: Get a session
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      responses:
        "200":
          description: Session data
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Session" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /sessions/{id}/addons:
    patch:
      tags: [Sessions]
      summary: Update session addons
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [addons]
              properties:
                addons:
                  type: object
                  description: Map of `addonId` to quantity (0 to remove).
                  additionalProperties: { type: integer, minimum: 0, maximum: 100 }
                  example: { "addon_01...": 1, "addon_02...": 2 }
      responses:
        "200":
          description: Updated totals
          content:
            application/json:
              schema:
                type: object
                properties:
                  addons:
                    type: object
                    additionalProperties: { type: integer }
                  addonsTotal: { type: number }
                  grandTotal:  { type: number }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /sessions/{id}/coupon:
    post:
      tags: [Sessions]
      summary: Apply, validate, or remove a coupon
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [couponCode]
              properties:
                couponCode:
                  type: string
                  description: Coupon code to apply. Send the literal `__REMOVE__` to clear an applied coupon.
                  example: "SUMMER10"
      responses:
        "200":
          description: Coupon result
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: Coupon validated/applied, or removed.
                    properties:
                      valid:               { type: boolean, enum: [true] }
                      discount:            { type: number, description: "0 when removed." }
                      couponCode:          { type: string, nullable: true, description: "`null` when removed." }
                      couponFreeAddonId:   { type: string, nullable: true }
                      newGrandTotal:       { type: number }
                  - type: object
                    description: Coupon rejected.
                    properties:
                      valid: { type: boolean, enum: [false] }
                      error: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Bookings ───────────────────────────────────────────────────
  /bookings:
    post:
      tags: [Bookings]
      summary: Submit a booking
      description: |
        Converts a session into a booking. For `pay_now` and `deposit` modes the response
        includes a Stripe `checkoutUrl` — redirect the customer there to complete payment.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BookingRequest" }
      responses:
        "201":
          description: Booking created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BookingCreated" }
        "400":
          description: |
            Validation error, session not found / expired, payment mode unavailable,
            or terms not accepted. Inspect `error` for the specific reason.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409":
          description: Car no longer available
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /bookings/{ref}:
    get:
      tags: [Bookings]
      summary: Look up a booking
      description: Customer-style lookup — both `ref` and `email` are required and must match.
      parameters:
        - { in: path,  name: ref,   required: true, schema: { type: string }, description: Booking reference. }
        - { in: query, name: email, required: true, schema: { type: string, format: email }, description: Customer email on the booking. }
      responses:
        "200":
          description: Booking details
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Booking" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /bookings/{ref}/cancel:
    post:
      tags: [Bookings]
      summary: Cancel a booking
      description: |
        Cancels a booking when self-service cancellation is enabled and the booking is still
        within the cancellation window.
      parameters:
        - { in: path, name: ref, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
      responses:
        "200":
          description: Cancelled
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }
        "400":
          description: Cancellation disabled, status not cancellable, or outside cancellation window.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /bookings/{ref}/message:
    post:
      tags: [Bookings]
      summary: Send a message about a booking
      parameters:
        - { in: path, name: ref, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, message]
              properties:
                email:   { type: string, format: email }
                message: { type: string, minLength: 1, maxLength: 1000 }
      responses:
        "200":
          description: Message sent
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }
        "400":
          description: Messaging disabled.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Admin (session auth) ───────────────────────────────────────
  /admin/api-keys:
    get:
      tags: [Admin]
      summary: List API keys
      security: [{ SessionAuth: [] }]
      responses:
        "200":
          description: API keys (metadata only — raw keys are never returned)
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items: { $ref: "#/components/schemas/ApiKey" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Admin]
      summary: Create an API key
      description: The full `rawKey` is returned **once** in the response — store it securely.
      security: [{ SessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 100 }
      responses:
        "201":
          description: API key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:        { type: string }
                  name:      { type: string }
                  prefix:    { type: string, example: "crb_live_a1b2c3d4" }
                  rawKey:    { type: string, description: "Full secret. Shown once." }
                  createdAt: { type: string, format: date-time }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /admin/api-keys/{id}:
    delete:
      tags: [Admin]
      summary: Revoke an API key
      security: [{ SessionAuth: [] }]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      responses:
        "200":
          description: Revoked
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /admin/stripe-keys:
    get:
      tags: [Admin]
      summary: Stripe key configuration status
      description: Returns whether per-tenant Stripe keys are configured, plus the per-tenant webhook URL. Secrets are never returned.
      security: [{ SessionAuth: [] }]
      responses:
        "200":
          description: Stripe configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  hasPublishableKey: { type: boolean }
                  publishableKey:    { type: string, nullable: true }
                  hasSecretKey:      { type: boolean }
                  hasWebhookSecret:  { type: boolean }
                  webhookUrl:        { type: string, format: uri }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /admin/webhooks:
    get:
      tags: [Admin]
      summary: List webhooks
      security: [{ SessionAuth: [] }]
      responses:
        "200":
          description: Webhooks
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items: { $ref: "#/components/schemas/Webhook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Admin]
      summary: Register a webhook
      description: The signing `secret` is returned **once** — store it to verify HMAC-SHA256 signatures on incoming events.
      security: [{ SessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:    { type: string, format: uri }
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      [
                        "booking.created",
                        "booking.status_changed",
                        "booking.cancelled",
                        "booking.payment_updated",
                        "booking.message_received",
                        "catalog.updated",
                      ]
      responses:
        "201":
          description: Webhook registered
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookCreated" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /admin/webhooks/{id}:
    delete:
      tags: [Admin]
      summary: Delete a webhook
      security: [{ SessionAuth: [] }]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Webhooks ───────────────────────────────────────────────────
  /webhooks/auto-register:
    post:
      tags: [Webhooks]
      summary: Auto-register a webhook (idempotent)
      description: |
        Idempotent helper for first-party plugins (e.g. cr-bee-connect) — registers a webhook
        URL or returns the existing registration. Returns `201` when newly created, `200` when
        already registered (`alreadyRegistered: true`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, maxLength: 2000 }
                events:
                  type: array
                  maxItems: 20
                  items: { type: string }
                  default: []
      responses:
        "200":
          description: |
            URL was already registered. The `secret` is **not** returned — store it
            from the original `201` response. `alreadyRegistered` is `true`.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Webhook"
                  - type: object
                    required: [alreadyRegistered]
                    properties:
                      alreadyRegistered: { type: boolean, enum: [true] }
        "201":
          description: Newly registered. The `secret` is shown **once** — store it.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookCreated" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /stripe/webhook:
    post:
      tags: [Webhooks]
      summary: Stripe webhook (global)
      description: |
        Receives Stripe events for tenants using the **platform-wide** Stripe account.
        Verified using `STRIPE_WEBHOOK_SECRET`. The `Authorization` header is **not** used —
        Stripe signs the request via `stripe-signature`.
      security: []
      parameters:
        - { in: header, name: stripe-signature, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, description: Stripe Event payload. }
      responses:
        "200":
          description: Event acknowledged.
          content:
            application/json:
              schema:
                type: object
                properties:
                  received: { type: boolean, enum: [true] }
                  error:    { type: string, nullable: true }
        "400":
          description: Missing or invalid signature.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "500":
          description: Stripe not configured.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  /stripe/webhook/{tenantId}:
    post:
      tags: [Webhooks]
      summary: Stripe webhook (per-tenant)
      description: |
        Receives Stripe events for tenants using their **own** Stripe account. Verified
        against the per-tenant webhook secret.
      security: []
      parameters:
        - { in: path,   name: tenantId,         required: true, schema: { type: string } }
        - { in: header, name: stripe-signature, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, description: Stripe Event payload. }
      responses:
        "200":
          description: Event acknowledged.
          content:
            application/json:
              schema:
                type: object
                properties:
                  received: { type: boolean, enum: [true] }
                  error:    { type: string, nullable: true }
        "400":
          description: Missing or invalid signature.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "404":
          description: Tenant not found or inactive.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "500":
          description: Stripe not configured for tenant.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: crb_live_*
      description: |
        `Authorization: Bearer crb_live_<32-hex>`. Create keys in **Admin → Settings → Developer**.
    SessionAuth:
      type: apiKey
      in: cookie
      name: authjs.session-token
      description: |
        Session cookie issued by NextAuth after admin login on the tenant subdomain.
        Production uses the `__Secure-` prefix (HTTPS-only). Used only by `/admin/*` endpoints —
        for programmatic access prefer the Bearer API key on the public endpoints.

  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "Invalid or missing API key" }
    Forbidden:
      description: Authenticated but lacks the required role.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BadRequest:
      description: Validation failed or invalid input.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: "Validation error"
            details: { pickupDate: ["Invalid date"] }
    RateLimited:
      description: Rate limit exceeded. Inspect `Retry-After` and `X-RateLimit-*` headers.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds until the limit window resets.
        X-RateLimit-Limit:
          schema: { type: integer }
        X-RateLimit-Remaining:
          schema: { type: integer }
        X-RateLimit-Reset:
          schema: { type: integer, description: Unix epoch milliseconds. }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "Rate limit exceeded" }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:   { type: string }
        details:
          type: object
          additionalProperties: true
          description: Field-level Zod errors when applicable.

    Success:
      type: object
      properties:
        success: { type: boolean, enum: [true] }

    Car:
      type: object
      properties:
        id:                { type: string }
        name:              { type: string }
        slug:              { type: string }
        description:       { type: string, nullable: true }
        quantity:          { type: integer }
        transmission:      { type: string, nullable: true, example: "manual" }
        fuelType:          { type: string, nullable: true, example: "petrol" }
        seats:             { type: integer, nullable: true }
        doors:             { type: integer, nullable: true }
        luggage:           { type: integer, nullable: true }
        engine:            { type: string,  nullable: true }
        licence:           { type: string,  nullable: true }
        maxSpeed:          { type: integer, nullable: true }
        featured:          { type: boolean }
        images:            { type: array, items: { type: string, format: uri } }
        specs:             { type: object, additionalProperties: true }
        features:
          type: array
          items: { type: string, description: "Feature ID or label." }
        orSimilarEnabled:  { type: boolean }
        orSimilarLabel:    { type: string, nullable: true }
        sortOrder:         { type: integer }
        category:
          type: object
          properties:
            id:   { type: string }
            name: { type: string }
            slug: { type: string }
        startingFromPrice:
          type: number
          nullable: true
          description: Present when `featured=true` or `withPrices=true`.

    Category:
      type: object
      properties:
        id:          { type: string }
        name:        { type: string }
        slug:        { type: string }
        description: { type: string, nullable: true }
        image:       { type: string, nullable: true, format: uri }
        sortOrder:   { type: integer }

    Feature:
      type: object
      properties:
        id:          { type: string }
        label:       { type: string }
        slug:        { type: string }
        icon:        { type: string, nullable: true }
        value:       { type: string, nullable: true }
        sortOrder:   { type: integer }
        showInCard:  { type: boolean }

    Location:
      type: object
      properties:
        id:                    { type: string }
        name:                  { type: string }
        slug:                  { type: string }
        address:               { type: string, nullable: true }
        lat:                   { type: number, nullable: true }
        lng:                   { type: number, nullable: true }
        sortOrder:             { type: integer }
        operating_hours_from:  { type: string, nullable: true, example: "08:00", description: "`null` = inherits global." }
        operating_hours_to:    { type: string, nullable: true, example: "20:00" }

    Addon:
      type: object
      properties:
        id:              { type: string }
        name:            { type: string }
        slug:            { type: string }
        description:     { type: string, nullable: true }
        type:            { type: string, example: "addon", description: "`addon` or `insurance`." }
        price:           { type: number }
        exclusiveGroup:  { type: string, nullable: true, description: Selecting one addon in a group deselects others. }
        maxQuantity:     { type: integer, nullable: true }
        image:           { type: string, nullable: true, format: uri }
        icon:            { type: string, nullable: true }
        accentColor:     { type: string, nullable: true }
        subtitle:        { type: string, nullable: true }
        coverageItems:
          type: array
          items: { type: string }
        preselected:     { type: boolean }
        noCoverage:      { type: boolean }
        notice:          { type: string, nullable: true }
        sortOrder:       { type: integer }

    Settings:
      type: object
      description: Public-facing tenant settings, locale-resolved.
      properties:
        currency:                 { type: string, example: "EUR" }
        tax_mode:                 { type: string, example: "inclusive" }
        vat_rate:                 { type: number, example: 24 }
        booking_modes:
          type: array
          items: { type: string, enum: [request, pay_at_pickup, pay_now, deposit] }
        deposit_percentage:       { type: number }
        default_pickup_time:      { type: string, example: "10:00" }
        default_dropoff_time:     { type: string, example: "10:00" }
        operating_hours_from:     { type: string, example: "08:00" }
        operating_hours_to:       { type: string, example: "20:00" }
        min_rental_days:          { type: integer }
        max_rental_days:          { type: integer }
        day_end_time:             { type: string, nullable: true, example: "10:00" }
        advance_booking_days:     { type: integer }
        lead_time_hours:          { type: integer }
        cancellation_window_hours: { type: integer }
        terms_url:                { type: string, format: uri, nullable: true }
        privacy_url:              { type: string, format: uri, nullable: true }
        business_phone:           { type: string, nullable: true }
        whatsapp_number:          { type: string, nullable: true }
        brand_color:              { type: string, nullable: true }
        accent_color:             { type: string, nullable: true }
        accent_text_color:        { type: string, nullable: true }
        accent_hover_color:       { type: string, nullable: true }
        button_hover_color:       { type: string, nullable: true }
        button_text_color:        { type: string, nullable: true }
        button_text_hover_color:  { type: string, nullable: true }
        button_border_width:      { type: string, nullable: true }
        button_border_color:      { type: string, nullable: true }
        card_border_width:        { type: string, nullable: true }
        card_border_color:        { type: string, nullable: true }
        input_border_width:       { type: string, nullable: true }
        input_border_color:       { type: string, nullable: true }
        border_radius:            { type: string, nullable: true }
        font_family:              { type: string, nullable: true }
        shadow_style:             { type: string, nullable: true }
        shadow_color:             { type: string, nullable: true }
        logo_url:                 { type: string, format: uri, nullable: true }
        whatsapp_template:        { type: string, nullable: true }
        trust_banners:            { type: array, items: { type: string } }
        trust_banners_enabled:    { type: boolean }
        trust_messages:           { type: array, items: { type: string } }
        trust_messages_enabled:   { type: boolean }
        default_locale:           { type: string, example: "en" }
        enabled_locales:          { type: array, items: { type: string }, example: ["en", "el"] }
        date_format:              { type: string, example: "DD/MM/YYYY" }
        manage_page_enabled:      { type: boolean }
        manage_cancel_enabled:    { type: boolean }
        manage_message_enabled:   { type: boolean }
        labels:
          type: object
          additionalProperties: { type: string }
          description: Flat key→string map of locale-resolved booking-flow labels.

    QuoteRequest:
      type: object
      required: [carId, pickupDate, dropoffDate]
      properties:
        carId:              { type: string }
        pickupDate:         { type: string, format: date }
        dropoffDate:        { type: string, format: date }
        pickupTime:         { type: string, example: "10:00" }
        dropoffTime:        { type: string, example: "10:00" }
        pickupLocationId:   { type: string }
        dropoffLocationId:  { type: string }
        driverAge:          { type: integer, minimum: 16, maximum: 120 }
        couponCode:         { type: string }

    QuoteSuccess:
      type: object
      properties:
        available:       { type: boolean, enum: [true] }
        pricePerDay:     { type: number }
        baseTotal:       { type: number }
        modifiersTotal:  { type: number }
        discountTotal:   { type: number }
        taxTotal:        { type: number }
        grandTotal:      { type: number }
        currency:        { type: string }
        rentalDays:      { type: integer }
        seasonId:        { type: string, nullable: true }
        tierId:          { type: string, nullable: true }
        priceGroupId:    { type: string, nullable: true }
        specialOfferId:  { type: string, nullable: true }
        couponCode:      { type: string, nullable: true }
        rules:
          type: array
          items: { type: object, additionalProperties: true }
          description: Pricing-engine rule trace (which seasons, modifiers, etc. applied).

    QuoteUnavailable:
      type: object
      properties:
        available: { type: boolean, enum: [false] }
        error:     { type: string }

    SessionRequest:
      type: object
      required: [carId, pickupDate, dropoffDate]
      properties:
        carId:              { type: string }
        pickupDate:         { type: string, format: date }
        dropoffDate:        { type: string, format: date }
        pickupTime:         { type: string, example: "10:00" }
        dropoffTime:        { type: string, example: "10:00" }
        pickupLocationId:   { type: string }
        dropoffLocationId:  { type: string }
        driverAge:          { type: integer, minimum: 16, maximum: 120 }
        isRequest:          { type: boolean, description: "True for `request`-mode bookings (skips operating-hours validation)." }

    Session:
      type: object
      properties:
        sessionId:           { type: string }
        carId:               { type: string }
        carName:             { type: string }
        carSlug:             { type: string }
        carImage:            { type: string, nullable: true, format: uri }
        categoryName:        { type: string }
        pickupDate:          { type: string, format: date }
        dropoffDate:         { type: string, format: date }
        pickupTime:          { type: string }
        dropoffTime:         { type: string }
        rentalDays:          { type: integer }
        pickupLocationId:    { type: string, nullable: true }
        pickupLocationName:  { type: string, nullable: true }
        dropoffLocationId:   { type: string, nullable: true }
        dropoffLocationName: { type: string, nullable: true }
        driverAge:           { type: integer, nullable: true }
        pricePerDay:         { type: number }
        baseTotal:           { type: number }
        modifiersTotal:      { type: number }
        discountTotal:       { type: number }
        taxTotal:            { type: number }
        taxMode:             { type: string }
        grandTotal:          { type: number }
        currency:            { type: string }
        seasonId:            { type: string, nullable: true }
        tierId:              { type: string, nullable: true }
        priceGroupId:        { type: string, nullable: true }
        specialOfferId:      { type: string, nullable: true }
        specialOfferName:    { type: string, nullable: true }
        addons:
          type: object
          additionalProperties: { type: integer }
        addonsTotal:         { type: number }
        couponDiscount:      { type: number }

    BookingRequest:
      type: object
      required: [sessionId, customer, bookingMode]
      properties:
        sessionId:    { type: string, minLength: 1, maxLength: 100 }
        customer:
          type: object
          required: [name, email, phone]
          properties:
            name:    { type: string, minLength: 1, maxLength: 200 }
            email:   { type: string, format: email }
            phone:   { type: string, minLength: 1, maxLength: 50 }
            age:     { type: integer, minimum: 16, maximum: 120 }
            hotel:   { type: string }
            flight:  { type: string }
        bookingMode:
          type: string
          enum: [request, pay_at_pickup, pay_now, deposit]
        termsAccepted: { type: boolean }
        successUrl:    { type: string, format: uri, description: "Stripe Checkout success URL (`pay_now`/`deposit` only)." }
        cancelUrl:     { type: string, format: uri }

    BookingCreated:
      type: object
      required: [bookingRef]
      properties:
        bookingRef:   { type: string, example: "BK-2025-001234" }
        checkoutUrl:  { type: string, format: uri, description: "Present for `pay_now` and `deposit` modes." }
        emailFailed:  { type: boolean, description: "`true` if the confirmation email could not be delivered (booking still created)." }

    Booking:
      type: object
      properties:
        bookingRef:      { type: string }
        bookingStatus:
          type: string
          enum: [request_received, confirmed, pending_payment, in_progress, completed, cancelled]
        paymentStatus:
          type: string
          enum: [none, pending, deposit_paid, paid, refunded, failed]
        bookingMode:     { type: string, enum: [request, pay_at_pickup, pay_now, deposit] }
        customerName:    { type: string }
        customerEmail:   { type: string, format: email }
        customerPhone:   { type: string }
        customerAge:     { type: integer, nullable: true }
        customerHotel:   { type: string, nullable: true }
        customerFlight:  { type: string, nullable: true }
        pickupDate:      { type: string, format: date-time, description: "ISO 8601 datetime — the date portion is the rental date." }
        dropoffDate:     { type: string, format: date-time }
        pickupTime:      { type: string, example: "10:00" }
        dropoffTime:     { type: string, example: "10:00" }
        rentalDays:      { type: integer }
        carId:           { type: string }
        pricePerDay:     { type: number }
        baseTotal:       { type: number }
        addonsTotal:     { type: number }
        modifiersTotal:  { type: number }
        discountTotal:   { type: number }
        taxTotal:        { type: number }
        grandTotal:      { type: number }
        currency:        { type: string }
        depositAmount:   { type: number, nullable: true }
        couponCode:      { type: string, nullable: true }
        couponDiscount:  { type: number, nullable: true }
        createdAt:       { type: string, format: date-time }
        items:
          type: array
          items:
            type: object
            properties:
              name:      { type: string }
              quantity:  { type: integer }
              unitPrice: { type: number }
              lineTotal: { type: number }
              itemType:  { type: string, example: "car" }

    ApiKey:
      type: object
      properties:
        id:         { type: string }
        name:       { type: string }
        prefix:     { type: string, example: "crb_live_a1b2c3d4" }
        type:       { type: string, example: "live" }
        lastUsedAt: { type: string, format: date-time, nullable: true }
        expiresAt:  { type: string, format: date-time, nullable: true }
        createdAt:  { type: string, format: date-time }

    Webhook:
      type: object
      properties:
        id:        { type: string }
        url:       { type: string, format: uri }
        events:
          type: array
          items: { type: string }
        active:    { type: boolean }
        createdAt: { type: string, format: date-time }

    WebhookCreated:
      allOf:
        - $ref: "#/components/schemas/Webhook"
        - type: object
          required: [secret]
          properties:
            secret: { type: string, description: HMAC-SHA256 signing secret. Shown once. }
