{
  "openapi": "3.0.3",
  "info": {
    "title": "Bookings API",
    "version": "1.0.0",
    "description": "OpenAPI specification for booking-related endpoints. This spec is standalone and not related to XLVask."
  },
  "servers": [
    { "url": "https://api.truckwash.dk:4433" }
  ],
  "tags": [
    { "name": "Bookings" },
    { "name": "User Bookings" },
    { "name": "Admin Bookings" },
    { "name": "Public Time Bookings" }
  ],
  "paths": {
    "/bookings": {
      "get": {
        "tags": ["Bookings"],
        "summary": "List bookings or fetch a single booking by id",
        "description": "Requires permission `list_bookings`. If query parameter `id` is set, returns a single booking; otherwise returns a paginated list. If the caller has permission `issue_wash_certificates`, the meta will include `wash_certificate_token`.",
        "parameters": [
          { "$ref": "#/components/parameters/id" },
          { "$ref": "#/components/parameters/page" },
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/search" },
          { "$ref": "#/components/parameters/filters" },
          { "$ref": "#/components/parameters/order" }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EnvelopeBookingsOrBooking" }
              }
            }
          },
          "400": { "description": "Invalid request or session" },
          "403": { "description": "Forbidden" },
          "404": { "description": "Booking not found" }
        },
        "security": [{ "BearerAuth": [] }]
      },
      "put": {
        "tags": ["Bookings"],
        "summary": "Update a booking (reference number)",
        "description": "Requires permission `list_own_bookings`. Updates only the `reference_number` for a booking the user has access to.",
        "parameters": [
          { "$ref": "#/components/parameters/id" },
          {
            "name": "reference_number",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "maxLength": 255 }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated booking",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeBooking" } } }
          },
          "400": { "description": "Invalid request or session" },
          "403": { "description": "Forbidden" },
          "404": { "description": "Booking not found" }
        },
        "security": [{ "BearerAuth": [] }]
      }
    },

    "/user/bookings": {
      "get": {
        "tags": ["User Bookings"],
        "summary": "List bookings for the authenticated user",
        "description": "Requires permission `list_own_bookings`. Returns a paginated list of the user's own bookings.",
        "parameters": [
          { "$ref": "#/components/parameters/page" },
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/search" },
          { "$ref": "#/components/parameters/filters" },
          { "$ref": "#/components/parameters/order" }
        ],
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeBookingsList" } } } },
          "400": { "description": "Invalid request or session" }
        },
        "security": [{ "BearerAuth": [] }]
      }
    },

    "/admin/bookings/sync": {
      "post": {
        "tags": ["Admin Bookings"],
        "summary": "Sync or upsert a booking from external system",
        "description": "Requires permission `sync_bookings` unless `auth_key` equals the configured value. Accepts all booking fields as query/body form parameters and upserts the booking.",
        "requestBody": {
          "required": false,
          "content": {
            "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/SyncBookingRequest" } },
            "application/json": { "schema": { "$ref": "#/components/schemas/SyncBookingRequest" } }
          }
        },
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeMessage" } } } }
        }
      }
    },

    "/admin/bookings/department/count": {
      "get": {
        "tags": ["Admin Bookings"],
        "summary": "Get unfulfilled bookings count for a department (today)",
        "description": "Requires permission `list_department_bookings_count`. Returns an integer count. Response meta may include `cached: true` when the value was served from cache.",
        "parameters": [
          { "$ref": "#/components/parameters/department_id" }
        ],
        "responses": {
          "200": {
            "description": "Count",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeInteger" } } }
          },
          "400": { "description": "Invalid request or session" },
          "404": { "description": "Department not found" }
        },
        "security": [{ "BearerAuth": [] }]
      }
    },

    "/user/bookings/washcertificate/download": {
      "post": {
        "tags": ["User Bookings"],
        "summary": "Get download link for a booking's wash certificate",
        "description": "Requires permission `download_own_wash_certificate`, except authenticated customer accounts may download certificates for their own bookings. Returns a presigned download link if certificate exists and user has access.",
        "parameters": [ { "$ref": "#/components/parameters/id" } ],
        "responses": {
          "200": { "description": "Link", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeDownloadLink" } } } },
          "400": { "description": "Not allowed or invalid status" },
          "404": { "description": "Booking or certificate not found" }
        },
        "security": [{ "BearerAuth": [] }]
      }
    },

    "/bookings/download_pdf": {
      "get": {
        "tags": ["User Bookings"],
        "summary": "Get download link for a booking's wash certificate PDF",
        "description": "Requires permission `download_own_wash_certificate`, except authenticated customer accounts may download certificates for their own bookings. Checks both legacy and current storage buckets.",
        "parameters": [ { "$ref": "#/components/parameters/id" } ],
        "responses": {
          "200": { "description": "Link", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeDownloadLink" } } } },
          "400": { "description": "Not allowed or invalid session" },
          "404": { "description": "Booking or certificate not found" },
          "500": { "description": "Storage inconsistency" }
        },
        "security": [{ "BearerAuth": [] }]
      }
    },

    "/admin/bookings/delete": {
      "post": {
        "tags": ["Admin Bookings"],
        "summary": "Delete a booking",
        "description": "Requires permission `delete_booking`. Deletes the booking if the user has access.",
        "parameters": [ { "$ref": "#/components/parameters/id" } ],
        "responses": {
          "200": { "description": "Deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeMessage" } } } },
          "400": { "description": "Not allowed or invalid session" }
        },
        "security": [{ "BearerAuth": [] }]
      }
    },

    "/superuser/bookings/sync/all": {
      "post": {
        "tags": ["Admin Bookings"],
        "summary": "Sync all bookings from external system",
        "description": "Requires permission `sync_all_bookings`.",
        "responses": {
          "200": { "description": "Triggered", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeMessage" } } } },
          "400": { "description": "Invalid session" }
        },
        "security": [{ "BearerAuth": [] }]
      }
    },

    "/admin/bookings/completeWashWithoutWashCertificate": {
      "post": {
        "tags": ["Admin Bookings"],
        "summary": "Complete wash without generating a certificate",
        "description": "Requires permission `complete_wash_without_wash_certificate`.",
        "parameters": [ { "$ref": "#/components/parameters/id" } ],
        "responses": {
          "200": { "description": "Completed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeMessage" } } } },
          "400": { "description": "Not allowed or invalid session" }
        },
        "security": [{ "BearerAuth": [] }]
      }
    },

    "/user/bookings/delete": {
      "post": {
        "tags": ["User Bookings"],
        "summary": "Delete own booking",
        "description": "Requires permission `delete_own_booking`. Deletes the user's own booking if accessible.",
        "parameters": [ { "$ref": "#/components/parameters/id" } ],
        "responses": {
          "200": { "description": "Deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeMessage" } } } },
          "400": { "description": "Not allowed or invalid session" }
        },
        "security": [{ "BearerAuth": [] }]
      }
    },

    "/department/timebookings/departments/public": {
      "get": {
        "tags": ["Public Time Bookings"],
        "summary": "List public departments with time bookings enabled",
        "description": "Returns only visible, active departments where `bookingsystem_time_based_enabled` is `true`.",
        "parameters": [ { "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/limit" }, { "$ref": "#/components/parameters/search" }, { "$ref": "#/components/parameters/filters" }, { "$ref": "#/components/parameters/order" } ],
        "responses": {
          "200": { "description": "Departments", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopePublicTimeBookingDepartmentsList" } } } }
        }
      }
    },

    "/department/timebookings/opening-hours/public": {
      "get": {
        "tags": ["Public Time Bookings"],
        "summary": "Get public opening hours for a department",
        "parameters": [ { "$ref": "#/components/parameters/id" } ],
        "responses": {
          "200": { "description": "Opening hours", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeOpeningHours" } } } },
          "400": { "description": "Missing or invalid id" },
          "404": { "description": "Department or opening hours not found, or module disabled" }
        }
      }
    },

    "/department/timebookings/types/public": {
      "get": {
        "tags": ["Public Time Bookings"],
        "summary": "List public booking types for a department",
        "parameters": [ { "$ref": "#/components/parameters/id" } ],
        "responses": {
          "200": { "description": "Types", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeBookingTypesList" } } } },
          "400": { "description": "Missing or invalid id" },
          "404": { "description": "Department or types not found, or module disabled" }
        }
      }
    },

    "/department/timebookings/entries/public": {
      "get": {
        "tags": ["Public Time Bookings"],
        "summary": "List public booking entries for a department",
        "parameters": [ { "$ref": "#/components/parameters/id" }, { "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/limit" }, { "$ref": "#/components/parameters/search" }, { "$ref": "#/components/parameters/filters" }, { "$ref": "#/components/parameters/order" } ],
        "responses": {
          "200": { "description": "Entries", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeTimeBookingEntriesList" } } } },
          "400": { "description": "Missing or invalid id" },
          "404": { "description": "Department not found or module disabled" }
        }
      },
      "post": {
        "tags": ["Public Time Bookings"],
        "summary": "Create a public booking entry",
        "description": "Creates a time booking entry for a department and type at a specific start time. The duration is derived from the type.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/PublicEntryCreate" } },
            "application/json": { "schema": { "$ref": "#/components/schemas/PublicEntryCreate" } }
          }
        },
        "responses": {
          "200": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnvelopeTimeBookingEntry" } } } },
          "400": { "description": "Validation error" },
          "404": { "description": "Department/type not found or module disabled" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    },
    "parameters": {
      "id": { "name": "id", "in": "query", "required": false, "schema": { "type": "integer", "format": "int32" } },
      "department_id": { "name": "department_id", "in": "query", "required": true, "schema": { "type": "integer", "format": "int32" } },
      "page": { "name": "page", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1 } },
      "limit": { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1 } },
      "search": { "name": "search", "in": "query", "required": false, "schema": { "type": "string" } },
      "filters": { "name": "filters", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated filters: key:value pairs (supports multiple keys)." },
      "order": { "name": "order", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Sort order, e.g. `id:ASC` or `created_at:DESC`." }
    },
    "schemas": {
      "Envelope": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "data": {},
          "meta": { "type": "object", "additionalProperties": true },
          "includes": { "type": "object", "additionalProperties": true }
        },
        "required": ["success", "data", "meta", "includes"]
      },
      "EnvelopeMessage": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "type": "object", "properties": { "message": { "type": "string" } }, "required": ["message"] } } }
        ]
      },
      "EnvelopeInteger": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "type": "integer" } } }
        ]
      },
      "EnvelopeDownloadLink": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/DownloadLink" } } }
        ]
      },
      "EnvelopeBookingsList": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Booking" } } } }
        ]
      },
      "EnvelopeBooking": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Booking" } } }
        ]
      },
      "EnvelopeBookingsOrBooking": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "oneOf": [ { "$ref": "#/components/schemas/Booking" }, { "type": "array", "items": { "$ref": "#/components/schemas/Booking" } } ] } } }
        ]
      },
      "EnvelopeOpeningHours": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/OpeningHours" } } }
        ]
      },
      "EnvelopePublicTimeBookingDepartmentsList": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/PublicTimeBookingDepartment" } } } }
        ]
      },
      "EnvelopeBookingTypesList": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/BookingType" } } } }
        ]
      },
      "EnvelopeTimeBookingEntriesList": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/TimeBookingPublicEntry" } } } }
        ]
      },
      "EnvelopeTimeBookingEntry": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/TimeBookingEntry" } } }
        ]
      },

      "Booking": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "customer_number": { "type": "integer" },
          "wash_type": { "type": "string" },
          "contact_email": { "type": "string" },
          "reference_number": { "type": "string", "nullable": true },
          "regNrTraekker": { "type": "string", "nullable": true },
          "regNrTrailer": { "type": "string", "nullable": true },
          "washCertificateEmail": { "type": "string", "nullable": true },
          "date": { "type": "string" },
          "department": { "type": "integer" },
          "pickup_bool": { "type": "boolean" },
          "notes": { "type": "string", "nullable": true },
          "washCertificateStatus": { "type": "string", "enum": ["pending", "completed", "cancelled"], "nullable": true },
          "washCertificateUrl": { "type": "string", "nullable": true },
          "wash_certificate_pdf": { "type": "string", "nullable": true },
          "status": { "type": "string", "enum": ["pending", "completed", "cancelled"] },
          "data": { "type": "object", "nullable": true, "additionalProperties": true },
          "parsed_services": {
            "type": "object",
            "nullable": true,
            "properties": {
              "string": { "type": "string" },
              "array": { "type": "array", "items": { "type": "integer" } }
            }
          }
        },
        "required": ["id", "customer_number", "wash_type", "date", "department", "pickup_bool", "status"]
      },

      "DownloadLink": {
        "type": "object",
        "properties": { "link": { "type": "string", "format": "uri" } },
        "required": ["link"]
      },

      "PublicTimeBookingDepartment": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "address": { "type": "string" },
          "longitude": { "type": "number", "format": "float" },
          "latitude": { "type": "number", "format": "float" },
          "order_priority": { "type": "integer" },
          "time_booking_enabled": { "type": "boolean" }
        },
        "required": ["id", "name", "description", "address", "longitude", "latitude", "order_priority", "time_booking_enabled"]
      },

      "OpeningHours": {
        "type": "object",
        "properties": {
          "monday_start": { "type": ["string", "null"] },
          "monday_end": { "type": ["string", "null"] },
          "tuesday_start": { "type": ["string", "null"] },
          "tuesday_end": { "type": ["string", "null"] },
          "wednesday_start": { "type": ["string", "null"] },
          "wednesday_end": { "type": ["string", "null"] },
          "thursday_start": { "type": ["string", "null"] },
          "thursday_end": { "type": ["string", "null"] },
          "friday_start": { "type": ["string", "null"] },
          "friday_end": { "type": ["string", "null"] },
          "saturday_start": { "type": ["string", "null"] },
          "saturday_end": { "type": ["string", "null"] },
          "sunday_start": { "type": ["string", "null"] },
          "sunday_end": { "type": ["string", "null"] }
        }
      },

      "BookingType": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "department": { "type": "integer" },
          "product": { "type": "integer" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "duration": { "type": "integer", "description": "Duration in minutes" }
        },
        "required": ["id", "department", "product", "name", "description", "duration"]
      },

      "TimeBookingPublicEntry": {
        "type": "object",
        "properties": {
          "start": { "type": "string" },
          "duration": { "type": "integer" }
        },
        "required": ["start", "duration"]
      },

      "TimeBookingEntry": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "department": { "type": "integer" },
          "type": { "type": "integer" },
          "start": { "type": "string" },
          "end": { "type": "string" },
          "note": { "type": ["string", "null"] },
          "reg": { "type": ["string", "null"] },
          "phone": { "type": "integer" },
          "phone_country_code": { "type": "integer" }
        },
        "required": ["id", "department", "type", "start", "end", "phone", "phone_country_code"]
      },

      "PublicEntryCreate": {
        "type": "object",
        "properties": {
          "department": { "type": "integer" },
          "type": { "type": "integer" },
          "start": { "type": "string", "example": "2025-05-01 10:00:00" },
          "phone_country_code": { "type": "integer", "nullable": true },
          "phone": { "type": "integer", "nullable": true },
          "note": { "type": "string", "nullable": true },
          "reg": { "type": "string", "nullable": true },
          "addons": { "type": "array", "nullable": true, "items": { "type": "integer" } }
        },
        "required": ["department", "type", "start"]
      },

      "SyncBookingRequest": {
        "type": "object",
        "properties": {
          "auth_key": { "type": "string", "description": "Special key which allows syncing without the `sync_bookings` permission when valid" },
          "id": { "type": "integer" },
          "customer_number": { "type": "integer" },
          "wash_type": { "type": "string" },
          "contact_email": { "type": "string" },
          "reference_number": { "type": "string" },
          "regNrTraekker": { "type": "string" },
          "regNrTrailer": { "type": "string" },
          "washCertificateEmail": { "type": "string" },
          "date": { "type": "string" },
          "department": { "type": "integer" },
          "pickup_bool": { "type": "integer" },
          "notes": { "type": "string" },
          "washCertificateStatus": { "type": "string" },
          "washCertificateUrl": { "type": "string" },
          "status": { "type": "string" }
        },
        "required": ["id", "customer_number", "wash_type", "contact_email", "reference_number", "regNrTraekker", "regNrTrailer", "washCertificateEmail", "date", "department", "pickup_bool", "notes", "washCertificateStatus", "washCertificateUrl", "status"]
      }
    }
  }
}
