openapi: 3.0.3
info:
    title: Copenhagen Truck Wash API
    description: |
        This API provides access to the Copenhagen Truck Wash system, managing orders, bookings, 
        departments, products, customers, and various integrations including e-conomic, Stripe, 
        XLVask, and more.
        
        ## Authentication
        Most endpoints require authentication using a Bearer token obtained from the `/auth/login` 
        or `/auth/employee/login` endpoints.
        
        ## Permissions
        Many endpoints require specific permissions that are assigned to user groups/roles.
        
        ## Subusers and customer targeting
        When authenticated as a subuser, most customer-scoped endpoints require an explicit target
        customer context. Provide the header `X-Customer-Number: <customer_number>` to target a
        specific customer. If omitted, the API attempts to infer the customer from the authenticated
        user context when possible. Classic user sessions ignore this header.
    version: 1.0.0
    contact:
        name: Copenhagen Truck Wash
        email: support@truckwash.dk
servers:
    - url: https://api.truckwash.dk
      description: Production server (.dk)
    - url: https://api.truckwash.io
      description: Production server (.io)
    - url: http://localhost
      description: Local development server

security:
    - BearerAuth: []

tags:
    - name: Authentication
      description: User and employee authentication endpoints
    - name: Users
      description: User management and customer operations
    - name: Orders
      description: Order creation, management, and retrieval
    - name: Order Items
      description: Managing items within orders
    - name: Bookings
      description: Booking management for wash services
    - name: Departments
      description: Department and location management
    - name: Products
      description: Product catalog and pricing
    - name: Categories
      description: Product category management
    - name: Invoices
      description: Invoice generation and management
    - name: Payments
      description: Payment processing and collection
    - name: Vehicles
      description: Vehicle registration and management
    - name: Notifications
      description: System notifications and alerts
    - name: Statistics
      description: Business analytics and reporting
    - name: Modules
      description: Third-party integrations and modules
    - name: Attachments
      description: File upload and attachment management
    - name: Forms
      description: Form submissions and management
    - name: Worker
      description: System worker status and maintenance
    - name: Plate Scans
      description: License plate scanning operations
    - name: Config
      description: Module configuration management
    - name: Branding
      description: Branding options management
    - name: Roles
      description: Role and permission management
    - name: Self-Serve
      description: Self-serve lane operations and questions
    - name: Goals
      description: Department goals management
    - name: Subusers
      description: Subuser registration and setup
    - name: Bird
      description: Voice Calls via Bird

paths:
  # Bird Voice Calls
  /bird/voice/calls:
    post:
      tags:
        - Bird
      summary: Create/place a voice call via Bird
      operationId: birdCreateVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              properties:
                to:
                  type: string
                  description: E.164 phone number of the callee
                  example: "+4511122233"
                from:
                  type: string
                  description: E.164 phone number of the caller (sender)
                  example: "+4599988877"
      responses:
        '200':
          description: Call created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallSingleResponse' }
    get:
      tags:
        - Bird
      summary: List voice calls
      operationId: birdListVoiceCalls
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
        - in: query
          name: page
          schema:
            type: integer
          required: false
      responses:
        '200':
          description: A list of calls
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallListResponse' }

  /bird/voice/calls/{id}:
    get:
      tags:
        - Bird
      summary: Get a voice call by ID
      operationId: birdGetVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Call details
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallSingleResponse' }

  /bird/voice/calls/{id}/hangup:
    post:
      tags:
        - Bird
      summary: Hang up a voice call by ID
      operationId: birdHangupVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Hangup requested
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallSingleResponse' }

  /bird/voice/calls/{id}/say:
    post:
      tags:
        - Bird
      summary: Say a message on an active voice call and hang up afterwards
      operationId: birdSayOnVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: Call identifier
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - text
              properties:
                text:
                  type: string
                  description: The text message to play via TTS
                  example: "The gate will open shortly."
                locale:
                  type: string
                  description: The locale to use for the TTS voice (e.g. en-US)
                  example: "en-US"
                voice:
                  type: string
                  description: The voice identifier to use
                  example: "male"
                loop:
                  type: integer
                  description: Number of times to loop the message
                  example: 1
                timeout:
                  type: integer
                  description: Timeout in seconds for the TTS action
                  example: 1
                hangup:
                  type: boolean
                  description: Whether to hang up the call after the message finishes playing (defaults to true)
                  example: true
      responses:
        '200':
          description: TTS action requested
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallSingleResponse' }

  /bird/voice/calls/test-outbound:
    post:
      tags:
        - Bird
      summary: Place a test outbound call and hang up when accepted
      operationId: birdTestOutboundVoiceCall
      description: Calls +45 42 33 11 28 and hangs up when the call reaches accepted/ongoing state.
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              properties:
                from:
                  type: string
                  description: Caller E.164 number to use for the test call
                  example: "+4599988877"
                pollIntervalSeconds:
                  type: integer
                  minimum: 1
                  description: Poll interval while waiting for accepted status
                  example: 2
                maxPollSeconds:
                  type: integer
                  minimum: 5
                  description: Max time to wait before timing out
                  example: 30
                hangupCause:
                  type: string
                  description: Optional hangup cause passed through to Bird
      responses:
        '200':
          description: Test call created and either hung up or timed out
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdTestOutboundCallResponse' }

  # Bird Numbers
  /bird/numbers:
    get:
      tags:
        - Bird
      summary: List your numbers
      operationId: birdListNumbers
      parameters:
        - in: query
          name: workspaceId
          required: false
          schema:
            type: string
          description: Bird Workspace identifier (optional if configured)
        - in: query
          name: page
          required: false
          schema:
            type: integer
        - in: query
          name: limit
          required: false
          schema:
            type: integer
      responses:
        '200':
          description: A list of numbers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdNumberListResponse'

  /bird/numbers/{id}:
    get:
      tags:
        - Bird
      summary: Get a number by ID
      operationId: birdGetNumber
      parameters:
        - in: query
          name: workspaceId
          required: false
          schema:
            type: string
          description: Bird Workspace identifier (optional if configured)
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Number details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdNumberSingleResponse'
    delete:
      tags:
        - Bird
      summary: Delete/release a number by ID
      operationId: birdDeleteNumber
      parameters:
        - in: query
          name: workspaceId
          required: false
          schema:
            type: string
          description: Bird Workspace identifier (optional if configured)
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Number deletion/release accepted
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  # Bird Voice Flash Calling
  /bird/voice/flash-calls:
    post:
      tags:
        - Bird
      summary: Create/place a flash call via Bird
      operationId: birdCreateFlashCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Flash call created
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
    get:
      tags:
        - Bird
      summary: List flash calls
      operationId: birdListFlashCalls
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
        - in: query
          name: page
          schema:
            type: integer
          required: false
      responses:
        '200':
          description: A list of flash calls
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /bird/voice/flash-calls/{id}:
    get:
      tags:
        - Bird
      summary: Get a flash call by ID
      operationId: birdGetFlashCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Flash call details
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
    post:
      tags:
        - Bird
      summary: Complete/end a flash call by ID
      description: Posts a completion/update payload to the flash call resource to finalize verification.
      operationId: birdEndFlashCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
        - in: path
          name: id
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Flash call completed
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /bird/voice/flash-calls/end:
    post:
      tags:
        - Bird
      summary: Complete/end a flash call using from/to numbers
      description: Ends an ongoing flash call by specifying the originating and destination numbers.
      operationId: birdEndFlashCallByNumbers
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              properties:
                from:
                  type: string
                  description: E.164 formatted caller number
                  example: "+4599988877"
                to:
                  type: string
                  description: E.164 formatted callee number
                  example: "+4511122233"
      responses:
        '200':
          description: Flash call completed (by numbers)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  # Subusers (public registration + setup)
  /subusers:
    get:
      tags:
        - Subusers
      summary: List subusers visible to the authenticated user
      description: |
        Returns a paginated list of subusers (drivers) that have enabled grants tied to the
        authenticated user's customer number. Only subusers with at least one enabled, non-deleted
        grant for the caller's customer are returned.
      operationId: listSubusers
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 1000
        - name: search
          in: query
          required: false
          schema:
            type: string
        - name: include_non_enabled
          in: query
          required: false
          description: Include subusers that only have non-enabled grants (default false)
          schema:
            type: boolean
      responses:
        '200':
          description: List of visible subusers
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                    username:
                      type: string
                      nullable: true
                    name:
                      type: string
                      nullable: true
                    email:
                      type: string
                      format: email
                      nullable: true
                    phone_country_code:
                      type: integer
                      nullable: true
                    phone:
                      type: integer
                      nullable: true
                    created_at:
                      type: string
                      format: date-time
                      nullable: true
                    updated_at:
                      type: string
                      format: date-time
                      nullable: true
                    suspended_at:
                      type: string
                      format: date-time
                      nullable: true
                    two_factor_enabled:
                      type: boolean
                      description: Indicates if 2FA is enabled for this account
                    permissions:
                      type: array
                      description: Aggregated permission keys granted for the caller's customer
                      items:
                        type: string
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalServerError' }
  
  /subusers/me:
    get:
      tags:
        - Subusers
      summary: Get current subuser profile
      description: |
        Returns the authenticated subuser (driver) profile and their enabled grants grouped by
        `billing_customer_number`.

        Notes:
        - This endpoint is available only to authenticated subuser sessions.
        - It does not require the `X-Customer-Number` header; all enabled, non-deleted grants for the
          subuser are included in the response.
      operationId: getCurrentSubuser
      responses:
        '200':
          description: Current subuser details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubuserSelf'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    post:
      tags:
        - Subusers
      summary: Create a subuser registration
      description: |
                Creates a subuser (driver) account using a company's CVR and a phone number. Validates the
                CVR via e-conomic, ensures the phone number is not already in use, and if SMS is enabled
                sends a setup link by SMS for the user to complete registration.
      operationId: createSubuser
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - cvr
                - phone_country_code
                - phone
              properties:
                cvr:
                  type: integer
                  description: Danish CVR (8 digits)
                  example: 12345678
                phone_country_code:
                  type: integer
                  description: Phone country code (1–3 digits)
                  example: 45
                phone:
                  type: integer
                  description: Phone number (4–15 digits, no leading +)
                  example: 12345678
      responses:
        '200':
          description: Subuser created (or pending setup) and company identified
          content:
            application/json:
              schema:
                type: object
                properties:
                  cvr:
                    type: integer
                    example: 12345678
                  customer_number:
                    type: integer
                    description: Matched e-conomic customer number
                    example: 1000
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /subusers/{id}:
    get:
      tags:
        - Subusers
      summary: Get a subuser by ID (visible by grant)
      description: |
        Returns the subuser if the authenticated user has at least one enabled, non-deleted grant
        for their customer number to this subuser. Otherwise returns 404.
      operationId: getSubuser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Subuser details
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  username:
                    type: string
                    nullable: true
                  name:
                    type: string
                    nullable: true
                  email:
                    type: string
                    format: email
                    nullable: true
                  phone_country_code:
                    type: integer
                    nullable: true
                  phone:
                    type: integer
                    nullable: true
                  created_at:
                    type: string
                    format: date-time
                    nullable: true
                  updated_at:
                    type: string
                    format: date-time
                    nullable: true
                  suspended_at:
                    type: string
                    format: date-time
                    nullable: true
                  permissions:
                    type: array
                    description: Aggregated permission keys granted for the caller's customer
                    items:
                      type: string
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /subusers/setup:
    get:
      tags:
        - Subusers
      summary: Validate setup token
      description: Validates a subuser setup token generated during registration.
      operationId: validateSubuserSetupToken
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: One-time setup token received via SMS
      responses:
        '200':
          description: Token is valid
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Token is valid
                  subuser_id:
                    type: integer
                    example: 42
        '400': { $ref: '#/components/responses/BadRequest' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    post:
      tags:
        - Subusers
      summary: Complete subuser setup
      description: |
        Completes subuser setup by setting a password and basic profile fields. Accepts optional
        `username` and `email`.
      operationId: completeSubuserSetup
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
                - password
                - name
              properties:
                token:
                  type: string
                  description: One-time setup token
                password:
                  type: string
                  format: password
                  minLength: 8
                  description: Must include at least one uppercase letter, one lowercase letter, and one number
                  pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$'
                name:
                  type: string
                  minLength: 3
                  maxLength: 255
                username:
                  type: string
                  minLength: 3
                  maxLength: 255
                email:
                  type: string
                  format: email
                  minLength: 3
                  maxLength: 255
      responses:
        '200':
          description: Setup completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Password set successfully
        '400': { $ref: '#/components/responses/BadRequest' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /subusers/auth/password:
    post:
      tags:
        - Subusers
      summary: Authenticate subuser with password
      description: |
        Authenticates a subuser (driver) using a password together with one of the supported
        identifiers: `phone_country_code` + `phone`, `subuser_id`, or `username`.
        
        On success, returns a newly generated session token for the subuser.
      operationId: subuserPasswordAuth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [phone_country_code, phone, password]
                  properties:
                    phone_country_code:
                      type: integer
                      description: Phone country code (1–3 digits)
                      minimum: 1
                      maximum: 999
                      example: 45
                    phone:
                      type: integer
                      description: Phone number (4–15 digits, no leading +)
                      minimum: 1000
                      maximum: 999999999999999
                      example: 12345678
                    password:
                      type: string
                      format: password
                      minLength: 8
                      maxLength: 255
                - type: object
                  required: [subuser_id, password]
                  properties:
                    subuser_id:
                      type: integer
                      description: Subuser ID
                      example: 42
                    password:
                      type: string
                      format: password
                      minLength: 8
                      maxLength: 255
                - type: object
                  required: [username, password]
                  properties:
                    username:
                      type: string
                      minLength: 3
                      maxLength: 255
                      example: jdoe
                    password:
                      type: string
                      format: password
                      minLength: 8
                      maxLength: 255
            examples:
              withPhone:
                summary: Authenticate with phone
                value:
                  phone_country_code: 45
                  phone: 12345678
                  password: MySecureP@ssw0rd
              withSubuserId:
                summary: Authenticate with subuser_id
                value:
                  subuser_id: 42
                  password: MySecureP@ssw0rd
              withUsername:
                summary: Authenticate with username
                value:
                  username: jdoe
                  password: MySecureP@ssw0rd
      responses:
        '200':
          description: Authentication successful
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [session]
                    properties:
                      session:
                        type: string
                        description: Newly generated subuser session token
                        example: "2f7a8c0e-9b1d-4c6a-91a9-1a2b3c4d5e6f"
                  - type: object
                    required: [2fa_required, 2fa_token]
                    properties:
                      2fa_required:
                        type: boolean
                        example: true
                      2fa_token:
                        type: string
                        description: Temporary 2FA verification token
                        example: "557a3e7b1a2b..."
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /subusers/grants:
    get:
      tags:
        - Subusers
      summary: List subuser grants
      description: Returns subuser grant records filtered by `customer_number` and/or `subuser_id`.
      operationId: listSubuserGrants
      security:
        - BearerAuth: []
      parameters:
        - name: customer_number
          in: query
          required: false
          schema:
            type: integer
          description: e-conomic customer number to filter by
        - name: subuser_id
          in: query
          required: false
          schema:
            type: integer
          description: Subuser ID to filter by
      responses:
        '200':
          description: Grants fetched
          content:
            application/json:
              schema:
                type: object
                properties:
                  grants:
                    type: array
                    items:
                      $ref: '#/components/schemas/SubuserGrant'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    post:
      tags:
        - Subusers
      summary: Create subuser grant
      operationId: createSubuserGrant
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubuserGrantCreateRequest'
            examples:
              default:
                value:
                  customer_number: 1000
                  subuser_id: 42
                  enabled: true
                  note: "Grant for bookings access"
                  permissions: ["BOOKINGS_LIST", "BOOKINGS_ADD", "BOOKINGS_EDIT"]
      responses:
        '200':
          description: Grant created
          content:
            application/json:
              schema:
                type: object
                properties:
                  grant:
                    $ref: '#/components/schemas/SubuserGrant'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /subusers/grants/{id}:
    patch:
      tags:
        - Subusers
      summary: Update subuser grant
      operationId: updateSubuserGrant
      security:
        - BearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubuserGrantUpdateRequest'
            examples:
              enableOnly:
                value:
                  enabled: true
              updatePermissions:
                value:
                  permissions: ["VEHICLES_LIST", "SELFSERVE_ADD"]
      responses:
        '200':
          description: Grant updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  grant:
                    $ref: '#/components/schemas/SubuserGrant'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    delete:
      tags:
        - Subusers
      summary: Delete subuser grant
      operationId: deleteSubuserGrant
      security:
        - BearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Grant deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Grant deleted
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /subusers/permission-nodes:
    get:
      tags:
        - Subusers
      summary: List available subuser permission nodes
      description: Returns grouped permission nodes available for subuser grants.
      operationId: listSubuserPermissionNodes
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Permission nodes fetched
          content:
            application/json:
              schema:
                type: object
                properties:
                  permission_nodes:
                    type: array
                    items:
                      $ref: '#/components/schemas/PermissionNodeGroup'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  # Authentication Endpoints
  /auth/login:
    post:
      tags:
        - Authentication
      summary: Customer login
      description: Authenticate a customer using customer number and password
      operationId: customerLogin
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customer_number
                - password
                - g_recaptcha_response
              properties:
                customer_number:
                  type: integer
                  description: Customer's e-conomic customer number
                  example: 12345
                password:
                  type: string
                  format: password
                  description: Customer password
                  minLength: 1
                g_recaptcha_response:
                  type: string
                  description: reCAPTCHA verification token
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [token]
                    properties:
                      token:
                        type: string
                        description: Bearer authentication token
                        example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
                  - type: object
                    required: [2fa_required, 2fa_token]
                    properties:
                      2fa_required:
                        type: boolean
                        example: true
                      2fa_token:
                        type: string
                        description: Temporary 2FA verification token
                        example: "557a3e7b1a2b..."
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/employee/login:
    post:
      tags:
        - Authentication
      summary: Employee login
      description: Authenticate an employee using user ID and password
      operationId: employeeLogin
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - user_id
                - password
                - g_recaptcha_response
              properties:
                user_id:
                  type: integer
                  description: Employee user ID
                  example: 1
                password:
                  type: string
                  format: password
                  description: Employee password
                g_recaptcha_response:
                  type: string
                  description: reCAPTCHA verification token
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [token]
                    properties:
                      token:
                        type: string
                        description: Bearer authentication token
                  - type: object
                    required: [2fa_required, 2fa_token]
                    properties:
                      2fa_required:
                        type: boolean
                        example: true
                      2fa_token:
                        type: string
                        description: Temporary 2FA verification token
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/passkey/challenge:
    post:
      tags:
        - Authentication
      summary: Initiate passkey authentication challenge
      description: Generates a WebAuthn PublicKeyCredentialRequestOptions payload. If customer_number is provided, allowCredentials will be populated with existing passkeys for that account. Otherwise, a challenge is issued for discoverable credentials.
      operationId: passkeyChallenge
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - g_recaptcha_response
              properties:
                customer_number:
                  type: integer
                  description: Optional customer's e-conomic customer number
                  example: 12345
                g_recaptcha_response:
                  type: string
                  description: reCAPTCHA verification token
      responses:
        '200':
          description: Challenge generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  challenge_token:
                    type: string
                    description: Temporary token binding the challenge to the login attempt
                  publicKey:
                    type: object
                    properties:
                      challenge:
                        type: string
                        description: Base64URL-encoded challenge
                      rpId:
                        type: string
                        description: Relying party ID (truckwash.io or localhost)
                        example: truckwash.io
                      timeout:
                        type: integer
                        description: Timeout in milliseconds
                      userVerification:
                        type: string
                        enum: [required, preferred, discouraged]
                      allowCredentials:
                        type: array
                        items:
                          type: object
                          properties:
                            type:
                              type: string
                              example: public-key
                            id:
                              type: string
                              description: Base64URL-encoded credential ID
                            transports:
                              type: array
                              items:
                                type: string
        '400':
          $ref: '#/components/responses/BadRequest'

  /auth/passkey/verify:
    post:
      tags:
        - Authentication
      summary: Verify passkey authentication and start session
      description: Verifies the WebAuthn assertion and challenge token. Returns a session token on success.
      operationId: passkeyVerify
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - challenge_token
                - credential
                - g_recaptcha_response
              properties:
                challenge_token:
                  type: string
                  description: The token returned by the challenge endpoint
                g_recaptcha_response:
                  type: string
                  description: reCAPTCHA verification token
                credential:
                  type: object
                  description: The WebAuthn PublicKeyCredential object (assertion)
                  required:
                    - id
                    - rawId
                    - type
                    - response
                  properties:
                    id:
                      type: string
                      description: The credential ID (base64url)
                    rawId:
                      type: string
                      description: The raw credential ID (base64url)
                    type:
                      type: string
                      example: public-key
                    clientExtensionResults:
                      type: object
                    response:
                      type: object
                      required:
                        - clientDataJSON
                        - authenticatorData
                        - signature
                      properties:
                        clientDataJSON:
                          type: string
                          description: Base64URL-encoded client data
                        authenticatorData:
                          type: string
                          description: Base64URL-encoded authenticator data
                        signature:
                          type: string
                          description: Base64URL-encoded signature
                        userHandle:
                          type: string
                          nullable: true
                          description: Base64URL-encoded user handle
      responses:
        '200':
          description: Verification successful, session started
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [token]
                    properties:
                      token:
                        type: string
                        description: Bearer token for customer
                  - type: object
                    required: [session]
                    properties:
                      session:
                        type: string
                        description: Session token for subuser
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/logout:
    get:
      tags:
        - Authentication
      summary: Logout
      description: Invalidate the current authentication token
      operationId: logout
      responses:
        '200':
          description: Logout successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Logged out"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/session:
    get:
      tags:
        - Authentication
      summary: Get current session
      description: Retrieve information about the current authenticated user session
      operationId: getSession
      responses:
        '200':
          description: Session information retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/User'
                  - type: object
                    properties:
                      two_factor_enabled:
                        type: boolean
                        description: Indicates if 2FA is enabled for this account
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/2fa/setup:
    post:
      tags:
        - Authentication
      summary: Generate 2FA secret
      description: Generate a new TOTP secret for the authenticated user/subuser
      operationId: setup2fa
      responses:
        '200':
          description: 2FA secret generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret:
                    type: string
                    description: The base32 encoded TOTP secret
                  qr_code_url:
                    type: string
                    description: An otpauth URL for generating a QR code
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/2fa/enable:
    post:
      tags:
        - Authentication
      summary: Enable 2FA
      description: Verify a code and enable 2FA for the authenticated user/subuser
      operationId: enable2fa
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  description: The 6-digit TOTP code
      responses:
        '200':
          description: 2FA enabled successfully
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/2fa/disable:
    post:
      tags:
        - Authentication
      summary: Disable 2FA
      description: Verify a code and disable 2FA for the authenticated user/subuser
      operationId: disable2fa
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  description: The 6-digit TOTP code
      responses:
        '200':
          description: 2FA disabled successfully
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/2fa/verify:
    post:
      tags:
        - Authentication
      summary: Verify 2FA code during login
      description: Complete the login process by verifying the 2FA code
      operationId: verify2fa
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [2fa_token, code]
              properties:
                2fa_token:
                  type: string
                  description: The temporary 2FA verification token
                code:
                  type: string
                  description: The 6-digit TOTP code
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [token]
                    properties:
                      token:
                        type: string
                        description: Bearer authentication token (for users/employees)
                  - type: object
                    required: [session]
                    properties:
                      session:
                        type: string
                        description: Session token (for subusers)
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/reCAPTCHA/public:
    get:
      tags:
        - Authentication
      summary: Get reCAPTCHA configuration
      description: Retrieve public reCAPTCHA configuration for login forms
      operationId: getRecaptchaConfig
      security: []
      responses:
        '200':
          description: reCAPTCHA configuration retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  rate_limit:
                    type: object
                    properties:
                      enabled:
                        type: boolean
                      limit:
                        type: integer
                      remaining:
                        type: integer
                      reset:
                        type: integer
                      warning:
                        type: string
                        nullable: true
                  recaptcha:
                    type: object

  /auth/register/cvr:
    post:
      tags:
        - Authentication
      summary: Register new customer by CVR
      description: Register a new customer account using Danish CVR number
      operationId: registerCustomerByCvr
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - cvr
                - companyPhone
                - invoiceEmail
                - contactEmail
                - contactPhone
                - contactName
                - g_recaptcha_response
              properties:
                cvr:
                  type: string
                  description: Danish CVR number
                  minLength: 8
                  maxLength: 20
                  example: "44794780"
                companyPhone:
                  type: integer
                  description: Company phone number
                  minimum: 10000000
                  maximum: 9999999999
                  example: 21754690
                invoiceEmail:
                  type: string
                  format: email
                  description: Email for invoices
                  minLength: 5
                  maxLength: 255
                  example: "invoice@company.dk"
                contactEmail:
                  type: string
                  format: email
                  description: Contact email
                  minLength: 5
                  maxLength: 255
                  example: "contact@company.dk"
                contactPhone:
                  type: integer
                  description: Contact phone number
                  minimum: 10000000
                  maximum: 9999999999
                  example: 21754690
                contactName:
                  type: string
                  description: Contact person name
                  example: "Mikkel"
                g_recaptcha_response:
                  type: string
                  description: reCAPTCHA verification token
      responses:
        '201':
          description: Customer registered successfully
        '400':
          $ref: '#/components/responses/BadRequest'

  /auth/password-reset/request:
    post:
      tags:
        - Authentication
      summary: Request a customer password reset email
      description: Send an email with a password reset token to the customer's email address
      operationId: requestPasswordReset
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customer_number
                - g_recaptcha_response
              properties:
                customer_number:
                  type: integer
                  description: The customer number
                  example: 123456
                g_recaptcha_response:
                  type: string
                  description: reCAPTCHA verification token
      responses:
        '200':
          description: Request processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'

  /auth/password-reset/validate:
    get:
      tags:
        - Authentication
      summary: Validate a customer password reset key
      description: Check if a password reset token is valid and hasn't expired
      operationId: validatePasswordResetToken
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: The password reset token
      responses:
        '200':
          description: Token is valid
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  customer_id:
                    type: integer
        '404':
          description: Invalid or expired token

  /auth/password-reset/set:
    post:
      tags:
        - Authentication
      summary: Set a customer password using a reset key
      description: Update the customer password using a valid reset token
      operationId: setPasswordUsingResetToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
                - password
                - g_recaptcha_response
              properties:
                token:
                  type: string
                  description: The password reset token
                password:
                  type: string
                  description: The new password
                g_recaptcha_response:
                  type: string
                  description: reCAPTCHA verification token
      responses:
        '200':
          description: Password updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Invalid or expired token

  /su/intimidate:
    post:
      tags:
        - Authentication
      summary: Intimidate a user
      description: Create an authentication token for another user (Superuser only)
      operationId: suIntimidate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id]
              properties:
                user_id: {type: integer}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: {type: string}

  # User Endpoints
  /users:
    get:
      tags:
        - Users
      summary: List users
      description: Retrieve a paginated list of users
      operationId: listUsers
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
      responses:
        '200':
          description: Users retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Users
      summary: Create new user
      description: Create a new user account
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserCreate'
      responses:
        '201':
          description: User created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
    put:
      tags:
        - Users
      summary: Update user
      description: Update an existing user
      operationId: updateUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserUpdate'
      responses:
        '200':
          description: User updated successfully
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /users/customer:
    get:
      tags:
        - Users
      summary: Get customer details
      description: Get details about a specific customer
      operationId: getCustomer
      parameters:
        - name: customer_number
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Customer retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          $ref: '#/components/responses/NotFound'

  /superuser/user:
    get:
      tags:
        - Users
      summary: Get user by ID (superuser)
      description: Get detailed user information by user ID
      operationId: getSuperuserUser
      parameters:
        - name: user_id
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: User retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/customer/code:
    get:
      tags:
        - Users
      summary: Get customer code
      operationId: getCustomerCode
      parameters:
        - name: customer_number
          in: query
          schema: {type: integer}
        - name: user_id
          in: query
          schema: {type: integer}
      responses:
        '200':
          description: Success
    post:
      tags:
        - Users
      summary: Add customer code
      operationId: addCustomerCode
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_number: {type: integer}
                user_id: {type: integer}
                code: {type: string}
      responses:
        '200':
          description: Success

  /customer/department/default:
    get:
      tags:
        - Users
      summary: Get customer default department
      operationId: getCustomerDefaultDepartment
      parameters:
        - name: customer_number
          in: query
          schema: {type: integer}
      responses:
        '200':
          description: Success
    post:
      tags:
        - Users
      summary: Add customer default department
      operationId: addCustomerDefaultDepartment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department]
              properties:
                customer_number: {type: integer}
                department: {type: integer}
      responses:
        '200':
          description: Success
    delete:
      tags:
        - Users
      summary: Delete customer default department
      operationId: deleteCustomerDefaultDepartment
      parameters:
        - name: customer_number
          in: query
          schema: {type: integer}
      responses:
        '200':
          description: Success

  /customer/pricing/fixed:
    get:
      tags:
        - Users
      summary: Get customer fixed pricing
      operationId: getCustomerFixedPricing
      parameters:
        - name: customer_number
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success
    post:
      tags:
        - Users
      summary: Add customer fixed pricing
      operationId: addCustomerFixedPricing
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_number, price, description]
              properties:
                customer_number: {type: integer}
                price: {type: integer}
                description: {type: string}
      responses:
        '200':
          description: Success
    delete:
      tags:
        - Users
      summary: Delete customer fixed pricing
      operationId: deleteCustomerFixedPricing
      parameters:
        - name: customer_number
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success

  /account/notifications:
    put:
      tags:
        - Users
      summary: Update user notification settings
      operationId: updateUserNotifications
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                wash_certificate_email: {type: string}
                sms_notifications_enabled: {type: boolean}
                email_notifications_enabled: {type: boolean}
      responses:
        '200':
          description: Success

  /user/permissions:
    get:
      tags:
        - Users
      summary: Get user permissions
      operationId: getUserPermissions
      responses:
        '200':
          description: Success

  /customers:
    get:
      tags:
        - Users
      summary: List customers
      operationId: listCustomers
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
      responses:
        '200':
          description: Success

  /superuser/user/discounts:
    get:
      tags:
        - Users
      summary: Get user discounts
      operationId: getUserDiscounts
      parameters:
        - name: user_id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success
    post:
      tags:
        - Users
      summary: Set user discount
      operationId: setUserDiscount
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [discount, object_id, is_category]
              properties:
                user_id: {type: integer}
                discount: {type: integer}
                object_id: {type: string}
                is_category: {type: boolean}
      responses:
        '200':
          description: Success

  /superuser/user/keys:
    get:
      tags:
        - Users
      summary: Get user keys
      operationId: getUserKeys
      parameters:
        - name: user_id
          in: query
          required: true
          schema: {type: integer}
        - name: key
          in: query
          schema: {type: string}
      responses:
        '200':
          description: Success
    post:
      tags:
        - Users
      summary: Set user key
      operationId: setUserKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key, value]
              properties:
                user_id: {type: integer}
                key: {type: string}
                value: {type: string}
      responses:
        '200':
          description: Success

  /superuser/user/password:
    post:
      tags:
        - Users
      summary: Set user password
      operationId: setUserPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                user_id: {type: integer}
                password: {type: string}
      responses:
        '200':
          description: Success

  /admin/customer/getUserId:
    get:
      tags:
        - Users
      summary: Get user ID from customer number
      description: Convert e-conomic customer number to internal user ID
      operationId: getUserIdFromCustomerNumber
      parameters:
        - name: customer_number
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: User ID retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id:
                    type: integer

  /admin/customer/name:
    get:
      tags:
        - Users
      summary: Get customer name
      description: Get the full name of a customer
      operationId: getCustomerName
      parameters:
        - name: user_id
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Customer name retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string

  # Orders Endpoints
  /orders:
    get:
      tags:
        - Orders
      summary: List orders
      description: Retrieve a paginated list of orders
      operationId: listOrders
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
        - name: show_wash_subscription
          in: query
          schema:
            type: string
            enum: [true, false]
      responses:
        '200':
          description: Orders retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
        - Orders
      summary: Create new order
      description: Create a new wash order
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreate'
      responses:
        '201':
          description: Order created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      tags:
        - Orders
      summary: Delete order
      description: Delete an existing order
      operationId: deleteOrder
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Order deleted successfully
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
        - Orders
      summary: Update order (alias)
      description: Update an existing order
      operationId: updateOrders
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderUpdate'
      responses:
        '200':
          description: Order updated successfully
        '400':
          $ref: '#/components/responses/BadRequest'

  /order:
    get:
      tags:
        - Orders
      summary: Get order details
      description: Get detailed information about a specific order
      operationId: getOrder
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Order retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
        - Orders
      summary: Update order
      description: Update an existing order
      operationId: updateOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderUpdate'
      responses:
        '200':
          description: Order updated successfully
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /user/orders:
    get:
      tags:
        - Orders
      summary: Get current user's orders
      description: Retrieve orders for the authenticated user
      operationId: getUserOrders
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Orders retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Order'

  /user/order:
    get:
      tags:
        - Orders
      summary: Get user's specific order
      description: Get details of a specific order for the authenticated user
      operationId: getUserOrder
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Order retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'

  /orders/mark_as_completed:
    post:
      tags:
        - Orders
      summary: Mark order as completed
      description: Mark an order as completed
      operationId: markOrderCompleted
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
              properties:
                id:
                  type: integer
      responses:
        '200':
          description: Order marked as completed successfully
        '400':
          $ref: '#/components/responses/BadRequest'

  /order/wash-certificate:
    post:
      tags:
        - Orders
      summary: Generate wash certificate
      description: Generate a wash certificate for an order
      operationId: generateWashCertificate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                order_id:
                  type: integer
      responses:
        '200':
          description: Wash certificate generated successfully
        '400':
          $ref: '#/components/responses/BadRequest'

  # Order Items Endpoints
  /order/items:
    get:
      tags:
        - Order Items
      summary: List order items
      description: Get all items for a specific order
      operationId: listOrderItems
      parameters:
        - name: order_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Order items retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/OrderItem'
    post:
      tags:
        - Order Items
      summary: Add item to order
      description: Add a new item to an existing order
      operationId: addOrderItem
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderItemCreate'
      responses:
        '201':
          description: Order item added successfully
        '400':
          $ref: '#/components/responses/BadRequest'
    put:
      tags:
        - Order Items
      summary: Update order item
      description: Update an existing order item
      operationId: updateOrderItem
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderItemUpdate'
      responses:
        '200':
          description: Order item updated successfully
        '400':
          $ref: '#/components/responses/BadRequest'
    delete:
      tags:
        - Order Items
      summary: Delete order item
      description: Remove an item from an order
      operationId: deleteOrderItem
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Order item deleted successfully
        '404':
          $ref: '#/components/responses/NotFound'

  # Departments Endpoints
  /departments:
    get:
      tags:
        - Departments
      summary: List departments
      description: Retrieve a list of all visible departments
      operationId: listDepartments
      parameters:
        - name: id
          in: query
          schema:
            type: integer
          description: Filter by specific department ID
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
      responses:
        '200':
          description: Departments retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Department'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Departments
      summary: Create department
      description: Create a new department
      operationId: createDepartment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentCreate'
      responses:
        '201':
          description: Department created successfully
        '400':
          $ref: '#/components/responses/BadRequest'
    put:
      tags:
        - Departments
      summary: Update department
      description: Update an existing department
      operationId: updateDepartment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentUpdate'
      responses:
        '200':
          description: Department updated successfully
        '400':
          $ref: '#/components/responses/BadRequest'

  /departments/categories:
    get:
      tags:
        - Departments
      summary: Get department categories
      description: Get product categories available in a department
      operationId: getDepartmentCategories
      parameters:
        - name: department_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Department categories retrieved successfully
    post:
      tags:
        - Departments
      summary: Add category to department
      description: Associate a product category with a department
      operationId: addDepartmentCategory
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                department_id:
                  type: integer
                category_id:
                  type: integer
      responses:
        '201':
          description: Category added to department successfully
    delete:
      tags:
        - Departments
      summary: Remove category from department
      operationId: removeDepartmentCategory
      responses:
        '200': {description: Success}

  /departments/self-serve/enabled:
    get:
      tags:
        - Departments
      summary: Get department self-serve status
      description: Check if self-serve is enabled for a specific department
      operationId: getDepartmentSelfServeEnabled
      parameters:
        - name: id
          in: query
          required: true
          description: Department ID
          schema:
            type: integer
      responses:
        '200':
          description: Successfully retrieved status
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
        - Departments
      summary: Update department self-serve status
      description: Enable or disable self-serve for a specific department
      operationId: updateDepartmentSelfServeEnabled
      parameters:
        - name: id
          in: query
          required: true
          description: Department ID
          schema:
            type: integer
        - name: enabled
          in: query
          required: true
          description: Enabled status (true/false)
          schema:
            type: string
            enum: ['true', 'false']
      responses:
        '200':
          description: Status updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '404':
          $ref: '#/components/responses/NotFound'

  /departments/order/recommended:
    get:
      tags:
        - Departments
      summary: Get recommended order for department
      operationId: getDepartmentRecommendedOrder
      parameters:
        - name: department_id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200': {description: Success}

  /department/lanes:
    get:
      tags:
        - Departments
      summary: List department lanes
      description: Retrieve a list of all department lanes
      operationId: listDepartmentLanes
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
      responses:
        '200':
          description: Department lanes retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DepartmentLane'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Departments
      summary: Create department lane
      description: Create a new department lane
      operationId: createDepartmentLane
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentLaneCreate'
      responses:
        '201':
          description: Department lane created successfully
        '400':
          $ref: '#/components/responses/BadRequest'
    put:
      tags:
        - Departments
      summary: Update department lane
      description: Update an existing department lane
      operationId: updateDepartmentLane
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentLaneUpdate'
      responses:
        '200':
          description: Department lane updated successfully
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /department/gates:
    get:
      tags:
        - Departments
      summary: List department gates
      description: Retrieve department gates, optionally filtered by id
      operationId: listDepartmentGates
      parameters:
        - name: id
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
      responses:
        '200':
          description: Department gates retrieved successfully
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/DepartmentGate'
                  - type: array
                    items:
                      $ref: '#/components/schemas/DepartmentGate'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Departments
      summary: Create department gate
      operationId: createDepartmentGate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentGateCreate'
      responses:
        '201':
          description: Department gate created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentGate'
    put:
      tags:
        - Departments
      summary: Update department gate
      operationId: updateDepartmentGate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentGateUpdate'
      responses:
        '200':
          description: Department gate updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentGate'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
        - Departments
      summary: Delete department gate
      operationId: deleteDepartmentGate
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Department gate deleted

  /department/relays:
    get:
      tags:
        - Departments
      summary: List department relays
      description: Retrieve department relays, optionally filtered by id
      operationId: listDepartmentRelays
      parameters:
        - name: id
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
      responses:
        '200':
          description: Department relays retrieved successfully
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/DepartmentRelay'
                  - type: array
                    items:
                      $ref: '#/components/schemas/DepartmentRelay'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Departments
      summary: Create department relay
      operationId: createDepartmentRelay
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentRelayCreate'
      responses:
        '201':
          description: Department relay created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentRelay'
    put:
      tags:
        - Departments
      summary: Update department relay
      operationId: updateDepartmentRelay
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentRelayUpdate'
      responses:
        '200':
          description: Department relay updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentRelay'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
        - Departments
      summary: Delete department relay
      operationId: deleteDepartmentRelay
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Department relay deleted

  /department/lanes/dynamic-image:
    get:
      tags:
        - Departments
      summary: Generate dynamic image for a department lane
      description: |
        Returns a composed machine UI image for the specified department lane.
        You can optionally highlight button indices, set the current step indicator, and toggle only-current-step mode.
      operationId: getDepartmentLaneDynamicImage
      parameters:
        - name: department
          in: query
          required: true
          description: Department ID
          schema:
            type: integer
            minimum: 1
        - name: lane
          in: query
          required: true
          description: Lane ID
          schema:
            type: integer
            minimum: 1
        - name: buttons
          in: query
          required: false
          description: Highlighted button IDs (0-indexed). Accepts CSV, JSON array, or repeated query params.
          schema:
            oneOf:
              - type: string
              - type: array
                items:
                  type: integer
        - name: current_step
          in: query
          required: false
          description: Current step indicator (non-negative integer)
          schema:
            type: integer
            minimum: 0
        - name: only_current_step
          in: query
          required: false
          description: If true, only draw the current step highlight
          schema:
            type: boolean
        - name: vehicle_type
          in: query
          required: false
          description: Vehicle type selection override (nullable non-negative integer)
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: Dynamic image rendered successfully
          content:
            image/png:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /guest/validation/customer-number:
    post:
      tags:
        - Users
      summary: Validate customer number
      description: Check if a customer number is valid and exists
      operationId: validateCustomerNumber
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - customer_number
              properties:
                customer_number:
                  type: integer
      responses:
        '200':
          description: Customer number validation successful
        '400':
          $ref: '#/components/responses/BadRequest'

  /guest/departments:
    get:
      tags:
        - Departments
      summary: List public departments
      description: Get list of departments without authentication
      operationId: listGuestDepartments
      security: []
      parameters:
        - name: include_lanes
          in: query
          description: Whether to include lane status and self-serve information
          schema:
            type: boolean
      responses:
        '200':
          description: Departments retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DepartmentGuest'

  /department/selfserve/questions:
    get:
      tags:
        - Self-Serve
      summary: List self-serve questions
      description: Retrieve a list of self-serve questions for a department, lane, or product.
      operationId: listSelfserveQuestions
      parameters:
        - name: id
          in: query
          description: Filter by question ID
          schema:
            type: integer
        - name: department
          in: query
          description: Filter by department ID
          schema:
            type: integer
        - name: lane
          in: query
          description: Filter by lane ID
          schema:
            type: integer
        - name: product
          in: query
          description: Filter by product ID
          schema:
            type: integer
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
        - $ref: '#/components/parameters/FiltersParam'
      responses:
        '200':
          description: Successfully retrieved questions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DepartmentSelfserveQuestion'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      tags:
        - Self-Serve
      summary: Add self-serve question
      description: Add a new self-serve question.
      operationId: addSelfserveQuestion
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - department
                - lane
                - product
                - question
                - description
              properties:
                department:
                  type: integer
                lane:
                  type: integer
                product:
                  type: integer
                question:
                  type: string
                description:
                  type: string
                condition_id:
                  type: integer
                  nullable: true
                order_priority:
                  type: integer
                  default: 0
      responses:
        '200':
          description: Successfully added question
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveQuestion'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'

    put:
      tags:
        - Self-Serve
      summary: Update self-serve question
      description: Update an existing self-serve question.
      operationId: updateSelfserveQuestion
      parameters:
        - name: id
          in: query
          required: true
          description: Question ID
          schema:
            type: integer
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                department:
                  type: integer
                lane:
                  type: integer
                product:
                  type: integer
                question:
                  type: string
                description:
                  type: string
                condition_id:
                  type: integer
                  nullable: true
                order_priority:
                  type: integer
      responses:
        '200':
          description: Successfully updated question
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveQuestion'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags:
        - Self-Serve
      summary: Delete self-serve question
      description: Delete a self-serve question by ID.
      operationId: deleteSelfserveQuestion
      parameters:
        - name: id
          in: query
          required: true
          description: Question ID
          schema:
            type: integer
      responses:
        '200':
          description: Successfully deleted question
          content:
            application/json:
              schema:
                type: string
                example: Question deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /department/selfserve/conditions:
    get:
      tags:
        - Self-Serve
      summary: List self-serve conditions
      description: Retrieve a list of self-serve conditions for a department, lane, or product.
      operationId: listSelfserveConditions
      parameters:
        - name: id
          in: query
          description: Filter by condition ID
          schema:
            type: integer
        - name: department
          in: query
          description: Filter by department ID
          schema:
            type: integer
        - name: lane
          in: query
          description: Filter by lane ID
          schema:
            type: integer
        - name: product
          in: query
          description: Filter by product ID
          schema:
            type: integer
        - name: condition_id
          in: query
          description: Filter by condition ID
          schema:
            type: integer
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
        - $ref: '#/components/parameters/FiltersParam'
      responses:
        '200':
          description: Successfully retrieved conditions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DepartmentSelfserveCondition'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      tags:
        - Self-Serve
      summary: Add self-serve condition
      description: Add a new self-serve condition.
      operationId: addSelfserveCondition
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - department
                - lane
                - product
                - name
                - description
              properties:
                department:
                  type: integer
                lane:
                  type: integer
                product:
                  type: integer
                condition_id:
                  type: integer
                  nullable: true
                name:
                  type: string
                description:
                  type: string
      responses:
        '200':
          description: Successfully added condition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveCondition'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'

    put:
      tags:
        - Self-Serve
      summary: Update self-serve condition
      description: Update an existing self-serve condition.
      operationId: updateSelfserveCondition
      parameters:
        - name: id
          in: query
          required: true
          description: Condition ID
          schema:
            type: integer
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                department:
                  type: integer
                lane:
                  type: integer
                product:
                  type: integer
                condition_id:
                  type: integer
                  nullable: true
                name:
                  type: string
                description:
                  type: string
      responses:
        '200':
          description: Successfully updated condition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveCondition'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    delete:
      tags:
        - Self-Serve
      summary: Delete self-serve condition
      description: Delete a self-serve condition.
      operationId: deleteSelfserveCondition
      parameters:
        - name: id
          in: query
          required: true
          description: Condition ID
          schema:
            type: integer
      responses:
        '200':
          description: Successfully deleted condition
          content:
            application/json:
              schema:
                type: string
                example: Condition deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /department/selfserve/condition/rules:
    get:
      tags:
        - Self-Serve
      summary: List self-serve condition rules
      description: Retrieve a list of self-serve condition rules.
      operationId: listSelfserveConditionRules
      parameters:
        - name: id
          in: query
          description: Filter by rule ID
          schema:
            type: integer
        - name: condition_id
          in: query
          description: Filter by condition ID
          schema:
            type: integer
        - name: type
          in: query
          description: Filter by rule type
          schema:
            type: string
        - name: object_type
          in: query
          description: Filter by object type
          schema:
            type: string
        - name: object_id
          in: query
          description: Filter by object ID
          schema:
            type: integer
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
        - $ref: '#/components/parameters/FiltersParam'
      responses:
        '200':
          description: Successfully retrieved condition rules
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DepartmentSelfserveConditionRule'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      tags:
        - Self-Serve
      summary: Add self-serve condition rule
      description: Add a new self-serve condition rule.
      operationId: addSelfserveConditionRule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - condition_id
                - type
                - object_type
                - object_id
                - name
                - description
              properties:
                condition_id:
                  type: integer
                type:
                  type: string
                object_type:
                  type: string
                object_id:
                  type: integer
                name:
                  type: string
                description:
                  type: string
      responses:
        '200':
          description: Successfully added condition rule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveConditionRule'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'

    put:
      tags:
        - Self-Serve
      summary: Update self-serve condition rule
      description: Update an existing self-serve condition rule.
      operationId: updateSelfserveConditionRule
      parameters:
        - name: id
          in: query
          required: true
          description: Rule ID
          schema:
            type: integer
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                condition_id:
                  type: integer
                type:
                  type: string
                object_type:
                  type: string
                object_id:
                  type: integer
                name:
                  type: string
                description:
                  type: string
      responses:
        '200':
          description: Successfully updated condition rule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveConditionRule'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    delete:
      tags:
        - Self-Serve
      summary: Delete self-serve condition rule
      description: Delete a self-serve condition rule.
      operationId: deleteSelfserveConditionRule
      parameters:
        - name: id
          in: query
          required: true
          description: Rule ID
          schema:
            type: integer
      responses:
        '200':
          description: Successfully deleted condition rule
          content:
            application/json:
              schema:
                type: string
                example: Rule deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /department/selfserve/vehicle/conditions:
    get:
      tags:
        - Self-Serve
      summary: List vehicle conditions
      description: Retrieve a list of vehicle conditions for a department, lane, reg, or question. Customers will only see their own vehicle conditions.
      operationId: listSelfserveVehicleConditions
      parameters:
        - name: id
          in: query
          description: Filter by condition ID
          schema:
            type: integer
        - name: department
          in: query
          description: Filter by department ID
          schema:
            type: integer
        - name: lane
          in: query
          description: Filter by lane ID
          schema:
            type: integer
        - name: reg
          in: query
          description: Filter by vehicle registration number
          schema:
            type: string
        - name: question
          in: query
          description: Filter by question ID
          schema:
            type: integer
        - name: customer_id
          in: query
          description: Filter by customer ID
          schema:
            type: integer
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
        - $ref: '#/components/parameters/FiltersParam'
      responses:
        '200':
          description: Successfully retrieved vehicle conditions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DepartmentSelfserveVehicleCondition'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      tags:
        - Self-Serve
      summary: Add vehicle condition
      description: Add a new vehicle condition (answer to a question). Customers can only add conditions for their own vehicles.
      operationId: addSelfserveVehicleCondition
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - department
                - lane
                - reg
                - question
              properties:
                department:
                  type: integer
                lane:
                  type: integer
                reg:
                  type: string
                question:
                  type: integer
                value:
                  type: boolean
                customer_id:
                  type: integer
                  nullable: true
      responses:
        '200':
          description: Successfully added vehicle condition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveVehicleCondition'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'

    put:
      tags:
        - Self-Serve
      summary: Update vehicle condition
      description: Update an existing vehicle condition. Customers can only update conditions for their own vehicles.
      operationId: updateSelfserveVehicleCondition
      parameters:
        - name: id
          in: query
          required: true
          description: Condition ID
          schema:
            type: integer
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                department:
                  type: integer
                lane:
                  type: integer
                reg:
                  type: string
                question:
                  type: integer
                value:
                  type: boolean
                customer_id:
                  type: integer
                  nullable: true
      responses:
        '200':
          description: Successfully updated vehicle condition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveVehicleCondition'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    delete:
      tags:
        - Self-Serve
      summary: Delete vehicle condition
      description: Delete a vehicle condition. Customers can only delete conditions for their own vehicles.
      operationId: deleteSelfserveVehicleCondition
      parameters:
        - name: id
          in: query
          required: true
          description: Condition ID
          schema:
            type: integer
      responses:
        '200':
          description: Successfully deleted vehicle condition
          content:
            application/json:
              schema:
                type: string
                example: Condition deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /department/selfserve/tasks:
    get:
      tags:
        - Self-Serve
      summary: List self-serve tasks
      description: Retrieve a list of self-serve tasks for a department, lane, product, or condition_id.
      operationId: listSelfserveTasks
      parameters:
        - name: id
          in: query
          description: Filter by task ID
          schema:
            type: integer
        - name: department
          in: query
          description: Filter by department ID
          schema:
            type: integer
        - name: lane
          in: query
          description: Filter by lane ID
          schema:
            type: integer
        - name: product
          in: query
          description: Filter by product ID
          schema:
            type: integer
        - name: condition_id
          in: query
          description: Filter by condition ID
          schema:
            type: integer
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
        - $ref: '#/components/parameters/FiltersParam'
      responses:
        '200':
          description: Successfully retrieved tasks
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DepartmentSelfserveTask'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      tags:
        - Self-Serve
      summary: Add self-serve task
      description: Add a new self-serve task.
      operationId: addSelfserveTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - department
                - lane
                - product
                - task
                - description
              properties:
                department:
                  type: integer
                lane:
                  type: integer
                product:
                  type: integer
                condition_id:
                  type: integer
                  nullable: true
                task:
                  type: string
                description:
                  type: string
                order_priority:
                  type: integer
                  default: 0
                services:
                  type: array
                  description: Optional services enabled by this task. Items must be valid service enum names.
                  items:
                    $ref: '#/components/schemas/SelfserveLaneService'
                buttons:
                  type: array
                  description: Optional dynamic image button IDs enabled by this task.
                  items:
                    type: integer
                  default: []
                dynamic_images_vehicle_type:
                  type: integer
                  nullable: true
                  description: Optional vehicle type selection override for the machine UI. Integer >= 0 or null.
      responses:
        '200':
          description: Successfully added task
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveTask'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'

    put:
      tags:
        - Self-Serve
      summary: Update self-serve task
      description: Update an existing self-serve task.
      operationId: updateSelfserveTask
      parameters:
        - name: id
          in: query
          required: true
          description: Task ID
          schema:
            type: integer
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                department:
                  type: integer
                lane:
                  type: integer
                product:
                  type: integer
                condition_id:
                  type: integer
                  nullable: true
                task:
                  type: string
                description:
                  type: string
                order_priority:
                  type: integer
                services:
                  type: array
                  nullable: true
                  description: Services enabled by this task. Set to null to clear all services.
                  items:
                    $ref: '#/components/schemas/SelfserveLaneService'
                buttons:
                  type: array
                  nullable: true
                  description: Button IDs enabled by this task. Set to null to clear all buttons.
                  items:
                    type: integer
                dynamic_images_vehicle_type:
                  type: integer
                  nullable: true
                  description: Vehicle type selection override. Set to null to clear.
      responses:
        '200':
          description: Successfully updated task
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveTask'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags:
        - Self-Serve
      summary: Delete self-serve task
      description: Delete a self-serve task by ID.
      operationId: deleteSelfserveTask
      parameters:
        - name: id
          in: query
          required: true
          description: Task ID
          schema:
            type: integer
      responses:
        '200':
          description: Successfully deleted task
          content:
            application/json:
              schema:
                type: string
                example: Task deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /department/selfserve/tasks/attachments:
    get:
      tags:
        - Self-Serve
      summary: List task attachments
      description: Retrieve a list of attachments for a specific self-serve task.
      operationId: listSelfserveTaskAttachments
      parameters:
        - name: id
          in: query
          required: true
          description: Task ID
          schema:
            type: integer
      responses:
        '200':
          description: Successfully retrieved task attachments
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags:
        - Self-Serve
      summary: Delete task attachment
      description: Remove an attachment from a specific self-serve task.
      operationId: deleteSelfserveTaskAttachment
      parameters:
        - name: task_id
          in: query
          required: true
          description: Task ID
          schema:
            type: integer
        - name: attachment_id
          in: query
          required: true
          description: Attachment ID
          schema:
            type: integer
      responses:
        '200':
          description: Attachment deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Attachment deleted successfully
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /department/selfserve/tasks/attachments/upload:
    post:
      tags:
        - Self-Serve
      summary: Upload task attachment
      description: Upload a new attachment to a specific self-serve task using base64 encoding.
      operationId: uploadSelfserveTaskAttachment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - task_id
                - base64_file
                - file_name
              properties:
                task_id:
                  type: integer
                base64_file:
                  type: string
                  description: Base64 encoded file content
                file_name:
                  type: string
                  description: Name of the file including extension
      responses:
        '200':
          description: Attachment uploaded successfully
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /department/selfserve/tasks/attachments/download:
    get:
      tags:
        - Self-Serve
      summary: Download task attachment
      description: Generate a download link for a specific self-serve task attachment.
      operationId: downloadSelfserveTaskAttachment
      parameters:
        - name: task_id
          in: query
          required: true
          description: Task ID
          schema:
            type: integer
        - name: attachment_id
          in: query
          required: true
          description: Attachment ID
          schema:
            type: integer
      responses:
        '200':
          description: Successfully generated download link
          content:
            application/json:
              schema:
                type: object
                properties:
                  download_link:
                    type: string
                    format: uri
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  # Products Endpoints
  /products:
    get:
      tags:
        - Products
      summary: List products
      description: Retrieve a list of products with optional filters for customer pricing and department
      operationId: listProducts
      parameters:
        - name: customer_id
          in: query
          schema:
            type: integer
          description: Customer ID for custom pricing
        - name: department_id
          in: query
          schema:
            type: integer
          description: Department ID for department-specific pricing
        - name: category
          in: query
          schema:
            type: integer
          description: Filter by category ID
        - name: id
          in: query
          schema:
            type: integer
          description: Get specific product by ID
        - name: final_price
          in: query
          schema:
            type: boolean
          description: Whether to return final prices including discounts
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Products retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Product'
    post:
      tags:
        - Products
      summary: Create product
      description: Create a new product
      operationId: createProduct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductCreate'
      responses:
        '201':
          description: Product created successfully
        '400':
          $ref: '#/components/responses/BadRequest'
    put:
      tags:
        - Products
      summary: Update product
      description: Update an existing product
      operationId: updateProduct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductUpdate'
      responses:
        '200':
          description: Product updated successfully
        '400':
          $ref: '#/components/responses/BadRequest'

  # Categories Endpoints
  /categories:
    get:
      tags:
        - Categories
      summary: List categories
      description: Retrieve a list of product categories
      operationId: listCategories
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Categories retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Category'
    post:
      tags:
        - Categories
      summary: Create category
      description: Create a new product category
      operationId: createCategory
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryCreate'
      responses:
        '201':
          description: Category created successfully
    put:
      tags:
        - Categories
      summary: Update category
      description: Update an existing category
      operationId: updateCategory
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryUpdate'
      responses:
        '200':
          description: Category updated successfully

  # Bookings Endpoints
  /bookings:
    get:
      tags:
        - Bookings
      summary: List bookings
      description: Retrieve a list of bookings
      operationId: listBookings
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Bookings retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Booking'
    put:
      tags:
        - Bookings
      summary: Update booking
      description: Update an existing booking
      operationId: updateBooking
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookingUpdate'
      responses:
        '200':
          description: Booking updated successfully

  /user/bookings:
    get:
      tags:
        - Bookings
      summary: Get user bookings
      description: Retrieve bookings for the authenticated user
      operationId: getUserBookings
      responses:
        '200':
          description: User bookings retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Booking'


  /order-bookings:
    get:
      tags:
        - Bookings
      summary: List order bookings
      operationId: listOrderBookings
      parameters:
        - name: id
          in: query
          required: false
          schema: {type: integer}
      responses:
        '200':
          description: Success

  # Goals Endpoints
  /goals/department:
    get:
      tags:
        - Goals
      summary: List or get department goals
      description: |
        Retrieve a list of department goals or a single goal when `id` is provided.

        Access control:
        - A user may only access goals where the goal's `departments` set is a subset of the user's departments.
        - Users with the `superuser` permission may access all goals.
      operationId: listDepartmentGoals
      parameters:
        - name: id
          in: query
          required: false
          schema: { type: integer }
          description: When provided, returns the single goal with this id (if accessible)
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
        - $ref: '#/components/parameters/FiltersParam'
      responses:
        '200':
          description: Goals retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DepartmentGoal'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags:
        - Goals
      summary: Create department goal
      description: |
        Create a new department goal.

        Access control:
        - The provided `departments` must be a subset of the user's departments unless the user has `superuser`.
      operationId: createDepartmentGoal
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentGoalCreate'
      responses:
        '201':
          description: Department goal created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentGoal'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    put:
      tags:
        - Goals
      summary: Update department goal
      description: |
        Update an existing department goal by `id`.

        Access control:
        - The creator (`created_by`) may update regardless of department membership.
        - Otherwise the user must satisfy the same subset rule as for read access, and any new `departments` provided must also be a subset unless the user has `superuser`.
      operationId: updateDepartmentGoal
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentGoalUpdate'
      responses:
        '200':
          description: Department goal updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentGoal'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /goals/department/progress-alert/test:
    post:
      tags:
        - Goals
      summary: Send a test progress alert for a department goal
      description: |
        Sends a progress alert for a department goal to the destination defined in the goal's criteria.

        Permission required: `goals_department_progress_alert_test`.

        Access control:
        - The caller must be a superuser or belong to all departments targeted by the goal.

        Behavior:
        - Looks up the goal by `id`.
        - Rebuilds the criteria from stored JSON and attaches the goal's departments.
        - Renders the alert using the server-side renderer (respecting progress type/style/format and destination limits).
        - Sends the alert to Slack, Email, or SMS depending on `progress_alert_destination`, unless overridden.
      operationId: sendDepartmentGoalProgressAlertTest
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
              properties:
                id:
                  type: integer
                  description: The department goal id
                  example: 42
                overrideDestination:
                  type: string
                  description: Override the destination for this test
                  enum: [SLACK, EMAIL, SMS, NONE]
                  example: SLACK
                email_to:
                  type: string
                  format: email
                  description: Email recipient when destination is EMAIL
                  example: tester@example.com
                subject:
                  type: string
                  description: Optional email subject when destination is EMAIL
                  example: Dept Goal Progress Test
                sms_to:
                  description: One or more MSISDN recipients when destination is SMS
                  oneOf:
                    - type: string
                      description: Comma or semicolon separated list
                      example: "+4512345678, +4598765432"
                    - type: array
                      items:
                        type: string
                      example: ["+4512345678", "+4598765432"]
                slack_webhook:
                  type: string
                  description: Slack webhook URL when destination is SLACK
                  example: https://hooks.slack.com/services/T000/B000/XXX
                department_id:
                  type: integer
                  description: Department id to use that department's Slack webhook when destination is SLACK
                  example: 3
      responses:
        '200':
          description: Alert sent successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                    description: Goal id
                  destination:
                    type: string
                    description: Final destination used
                    enum: [SLACK, EMAIL, SMS, NONE]
                  target:
                    description: The target used for delivery (email address, phone numbers, department id, or webhook)
                  message_preview:
                    type: string
                    description: Rendered message preview
                  provider_response:
                    description: Provider-specific response or status message
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    delete:
      tags:
        - Goals
      summary: Delete department goal
      description: |
        Delete a department goal by `id`.

        Access control:
        - The creator (`created_by`) may delete regardless of department membership.
        - Otherwise the user must satisfy the subset rule or have `superuser`.
      operationId: deleteDepartmentGoal
      parameters:
        - name: id
          in: query
          required: true
          schema: { type: integer }
          description: ID of the goal to delete
      responses:
        '200':
          description: Department goal deleted successfully
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    '/order-bookings':
        post:
          tags:
            - Bookings
          summary: Create order booking
          operationId: createOrderBooking
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  type: object
                  required: [department, reg_1, datetime, items]
                  properties:
                    customer_number: {type: integer}
                    department: {type: integer}
                    reg_1: {type: string}
                    reg_2: {type: string}
                    reg_3: {type: string}
                    datetime: {type: string, format: date-time}
                    note: {type: string}
                    reference: {type: string}
                    po: {type: string}
                    pickup: {type: boolean}
                    items:
                      type: array
                      items:
                        type: object
                        required: [id, quantity]
                        properties:
                          id: {type: integer}
                          quantity: {type: integer}
          responses:
            '200':
              description: Success
        put:
          tags:
            - Bookings
          summary: Update order booking
          operationId: updateOrderBooking
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  type: object
                  required: [id]
                  properties:
                    id: {type: integer}
                    customer_number: {type: integer}
                    department: {type: integer}
                    reg_1: {type: string}
                    reg_2: {type: string}
                    reg_3: {type: string}
                    datetime: {type: string, format: date-time}
                    note: {type: string}
                    reference: {type: string}
                    po: {type: string}
                    pickup: {type: boolean}
                    order_id: {type: integer}
                    items:
                      type: array
                      items:
                        type: object
                        required: [id, quantity]
                        properties:
                          id: {type: integer}
                          quantity: {type: integer}
          responses:
            '200':
              description: Success
        delete:
          tags:
            - Bookings
          summary: Delete order booking
          operationId: deleteOrderBooking
          parameters:
            - name: id
              in: query
              required: true
              schema: {type: integer}
          responses:
            '200':
              description: Success

  /order-bookings/complete:
    post:
      tags:
        - Bookings
      summary: Complete order booking
      operationId: completeOrderBooking
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
                safety_seal: {type: integer}
      responses:
        '200':
          description: Success

  /admin/bookings/sync:
    post:
      tags:
        - Bookings
      summary: Sync booking from external system
      operationId: syncBooking
      responses:
        '200':
          description: Success

  /admin/bookings/department/count:
    get:
      tags:
        - Bookings
      summary: Get department unfulfilled bookings count
      operationId: getDepartmentBookingCount
      parameters:
        - name: department_id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success

  /user/bookings/washcertificate/download:
    post:
      tags:
        - Bookings
      summary: Get download link for own wash certificate
      operationId: downloadOwnWashCertificate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
      responses:
        '200':
          description: Success

  /bookings/download_pdf:
    get:
      tags:
        - Bookings
      summary: Download booking PDF
      operationId: downloadBookingPdf
      parameters:
        - name: id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success

  /admin/bookings/delete:
    post:
      tags:
        - Bookings
      summary: Delete booking (admin)
      operationId: adminDeleteBooking
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
      responses:
        '200':
          description: Success

  /superuser/bookings/sync/all:
    post:
      tags:
        - Bookings
      summary: Sync all bookings from external system
      operationId: syncAllBookings
      responses:
        '200':
          description: Success

  /admin/bookings/completeWashWithoutWashCertificate:
    post:
      tags:
        - Bookings
      summary: Complete wash without wash certificate
      operationId: completeWashWithoutWashCertificate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
      responses:
        '200':
          description: Success

  /user/bookings/delete:
    post:
      tags:
        - Bookings
      summary: Delete own booking
      operationId: deleteOwnBooking
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
      responses:
        '200':
          description: Success

  # Invoices Endpoints
  /invoices/draft:
    get:
      tags:
        - Invoices
      summary: List draft invoices
      description: Retrieve a list of draft invoices
      operationId: listDraftInvoices
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Draft invoices retrieved successfully

  /invoices/draft/close:
    post:
      tags:
        - Invoices
      summary: Close draft invoice
      description: Close a draft invoice
      operationId: closeDraftInvoice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: integer
      responses:
        '200':
          description: Draft invoice closed successfully

  /invoices/pdf:
    get:
      tags:
        - Invoices
      summary: Get invoice PDF
      description: Download an invoice as PDF
      operationId: getInvoicePdf
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: PDF retrieved successfully
          content:
            application/pdf:
              schema:
                type: string
                format: binary

  /user/invoices:
    get:
      tags:
        - Invoices
      summary: Get user invoices
      description: Retrieve invoices for the authenticated user
      operationId: getUserInvoices
      responses:
        '200':
          description: User invoices retrieved successfully

  /collected-invoices:
    get:
      tags:
        - Invoices
      summary: List collected invoices
      description: Get list of collected invoices
      operationId: listCollectedInvoices
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Collected invoices retrieved successfully
    post:
      tags:
        - Invoices
      summary: Create collected invoice
      description: Create a new collected invoice
      operationId: createCollectedInvoice
      responses:
        '201':
          description: Collected invoice created successfully
    put:
      tags:
        - Invoices
      summary: Update collected invoice
      description: Update a collected invoice
      operationId: updateCollectedInvoice
      responses:
        '200':
          description: Collected invoice updated successfully

  /collected-invoices/ready-to-invoice:
    get:
      tags:
        - Invoices
      summary: Get invoices ready to process
      description: Get collected invoices that are ready to be processed
      operationId: getReadyToInvoice
      responses:
        '200':
          description: Ready invoices retrieved successfully

  /collected-invoices/economic/compare:
    get:
      tags:
        - Invoices
      summary: Compare collected invoice totals with E-conomic
      description: |
        Compares a collected invoice in the system with its corresponding invoice in E-conomic.
        Returns totals from both sources, their difference, and any warnings detected during comparison.
      operationId: compareCollectedInvoiceEconomic
      parameters:
        - name: collected_invoice_id
          in: query
          required: true
          description: The internal collected invoice ID to compare
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Comparison completed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectedInvoiceEconomicCompareResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /superuser/invoicing/period:
    get:
      tags:
        - Invoices
      summary: Get invoicing periods
      description: Retrieve invoicing periods for superusers
      operationId: getInvoicingPeriods
      parameters:
        - name: dateFrom
          in: query
          required: true
          schema: {type: string, format: date}
        - name: dateTo
          in: query
          required: true
          schema: {type: string, format: date}
      responses:
        '200':
          description: Invoicing periods retrieved successfully

  /superuser/invoicing/period/distribution/fixed-pricing:
    get:
      tags:
        - Invoices
      summary: Get fixed pricing distribution
      description: Get invoicing distribution for fixed pricing items
      operationId: getInvoicingFixedPricingDistribution
      parameters:
        - name: dateFrom
          in: query
          required: true
          schema: {type: string, format: date}
        - name: dateTo
          in: query
          required: true
          schema: {type: string, format: date}
      responses:
        '200':
          description: Fixed pricing distribution retrieved successfully

  /superuser/invoicing/period/distribution/wash-subscriptions:
    get:
      tags:
        - Invoices
      summary: Get wash subscriptions distribution
      description: Get invoicing distribution for wash subscriptions
      operationId: getInvoicingWashSubscriptionsDistribution
      parameters:
        - name: dateFrom
          in: query
          required: true
          schema: {type: string, format: date}
        - name: dateTo
          in: query
          required: true
          schema: {type: string, format: date}
      responses:
        '200':
          description: Wash subscriptions distribution retrieved successfully

  # Vehicles Endpoints
  /vehicles:
    get:
      tags:
        - Vehicles
      summary: List vehicles
      description: |
        List vehicles or fetch a specific vehicle when `id` is provided.

        - When `id` is present, returns a single vehicle object (404 if not found).
        - Otherwise returns a paginated list of vehicles.

        Permissions:
        - Own scope: `list_own_vehicles` (linked to subuser node `VEHICLES_LIST`).
        - Broader scope: `list_vehicles_other`.

        Subusers may specify header `X-Customer-Number` to target a specific customer. If the broader
        permission is missing, the list will automatically be restricted to the effective customer context.
      operationId: listVehicles
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/XCustomerNumber'
        - name: id
          in: query
          schema: {type: integer}
        - name: reg
          in: query
          schema: {type: string}
        - name: customer_id
          in: query
          schema: {type: integer}
      responses:
        '200':
          description: Vehicle(s) retrieved successfully
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Vehicle'
                  - type: array
                    items:
                      $ref: '#/components/schemas/Vehicle'
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags:
        - Vehicles
      summary: Add vehicle
      operationId: addVehicle
      description: |
        Create a new vehicle for a customer.

        Permissions:
        - Own scope: `add_vehicle` (linked to subuser node `VEHICLES_ADD`).
        - Broader scope: `add_vehicle_other`.
      parameters:
        - $ref: '#/components/parameters/XCustomerNumber'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reg, type, wash_subscription]
              properties:
                reg:
                  type: string
                  minLength: 2
                  maxLength: 12
                  description: Vehicle registration number
                type:
                  type: integer
                  description: Product ID representing the vehicle wash type
                wash_subscription:
                  type: boolean
                reference:
                  type: string
                  maxLength: 255
                  nullable: true
                customer_id:
                  type: integer
                  description: Optional explicit target customer. Defaults to the effective customer context.
      responses:
        '200': {description: Vehicle created}
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
    put:
      tags:
        - Vehicles
      summary: Edit vehicle
      operationId: editVehicle
      description: |
        Update fields on an existing vehicle.

        Permissions:
        - Own scope: `edit_vehicle` (linked to subuser node `VEHICLES_EDIT`).
        - Broader scope: `edit_vehicle_other`.
      parameters:
        - $ref: '#/components/parameters/XCustomerNumber'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
                reg:
                  type: string
                  minLength: 2
                  maxLength: 12
                type: {type: integer}
                wash_subscription: {type: boolean}
                reference:
                  type: string
                  maxLength: 255
                  nullable: true
      responses:
        '200': {description: Vehicle updated}
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags:
        - Vehicles
      summary: Delete vehicle
      operationId: deleteVehicle
      description: |
        Delete an existing vehicle.

        Permissions:
        - Own scope: `delete_vehicle` (linked to subuser node `VEHICLES_DELETE`).
        - Broader scope: `delete_vehicle_other`.
      parameters:
        - $ref: '#/components/parameters/XCustomerNumber'
        - name: id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200': {description: Vehicle deleted}
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /vehicles/addons/available:
    get:
      tags:
        - Vehicles
      summary: Get available vehicle addons
      description: |
        Get list of available addons for a vehicle.

        Permissions:
        - Own scope: `list_vehicle_addon_own` (linked to subuser node `VEHICLES_LIST`).
        - Broader scope: `list_vehicles_addon_other`.
      operationId: getAvailableVehicleAddons
      parameters:
        - $ref: '#/components/parameters/XCustomerNumber'
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Available addons retrieved successfully
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /vehicles/addons/toggle:
    post:
      tags:
        - Vehicles
      summary: Toggle vehicle addon
      description: |
        Enable or disable a vehicle addon for a vehicle.

        Permissions:
        - Own scope: `toggle_vehicle_addon_own` (linked to subuser node `VEHICLES_EDIT`).
        - Broader scope: `toggle_vehicle_addon_other`.
      operationId: toggleVehicleAddon
      parameters:
        - $ref: '#/components/parameters/XCustomerNumber'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [vehicle_id, addon_id]
              properties:
                vehicle_id:
                  type: integer
                addon_id:
                  type: integer
      responses:
        '200':
          description: Vehicle addon toggled successfully
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /department/vehicles/unknown-customer:
    get:
      tags:
        - Vehicles
      summary: Get unknown customer vehicles in department
      operationId: getUnknownCustomerVehicles
      responses:
        '200': {description: Success}

  /department/vehicle/customer-suggestions:
    get:
      tags:
        - Vehicles
      summary: Get vehicle customer suggestions
      operationId: getVehicleCustomerSuggestions
      parameters:
        - name: reg
          in: query
          required: true
          schema: {type: string}
      responses:
        '200': {description: Success}

  /vehicles/set-auto-start-on-lpr:
    post:
      tags:
        - Vehicles
      summary: Set auto start on LPR
      operationId: setVehicleAutoStartOnLpr
      description: |
        Enable or disable automatic start on LPR for a vehicle in XL Vask.

        Permissions:
        - Own scope: `set_auto_start_on_lpr` (linked to subuser node `VEHICLES_EDIT`).
        - Broader scope: `set_auto_start_on_lpr_other`.
      parameters:
        - $ref: '#/components/parameters/XCustomerNumber'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, active]
              properties:
                id: {type: integer}
                active: {type: boolean}
      responses:
        '200': {description: Success}
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /vehicles/set-vehicle-type-id:
    post:
      tags:
        - Vehicles
      summary: Set vehicle type ID
      operationId: setVehicleTypeId
      description: |
        Set or change the XL Vask `vehicleTypeId` for a vehicle.

        Permissions:
        - Own scope: `set_vehicle_type_id` (linked to subuser node `VEHICLES_EDIT`).
        - Broader scope: `set_vehicle_type_id_other`.
      parameters:
        - $ref: '#/components/parameters/XCustomerNumber'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, vehicleTypeId]
              properties:
                id: {type: integer}
                vehicleTypeId:
                  type: string
                  minLength: 1
                  maxLength: 50
      responses:
        '200': {description: Success}
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /superuser/users-with-vehicle-subscriptions:
    get:
      tags:
        - Vehicles
      summary: Get users with vehicle subscriptions
      operationId: getUsersWithVehicleSubscriptions
      responses:
        '200': {description: Success}

  /vehicles/status:
    get:
      tags:
        - Vehicles
      summary: Get vehicle status
      operationId: getVehicleStatus
      parameters:
        - name: reg
          in: query
          required: true
          schema: {type: string}
      responses:
        '200': {description: Success}

  /vehicles/search:
    get:
      tags:
        - Vehicles
      summary: Search vehicles
      operationId: searchVehicles
      parameters:
        - name: search
          in: query
          required: true
          schema: {type: string}
      responses:
        '200': {description: Success}

  # Notifications Endpoints
  /notifications:
    get:
      tags:
        - Notifications
      summary: List notifications
      description: Get list of notifications
      operationId: listNotifications
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Notifications retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Notification'
    post:
      tags:
        - Notifications
      summary: Create notification
      description: Create a new notification
      operationId: createNotification
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotificationCreate'
      responses:
        '201':
          description: Notification created successfully
    delete:
      tags:
        - Notifications
      summary: Delete notification
      description: Delete a notification
      operationId: deleteNotification
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Notification deleted successfully

  # Statistics Endpoints
  /statistics/bookings/new:
    get:
      tags:
        - Statistics
      summary: Get new bookings statistics
      description: Get statistics for new bookings
      operationId: getNewBookingsStats
      responses:
        '200':
          description: New bookings statistics retrieved successfully

  /orders/module/stripe/payment_intent:
    get:
      tags:
        - Orders
      summary: Get Stripe payment intent
      operationId: getStripePaymentIntent
      parameters:
        - name: id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success
    post:
      tags:
        - Orders
      summary: Create Stripe payment intent
      operationId: createStripePaymentIntent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, reader]
              properties:
                id: {type: integer}
                reader: {type: string}
                tax_percentage: {type: integer}
      responses:
        '200':
          description: Success
    delete:
      tags:
        - Orders
      summary: Delete Stripe payment intent
      operationId: deleteStripePaymentIntent
      parameters:
        - name: id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success

  /orders/module/stripe/payment_intent/capture:
    post:
      tags:
        - Orders
      summary: Capture Stripe payment intent
      operationId: captureStripePaymentIntent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
      responses:
        '200':
          description: Success

  /orders/module/stripe/debug/simulate_payment:
    post:
      tags:
        - Orders
      summary: Simulate Stripe payment
      operationId: simulateStripePayment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
      responses:
        '200':
          description: Success

  /statistics/orders/new:
    get:
      tags:
        - Statistics
      summary: Get new orders statistics
      description: Get statistics for new orders
      operationId: getNewOrdersStats
      responses:
        '200':
          description: New orders statistics retrieved successfully

  /statistics/income/today:
    get:
      tags:
        - Statistics
      summary: Get today's income
      description: Get income statistics for today
      operationId: getTodayIncome
      responses:
        '200':
          description: Today's income statistics retrieved successfully

  /statistics/income/yesterday:
    get:
      tags:
        - Statistics
      summary: Get yesterday's income
      description: Get income statistics for yesterday
      operationId: getYesterdayIncome
      responses:
        '200':
          description: Yesterday's income statistics retrieved successfully

  /statistics/income/this-month:
    get:
      tags:
        - Statistics
      summary: Get this month's income
      description: Get income statistics for the current month
      operationId: getThisMonthIncome
      responses:
        '200':
          description: This month's income statistics retrieved successfully

  /statistics/income/last-month:
    get:
      tags:
        - Statistics
      summary: Get last month's income
      description: Get income statistics for the previous month
      operationId: getLastMonthIncome
      responses:
        '200':
          description: Last month's income statistics retrieved successfully

  /statistics/income/this-year:
    get:
      tags:
        - Statistics
      summary: Get this year's income
      description: Get income statistics for the current year
      operationId: getThisYearIncome
      responses:
        '200':
          description: This year's income statistics retrieved successfully

  /statistics/income/departments:
    get:
      tags:
        - Statistics
      summary: Get total income today by departments
      operationId: getTotalIncomeTodayByDepartments
      responses:
        '200': {description: Success}

  /statistics/economic/totals:
    get:
      tags:
        - Statistics
      summary: Get total economic statistics
      operationId: getEconomicTotals
      responses:
        '200': {description: Success}

  /statistics/economic/totals/department_sent_invoice_totals:
    get:
      tags:
        - Statistics
      summary: Get department sent invoice totals
      operationId: getDepartmentSentInvoiceTotals
      responses:
        '200': {description: Success}

  /statistics/economic/totals/department_draft_invoice_totals:
    get:
      tags:
        - Statistics
      summary: Get department draft invoice totals
      operationId: getDepartmentDraftInvoiceTotals
      responses:
        '200': {description: Success}

  # Worker Endpoints
  /worker/version:
    get:
      tags:
        - Worker
      summary: Get worker version
      description: Get the current version of the system worker
      operationId: getWorkerVersion
      responses:
        '200':
          description: Worker version retrieved successfully

  /worker/update-version:
    get:
      tags:
        - Worker
      summary: Update worker version
      description: Set the target version for the worker update
      operationId: updateWorkerVersion
      parameters:
        - name: version
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Version update target set successfully

  /worker/status:
    get:
      tags:
        - Worker
      summary: Get worker status
      description: Get detailed status of the system worker
      operationId: getWorkerStatus
      responses:
        '200':
          description: Worker status retrieved successfully

  /worker/debug:
    get:
      tags:
        - Worker
      summary: Debug worker
      description: Execute debug commands on the worker (often restricted)
      operationId: debugWorker
      responses:
        '200':
          description: Debug information retrieved successfully
        '403':
          $ref: '#/components/responses/Forbidden'

  /worker/debug/on:
    get:
      tags:
        - Worker
      summary: Enable worker debug
      operationId: enableWorkerDebug
      responses:
        '200':
          description: Worker debug enabled
        '403':
          $ref: '#/components/responses/Forbidden'

  /worker/debug/off:
    get:
      tags:
        - Worker
      summary: Disable worker debug
      operationId: disableWorkerDebug
      responses:
        '200':
          description: Worker debug disabled
        '403':
          $ref: '#/components/responses/Forbidden'

  /worker/licenseplates:
    get:
      tags:
        - Worker
      summary: Get unique license plates
      description: Fetch all unique license plates from various database tables
      operationId: getWorkerLicensePlates
      responses:
        '200':
          description: License plates retrieved successfully
        '403':
          $ref: '#/components/responses/Forbidden'

  /economic/doesCustomerExist:
    get:
      tags:
        - Modules
      summary: Check if customer exists in e-conomic
      operationId: checkEconomicCustomerExists
      parameters:
        - name: cvr
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Customer check completed
        '404':
          $ref: '#/components/responses/NotFound'

  /cvr/lookup:
    get:
      tags:
        - Modules
      summary: Lookup CVR information
      description: Get detailed information for a CVR number
      operationId: lookupCvr
      parameters:
        - name: cvr
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: CVR information retrieved successfully

  /cvr/search:
    get:
      tags:
        - Modules
      summary: Search CVR
      description: Search for companies by name or CVR
      operationId: searchCvr
      parameters:
        - name: query
          in: query
          required: true
          schema:
            type: string
            minLength: 2
      responses:
        '200':
          description: Search results retrieved successfully

  # Plate Scans Endpoints
  /numberplatescans:
    get:
      tags:
        - Plate Scans
      summary: List plate scans
      description: Get a list of license plate scans
      operationId: listPlateScans
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
      responses:
        '200':
          description: Plate scans retrieved successfully
    post:
      tags:
        - Plate Scans
      summary: Record plate scan
      description: Record a new license plate scan
      operationId: recordPlateScan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - plate
                - lane_id
              properties:
                plate:
                  type: string
                lane_id:
                  type: integer
      responses:
        '201':
          description: Plate scan recorded successfully

  /numberplatescans/department:
    post:
      tags:
        - Plate Scans
      summary: Record plate scan for department
      operationId: recordDepartmentPlateScan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - plate
                - department_id
              properties:
                plate:
                  type: string
                department_id:
                  type: integer
      responses:
        '201':
          description: Plate scan recorded successfully

  /numberplatescans/post:
    get:
      tags:
        - Plate Scans
      summary: Get post-scan results
      operationId: getPlateScanPostResults
      responses:
        '200':
          description: Post-scan results retrieved successfully

  /numberplatescanners:
    get:
      tags:
        - Plate Scans
      summary: List plate scanners
      description: Get a list of all number plate scanners
      operationId: listPlateScanners
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
      responses:
        '200':
          description: Plate scanners retrieved successfully
    post:
      tags:
        - Plate Scans
      summary: Add plate scanner
      operationId: addPlateScanner
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department_id, name, notes]
              properties:
                department_id: {type: integer}
                name: {type: string}
                notes: {type: string}
      responses:
        '201':
          description: Plate scanner added successfully
    put:
      tags:
        - Plate Scans
      summary: Update plate scanner
      operationId: updatePlateScanner
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, department_id, name, notes]
              properties:
                id: {type: integer}
                department_id: {type: integer}
                name: {type: string}
                notes: {type: string}
      responses:
        '200':
          description: Plate scanner updated successfully

  /department/numberplatescanners:
    get:
      tags:
        - Plate Scans
      summary: List department plate scanners
      operationId: listDepartmentPlateScanners
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Department plate scanners retrieved successfully

  /relay/button/press/post:
    get:
      tags:
        - Plate Scans
      summary: Add button press
      operationId: addButtonPress
      parameters:
        - name: token
          in: query
          required: true
          schema: {type: string}
      responses:
        '201':
          description: Button press recorded

  # Module - e-conomic Endpoints
  /economic/customers/import:
    post:
      tags:
        - Modules
      summary: Import e-conomic customers
      description: Import customers from e-conomic
      operationId: importEconomicCustomers
      responses:
        '200':
          description: Customers imported successfully

  /economic/departments:
    get:
      tags:
        - Modules
      summary: Get e-conomic departments
      operationId: getEconomicDepartments
      responses:
        '200':
          description: Success

  /economic/products:
    get:
      tags:
        - Modules
      summary: Get e-conomic products
      operationId: getEconomicProducts
      responses:
        '200':
          description: Success

  /modules/economic/customer:
    get:
      tags:
        - Modules
      summary: Get e-conomic customer details
      operationId: getEconomicCustomer
      parameters:
        - name: customer_number
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success
    post:
      tags:
        - Modules
      summary: Create e-conomic customer
      operationId: createEconomicCustomer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_number, cvr, email, phone, name]
              properties:
                customer_number: {type: integer}
                cvr: {type: integer}
                email: {type: string}
                phone: {type: integer}
                name: {type: string}
      responses:
        '200':
          description: Success

  /economic/layouts:
    get:
      tags:
        - Modules
      summary: Get e-conomic layouts
      description: Get available invoice layouts from e-conomic
      operationId: getEconomicLayouts
      responses:
        '200':
          description: Layouts retrieved successfully

  /economic/payment-terms:
    get:
      tags:
        - Modules
      summary: Get e-conomic payment terms
      description: Get available payment terms from e-conomic
      operationId: getEconomicPaymentTerms
      responses:
        '200':
          description: Payment terms retrieved successfully

  /economic/invoice/draft/export:
    post:
      tags:
        - Modules
      summary: Export draft invoice to e-conomic
      description: Export a draft invoice to e-conomic
      operationId: exportDraftInvoiceToEconomic
      responses:
        '200':
          description: Draft invoice exported successfully

  /economic/invoice/export:
    post:
      tags:
        - Modules
      summary: Export invoice to e-conomic
      description: Export a booked invoice to e-conomic
      operationId: exportInvoiceToEconomic
      responses:
        '200':
          description: Invoice exported successfully

  # Module - Stripe Endpoints
  /modules/stripe/customers:
    get:
      tags:
        - Modules
      summary: List Stripe customers
      description: Get list of Stripe customers
      operationId: listStripeCustomers
      responses:
        '200':
          description: Stripe customers retrieved successfully

  /modules/stripe/products:
    get:
      tags:
        - Modules
      summary: List Stripe products
      description: Get list of Stripe products
      operationId: listStripeProducts
      responses:
        '200':
          description: Stripe products retrieved successfully

  /modules/stripe/prices:
    get:
      tags:
        - Modules
      summary: List Stripe prices
      description: Get list of Stripe prices
      operationId: listStripePrices
      responses:
        '200':
          description: Stripe prices retrieved successfully

  /modules/stripe/invoice:
    post:
      tags:
        - Modules
      summary: Create Stripe invoice
      description: Create an invoice in Stripe
      operationId: createStripeInvoice
      responses:
        '201':
          description: Stripe invoice created successfully

  /modules/stripe/terminal/readers:
    get:
      tags:
        - Modules
      summary: List Stripe terminal readers
      operationId: listStripeTerminalReaders
      responses:
        '200':
          description: Success

  /modules/stripe/terminal/locations:
    get:
      tags:
        - Modules
      summary: List Stripe terminal locations
      operationId: listStripeTerminalLocations
      responses:
        '200':
          description: Success

  /modules/stripe/department/terminal/location:
    get:
      tags:
        - Modules
      summary: Get department terminal location
      operationId: getDepartmentTerminalLocation
      parameters:
        - name: id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success
    post:
      tags:
        - Modules
      summary: Set department terminal location
      operationId: setDepartmentTerminalLocation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, location]
              properties:
                id: {type: integer}
                location: {type: string}
      responses:
        '200':
          description: Success

  /modules/stripe/department/terminal/readers:
    get:
      tags:
        - Modules
      summary: Get department terminal readers
      operationId: getDepartmentTerminalReaders
      parameters:
        - name: id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success

  # Module - Backup Endpoints
  /modules/backup/backups:
    get:
      tags:
        - Modules
      summary: List backup modules
      operationId: listBackupModules
      responses:
        '200':
          description: Success
    post:
      tags:
        - Modules
      summary: Create backup module
      operationId: createBackupModule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, description]
              properties:
                name: {type: string}
                description: {type: string}
      responses:
        '200':
          description: Success

  # Module - XLVask Endpoints
  /modules/xlvask/usageLog:
    get:
      tags:
        - Modules
      summary: Get XLVask usage logs
      description: Retrieve usage logs from XLVask system
      operationId: getXlvaskUsageLogs
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Usage logs retrieved successfully

  /modules/xlvask/vehicles:
    get:
      tags:
        - Modules
      summary: List XLVask vehicles
      description: Get list of vehicles from XLVask
      operationId: listXlvaskVehicles
      responses:
        '200':
          description: XLVask vehicles retrieved successfully

  /modules/xlvask/customers:
    get:
      tags:
        - Modules
      summary: List XLVask customers
      description: Get list of customers from XLVask
      operationId: listXlvaskCustomers
      responses:
        '200':
          description: XLVask customers retrieved successfully

  /modules/action-logs:
    get:
      tags:
        - Modules
      summary: List module action logs
      description: Retrieve a paginated list of module action logs with searching and filtering
      operationId: listModuleActionLogs
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - $ref: '#/components/parameters/SearchParam'
        - $ref: '#/components/parameters/FiltersParam'
      responses:
        '200':
          description: Module action logs retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ModuleActionLog'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # Module - Self-Serve Endpoints
  /modules/self-serve/lane/status:
    get:
      tags:
        - Modules
      summary: Get self-serve lane status
      description: Retrieve the current status of a self-serve lane
      operationId: getSelfServeLaneStatus
      parameters:
        - name: lane_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Lane status retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfServeLaneStatus'

  /modules/self-serve/lane/command:
    post:
      tags:
        - Modules
      summary: Send self-serve lane command
      description: Send a command (e.g., start, stop, reset) to a self-serve lane
      operationId: sendSelfServeLaneCommand
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - lane_id
                - command
              properties:
                lane_id:
                  type: integer
                command:
                  type: string
                  enum: [START, STOP, RESET, RESERVE, RELEASE]
                license_plate:
                  type: string
                  description: Required for START command
      responses:
        '200':
          description: Command sent successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfServeLaneStatus'

  /modules/self-serve/lane/services/allowed:
    post:
      tags:
        - Modules
      summary: Set allowed services for a lane based on shown tasks
      description: |
        Updates the set of services that are allowed to be manually activated for a given self-serve lane,
        derived from the tasks currently shown to the user after answering the self-serve questions.
        This endpoint does not activate anything by itself; it only sets what is allowed to be activated.
      operationId: setSelfServeLaneAllowedServices
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - lane_id
              properties:
                lane_id:
                  type: integer
                task_ids:
                  type: array
                  description: List of task IDs that are currently shown to the user
                  items:
                    type: integer
      responses:
        '200':
          description: Allowed services updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  lane_id:
                    type: integer
                  allowed_services:
                    type: array
                    items:
                      type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /modules/self-serve/lane/relay/machine/enable:
    post:
      tags:
        - Modules
      summary: Manually enable MACHINE relay for a lane
      description: |
        Manually turns on the MACHINE relay for a self-serve lane if and only if the current allowed services
        include `MACHINE` (set via `/modules/self-serve/lane/services/allowed`). The relay is never automatically
        enabled; an explicit call to this endpoint is required.
      operationId: enableSelfServeLaneMachineRelay
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - lane_id
              properties:
                lane_id:
                  type: integer
                duration:
                  type: integer
                  description: Optional number of seconds after which the relay should automatically turn off
      responses:
        '200':
          description: MACHINE relay enabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  lane_id:
                    type: integer
                  relay:
                    type: string
                  enabled:
                    type: boolean
                  duration:
                    type: integer
                    nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Not allowed to enable MACHINE relay (no matching task currently shown)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /modules/self-serve/lane/force/machine/enable:
    post:
      tags:
        - Modules
      summary: Force enable MACHINE relay and mark lane as in-wash (superusers only)
      description: |
        Superuser/emergency endpoint. Bypasses the allowed services gating and directly turns on the MACHINE relay.
        Also ensures the lane is marked as OCCUPIED and IN_WASH with a wash start timestamp if not already set.
      operationId: forceEnableSelfServeLaneMachine
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - lane_id
              properties:
                lane_id:
                  type: integer
                duration:
                  type: integer
                  nullable: true
                  description: Optional number of seconds after which the relay should automatically turn off
                license_plate:
                  type: string
                  nullable: true
                  description: Optional license plate to associate with the lane
      responses:
        '200':
          description: MACHINE relay force-enabled and lane marked in-wash
          content:
            application/json:
              schema:
                type: object
                properties:
                  lane_id:
                    type: integer
                  forced:
                    type: boolean
                  machine:
                    type: string
                    enum: [ENABLED]
                  duration:
                    type: integer
                    nullable: true
                  status:
                    type: string
                  state:
                    type: string
                  wash_start_time:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /modules/self-serve/lane/force/machine/disable:
    post:
      tags:
        - Modules
      summary: Force disable MACHINE relay but keep lane as in-wash (superusers only)
      description: |
        Superuser/emergency endpoint. Turns off the MACHINE relay while ensuring the lane remains in an IN_WASH state
        (simulating a started wash without machine assistance).
      operationId: forceDisableSelfServeLaneMachine
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - lane_id
              properties:
                lane_id:
                  type: integer
                license_plate:
                  type: string
                  nullable: true
      responses:
        '200':
          description: MACHINE relay force-disabled and lane ensured in-wash
          content:
            application/json:
              schema:
                type: object
                properties:
                  lane_id:
                    type: integer
                  forced:
                    type: boolean
                  machine:
                    type: string
                    enum: [DISABLED]
                  status:
                    type: string
                  state:
                    type: string
                  wash_start_time:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # Module - Other Integration Endpoints
  /modules/motorapi/lookup:
    get:
      tags:
        - Modules
      summary: Lookup vehicle via MotorAPI
      description: Look up vehicle information using license plate
      operationId: motorApiLookup
      parameters:
        - name: plate
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Vehicle information retrieved successfully

  /modules/virkdata/search:
    get:
      tags:
        - Modules
      summary: Search VirkData
      description: Search for company information in VirkData
      operationId: virkdataSearch
      parameters:
        - name: search
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Company information retrieved successfully

  /modules/fxratesapi/rate:
    get:
      tags:
        - Modules
      summary: Get exchange rate
      description: Get current exchange rate
      operationId: getExchangeRate
      parameters:
        - name: from
          in: query
          required: true
          schema:
            type: string
        - name: to
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Exchange rate retrieved successfully

  /modules/fxratesapi/rates:
    get:
      tags:
        - Modules
      summary: Get all exchange rates
      description: Get all available exchange rates
      operationId: getAllExchangeRates
      responses:
        '200':
          description: Exchange rates retrieved successfully

  /modules/entra/users:
    get:
      tags:
        - Modules
      summary: List Microsoft Entra users
      description: Get list of users from Microsoft Entra (Azure AD)
      operationId: listEntraUsers
      responses:
        '200':
          description: Entra users retrieved successfully

  # Attachments Endpoints
  /attachments/upload:
    post:
      tags:
        - Attachments
      summary: Upload attachment
      description: Upload a file attachment
      operationId: uploadAttachment
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: Attachment uploaded successfully
        '400':
          $ref: '#/components/responses/BadRequest'

  /orders/attachments:
    get:
      tags:
        - Attachments
      summary: List order attachments
      description: Get attachments for an order
      operationId: listOrderAttachments
      parameters:
        - name: order_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Order attachments retrieved successfully

  /orders/attachments/upload:
    post:
      tags:
        - Attachments
      summary: Upload order attachment
      description: Upload an attachment to an order
      operationId: uploadOrderAttachment
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                order_id:
                  type: integer
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: Order attachment uploaded successfully

  /orders/attachments/download:
    get:
      tags:
        - Attachments
      summary: Download order attachment
      description: Download a specific order attachment
      operationId: downloadOrderAttachment
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Attachment downloaded successfully
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary

  # Forms Endpoints
  /form:
    get:
      tags:
        - Forms
      summary: Get form
      description: Retrieve a form definition
      operationId: getForm
      parameters:
        - name: id
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Form retrieved successfully
    post:
      tags:
        - Forms
      summary: Submit form
      description: Submit a form
      operationId: submitForm
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, data]
              properties:
                id:
                  type: string
                  description: Form identifier
                data:
                  type: object
                  description: Form submission data
                g_recaptcha_response:
                  type: string
                  description: reCAPTCHA verification token (required if not authenticated)
      responses:
        '201':
          description: Form submitted successfully

  # Permissions Endpoints
  /permissions:
    get:
      tags:
        - Users
      summary: List permissions
      description: Get list of all available permissions
      operationId: listPermissions
      responses:
        '200':
          description: Permissions retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Permission'

  # Customer Management Endpoints
  /customer/attributes:
    get:
      tags:
        - Users
      summary: Get customer attributes
      description: Get custom attributes for a customer
      operationId: getCustomerAttributes
      parameters:
        - name: customer_id
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Customer attributes retrieved successfully
    post:
      tags:
        - Users
      summary: Add customer attribute
      description: Add a custom attribute to a customer
      operationId: addCustomerAttribute
      responses:
        '201':
          description: Customer attribute added successfully
    delete:
      tags:
        - Users
      summary: Delete customer attribute
      description: Remove a custom attribute from a customer
      operationId: deleteCustomerAttribute
      responses:
        '200':
          description: Customer attribute deleted successfully

  /customer/notes:
    get:
      tags:
        - Users
      summary: Get customer notes
      description: Get notes for a customer
      operationId: getCustomerNotes
      parameters:
        - name: customer_id
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Customer notes retrieved successfully
    post:
      tags:
        - Users
      summary: Add customer note
      description: Add a note to a customer
      operationId: addCustomerNote
      responses:
        '201':
          description: Customer note added successfully
    delete:
      tags:
        - Users
      summary: Delete customer note
      description: Remove a note from a customer
      operationId: deleteCustomerNote
      responses:
        '200':
          description: Customer note deleted successfully

  /customers/search:
    post:
      tags:
        - Users
      summary: Search customers
      description: Search for customers using various criteria
      operationId: searchCustomers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
      responses:
        '200':
          description: Customers found successfully

  # Configuration Endpoints
  /economic/config:
    get:
      tags: [Config]
      summary: Get e-conomic config
      operationId: getEconomicConfig
      responses:
        '200':
          description: e-conomic configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicConfigListResponse'
    post:
      tags: [Config]
      summary: Update e-conomic config
      operationId: updateEconomicConfig
      responses:
        '200':
          description: e-conomic configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /reCAPTCHA/config:
    get:
      tags: [Config]
      summary: Get reCAPTCHA config
      operationId: getRecaptchaConfig
      responses:
        '200':
          description: reCAPTCHA configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecaptchaConfigListResponse'
    post:
      tags: [Config]
      summary: Update reCAPTCHA config
      operationId: updateRecaptchaConfig
      responses:
        '200':
          description: reCAPTCHA configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /email/config:
    get:
      tags: [Config]
      summary: Get email config
      operationId: getEmailConfig
      responses:
        '200':
          description: Email configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailConfigListResponse'
    post:
      tags: [Config]
      summary: Update email config
      operationId: updateEmailConfig
      responses:
        '200':
          description: Email configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /email/config/test:
    post:
      tags: [Config]
      summary: Test email config
      operationId: testEmailConfig
      responses:
        '200':
          description: Email configuration test completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigTestResponse'

  /backups/config:
    get:
      tags: [Config]
      summary: Get backups config
      operationId: getBackupsConfig
      responses:
        '200':
          description: Backups configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupsConfigListResponse'
    post:
      tags: [Config]
      summary: Update backups config
      operationId: updateBackupsConfig
      responses:
        '200':
          description: Backups configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /bird/config:
    get:
      tags: [Config]
      summary: Get Bird config
      operationId: getBirdConfig
      responses:
        '200':
          description: Bird configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdConfigListResponse'
    post:
      tags: [Config]
      summary: Update Bird config
      operationId: updateBirdConfig
      responses:
        '200':
          description: Bird configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /motorapi/config:
    get:
      tags: [Config]
      summary: Get MotorAPI config
      operationId: getMotorApiConfig
      responses:
        '200':
          description: MotorAPI configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MotorApiConfigListResponse'
    post:
      tags: [Config]
      summary: Update MotorAPI config
      operationId: updateMotorApiConfig
      responses:
        '200':
          description: MotorAPI configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /stripe/config:
    get:
      tags: [Config]
      summary: Get Stripe config
      operationId: getStripeConfig
      responses:
        '200':
          description: Stripe configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeConfigListResponse'
    post:
      tags: [Config]
      summary: Update Stripe config
      operationId: updateStripeConfig
      responses:
        '200':
          description: Stripe configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /fxratesapi/config:
    get:
      tags: [Config]
      summary: Get FXRatesAPI config
      operationId: getFxRatesApiConfig
      responses:
        '200':
          description: FXRatesAPI configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FxRatesApiConfigListResponse'
    post:
      tags: [Config]
      summary: Update FXRatesAPI config
      operationId: updateFxRatesApiConfig
      responses:
        '200':
          description: FXRatesAPI configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /gatewayapi/config:
    get:
      tags: [Config]
      summary: Get GatewayAPI config
      operationId: getGatewayApiConfig
      responses:
        '200':
          description: GatewayAPI configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GatewayApiConfigListResponse'
    post:
      tags: [Config]
      summary: Update GatewayAPI config
      operationId: updateGatewayApiConfig
      responses:
        '200':
          description: GatewayAPI configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /xlvask/config:
    get:
      tags: [Config]
      summary: Get XLVask config
      operationId: getXlvaskConfig
      responses:
        '200':
          description: XLVask configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/XlvaskConfigListResponse'
    post:
      tags: [Config]
      summary: Update XLVask config
      operationId: updateXlvaskConfig
      responses:
        '200':
          description: XLVask configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /entra/config:
    get:
      tags: [Config]
      summary: Get Entra config
      operationId: getEntraConfig
      responses:
        '200':
          description: Entra configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntraConfigListResponse'
    post:
      tags: [Config]
      summary: Update Entra config
      operationId: updateEntraConfig
      responses:
        '200':
          description: Entra configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /limble/config:
    get:
      tags: [Config]
      summary: Get Limble config
      operationId: getLimbleConfig
      responses:
        '200':
          description: Limble configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LimbleConfigListResponse'
    post:
      tags: [Config]
      summary: Update Limble config
      operationId: updateLimbleConfig
      responses:
        '200':
          description: Limble configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /ocrspace/config:
    get:
      tags: [Config]
      summary: Get OcrSpace config
      operationId: getOcrSpaceConfig
      responses:
        '200':
          description: OcrSpace configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OcrSpaceConfigListResponse'
    post:
      tags: [Config]
      summary: Update OcrSpace config
      operationId: updateOcrSpaceConfig
      responses:
        '200':
          description: OcrSpace configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /openai/config:
    get:
      tags: [Config]
      summary: Get OpenAI config
      operationId: getOpenAiConfig
      responses:
        '200':
          description: OpenAI configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OpenAiConfigListResponse'
    post:
      tags: [Config]
      summary: Update OpenAI config
      operationId: updateOpenAiConfig
      responses:
        '200':
          description: OpenAI configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /licenseplaterecognizer/config:
    get:
      tags: [Config]
      summary: Get LicensePlateRecognizer config
      operationId: getLicensePlateRecognizerConfig
      responses:
        '200':
          description: LicensePlateRecognizer configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LicensePlateRecognizerConfigListResponse'
    post:
      tags: [Config]
      summary: Update LicensePlateRecognizer config
      operationId: updateLicensePlateRecognizerConfig
      responses:
        '200':
          description: LicensePlateRecognizer configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /virkdata/config:
    get:
      tags: [Config]
      summary: Get Virkdata config
      operationId: getVirkdataConfig
      responses:
        '200':
          description: Virkdata configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VirkdataConfigListResponse'
    post:
      tags: [Config]
      summary: Update Virkdata config
      operationId: updateVirkdataConfig
      responses:
        '200':
          description: Virkdata configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /shelly/config:
    get:
      tags: [Config]
      summary: Get Shelly config
      operationId: getShellyConfig
      responses:
        '200':
          description: Shelly configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShellyConfigListResponse'
    post:
      tags: [Config]
      summary: Update Shelly config
      operationId: updateShellyConfig
      responses:
        '200':
          description: Shelly configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /selfserve/config:
    get:
      tags: [Config]
      summary: Get Self-Serve config
      operationId: getSelfServeConfig
      responses:
        '200':
          description: Self-serve configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfServeConfigListResponse'
    post:
      tags: [Config]
      summary: Update Self-Serve config
      operationId: updateSelfServeConfig
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelfServeConfig'
      responses:
        '200':
          description: Self-serve configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /branding:
    get:
      tags:
        - Branding
      summary: List branding options
      description: Retrieve a list of branding options or a specific branding option if ID is provided
      operationId: listBrandingOptions
      parameters:
        - name: id
          in: query
          required: false
          schema:
            type: integer
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: Branding options retrieved successfully
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
        - Branding
      summary: Add branding option
      description: Create a new branding option
      operationId: addBrandingOption
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, description, cvr]
              properties:
                name: {type: string}
                description: {type: string}
                cvr: {type: integer}
      responses:
        '200':
          description: Branding option added successfully
        '403':
          $ref: '#/components/responses/Forbidden'
    put:
      tags:
        - Branding
      summary: Edit branding option
      description: Update an existing branding option
      operationId: editBrandingOption
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
                name: {type: string}
                description: {type: string}
                cvr: {type: integer}
      responses:
        '200':
          description: Branding option updated successfully
        '403':
          $ref: '#/components/responses/Forbidden'

  /roles:
    get:
      tags:
        - Roles
      summary: List roles
      operationId: listRoles
      responses:
        '200':
          description: Success
    post:
      tags:
        - Roles
      summary: Add role
      operationId: addRole
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: {type: string}
      responses:
        '200':
          description: Success
    put:
      tags:
        - Roles
      summary: Edit role
      operationId: editRole
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, name]
              properties:
                id: {type: integer}
                name: {type: string}
      responses:
        '200':
          description: Success

  /roles/permissions:
    post:
      tags:
        - Roles
      summary: Add permission to role
      operationId: addRolePermission
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role_id, permission]
              properties:
                role_id: {type: integer}
                permission: {type: string}
      responses:
        '200':
          description: Success
    delete:
      tags:
        - Roles
      summary: Remove permission from role
      operationId: removeRolePermission
      parameters:
        - name: role_id
          in: query
          required: true
          schema: {type: integer}
        - name: permission
          in: query
          required: true
          schema: {type: string}
      responses:
        '200':
          description: Success

  /roles/clone:
    post:
      tags:
        - Roles
      summary: Clone role
      operationId: cloneRole
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role_id, name]
              properties:
                role_id: {type: integer}
                name: {type: string}
      responses:
        '200':
          description: Success

  /modules/washcertificates:
    get:
      tags:
        - Modules
      summary: List wash certificates
      operationId: listWashCertificates
      responses:
        '200':
          description: Success

  /modules/xlvask/services/usage/orders:
    get:
      tags:
        - Modules
      summary: Get XLVask usage orders
      operationId: getXlvaskUsageOrders
      responses:
        '200':
          description: Success

  /modules/xlvask/services/usage/orders/fast-link:
    get:
      tags:
        - Modules
      summary: Get XLVask usage orders fast link
      operationId: getXlvaskUsageOrdersFastLink
      responses:
        '200':
          description: Success

  /superuser/department:
    get:
      tags:
        - Departments
      summary: List departments (superuser)
      operationId: listSuperuserDepartments
      responses:
        '200':
          description: Success

  /superuser/department/prices:
    get:
      tags:
        - Departments
      summary: Get department prices
      operationId: getDepartmentPrices
      parameters:
        - name: department_id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success
    post:
      tags:
        - Departments
      summary: Set department price
      operationId: setDepartmentPrice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department_id, product_id, price]
              properties:
                department_id: {type: integer}
                product_id: {type: integer}
                price: {type: integer}
      responses:
        '200':
          description: Success

  /superuser/department/variables:
    get:
      tags:
        - Departments
      summary: Get department variables
      operationId: getDepartmentVariables
      parameters:
        - name: department_id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success
    post:
      tags:
        - Departments
      summary: Set department variable
      operationId: setDepartmentVariable
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department_id, variable, value]
              properties:
                department_id: {type: integer}
                variable: {type: string}
                value: {type: string}
      responses:
        '200':
          description: Success

  /departments/daily-reports:
    get:
      tags:
        - Departments
      summary: List daily reports
      operationId: listDailyReports
      responses:
        '200':
          description: Success
    post:
      tags:
        - Departments
      summary: Add daily report
      operationId: addDailyReport
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department_id, date, report]
              properties:
                department_id: {type: integer}
                date: {type: string}
                report: {type: string}
      responses:
        '200':
          description: Success
    put:
      tags:
        - Departments
      summary: Edit daily report
      operationId: editDailyReport
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, report]
              properties:
                id: {type: integer}
                report: {type: string}
      responses:
        '200':
          description: Success

  /departments/daily-reports/get:
    get:
      tags:
        - Departments
      summary: Get daily report
      operationId: getDailyReport
      parameters:
        - name: department_id
          in: query
          required: true
          schema: {type: integer}
        - name: date
          in: query
          required: true
          schema: {type: string}
      responses:
        '200':
          description: Success

  /departments/daily-reports/product-count:
    get:
      tags:
        - Departments
      summary: Get product count for daily reports
      operationId: getDailyReportProductCount
      parameters:
        - name: date
          in: query
          required: true
          schema: {type: string}
        - name: date_to
          in: query
          required: false
          schema: {type: string}
        - name: department_id
          in: query
          required: true
          schema: {type: integer}
        - name: product_id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success

  /departments/daily-reports/transaction-count:
    get:
      tags:
        - Departments
      summary: Get transaction count for daily reports
      operationId: getDailyReportTransactionCount
      parameters:
        - name: date
          in: query
          required: true
          schema: {type: string}
        - name: date_to
          in: query
          required: false
          schema: {type: string}
        - name: department_id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success

  /departments/daily-reports/bookings-count:
    get:
      tags:
        - Departments
      summary: Get bookings count for daily reports
      operationId: getDailyReportBookingsCount
      parameters:
        - name: date
          in: query
          required: true
          schema: {type: string}
        - name: department_id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success

  # Account Security - Passkeys
  /account/security/passkeys:
    get:
      tags:
        - Security
      summary: List passkeys for the authenticated user
      operationId: listPasskeys
      responses:
        '200':
          description: A list of passkeys
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Passkey'
        '400':
          description: Invalid session or request
    post:
      tags:
        - Security
      summary: Create/add a passkey for the authenticated user
      operationId: createPasskey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasskeyCreateRequest'
      responses:
        '200':
          description: Passkey created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
        '400':
          description: Invalid session or request

  /account/security/passkeys/{id}:
    patch:
      tags:
        - Security
      summary: Rename a passkey
      operationId: renamePasskey
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasskeyRenameRequest'
      responses:
        '200':
          description: Passkey renamed
        '404':
          description: Not found
    delete:
      tags:
        - Security
      summary: Delete a passkey
      operationId: deletePasskey
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Passkey deleted
        '404':
          description: Not found

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT token obtained from /auth/login or /auth/employee/login

  parameters:
    PageParam:
      name: page
      in: query
      description: Page number for pagination
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPageParam:
      name: per_page
      in: query
      description: Number of items per page
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 30
    SearchParam:
      name: search
      in: query
      description: Search query string
      schema:
        type: string
    LimitParam:
      name: limit
      in: query
      description: Number of items per page
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 100
    FiltersParam:
      name: filters
      in: query
      description: Filters for the list (e.g., module:selfserve,status_code:200)
      schema:
        type: string
    XCustomerNumber:
      name: X-Customer-Number
      in: header
      required: false
      description: |
        Target customer number for subuser requests. Ignored for classic user sessions.
        Required on customer-scoped endpoints when authenticated as a subuser unless
        the target customer can be inferred from context.
      schema:
        type: integer

  responses:
    BadRequest:
      description: Bad request - Invalid input parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Unauthorized - Invalid or missing authentication token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Forbidden - Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Not found - Resource does not exist
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          description: Error message
        code:
          type: integer
          description: HTTP status code

    ModuleConfigValue:
      oneOf:
        - type: string
        - type: integer
        - type: number
        - type: boolean
        - type: array
          items: {}
        - type: object
          additionalProperties: true
      nullable: true

    ModuleConfigEnvelopeBase:
      type: object
      properties:
        success:
          type: boolean
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - meta
        - includes

    EconomicConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [economic] }
        variable: { type: string, enum: [adminFeeMonthly, adminFeeOrder, feeProductId, invoiceLayoutNumber, paymentTermsNumber] }
        type: { type: string, enum: [string, int] }
        value:
          oneOf:
            - type: string
            - type: integer
      required: [module, variable, type, value]

    RecaptchaConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [reCAPTCHA] }
        variable: { type: string, enum: [enabled, secret_key_v2, site_key_v2] }
        type: { type: string, enum: [bool, string] }
        value:
          oneOf:
            - type: boolean
            - type: string
      required: [module, variable, type, value]

    EmailConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [Email] }
        variable:
          type: string
          enum: [enabled, mailersend_api_key, mailersend_enabled, smtp_encryption, smtp_from, smtp_from_name, smtp_host, smtp_password, smtp_port, smtp_reply_to, smtp_reply_to_name, smtp_username]
        type: { type: string, enum: [bool, string, int] }
        value:
          oneOf:
            - type: boolean
            - type: string
            - type: integer
      required: [module, variable, type, value]

    BackupsConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [Backups] }
        variable: { type: string, enum: [enabled] }
        type: { type: string, enum: [bool] }
        value: { type: boolean }
      required: [module, variable, type, value]

    BirdConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [bird] }
        variable: { type: string, enum: [api_key, enabled, server_url, workplaceId, channelId] }
        type: { type: string, enum: [string, bool] }
        value:
          oneOf:
            - type: string
            - type: boolean
      required: [module, variable, type, value]

    MotorApiConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [motorapi] }
        variable: { type: string, enum: [daily_limit, enabled, secret_key] }
        type: { type: string, enum: [int, bool, string] }
        value:
          oneOf:
            - type: integer
            - type: boolean
            - type: string
      required: [module, variable, type, value]

    StripeConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [Stripe] }
        variable: { type: string, enum: [economic_customer_number, enabled, publishable_key, secret_key] }
        type: { type: string, enum: [int, bool, string] }
        value:
          oneOf:
            - type: integer
            - type: boolean
            - type: string
      required: [module, variable, type, value]

    FxRatesApiConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [fxratesapi] }
        variable: { type: string, enum: [daily_limit, enabled, secret_key] }
        type: { type: string, enum: [int, bool, string] }
        value:
          oneOf:
            - type: integer
            - type: boolean
            - type: string
      required: [module, variable, type, value]

    GatewayApiConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [GatewayAPI] }
        variable: { type: string, enum: [api_key, api_secret, api_token, enabled, sender] }
        type: { type: string, enum: [string, bool] }
        value:
          oneOf:
            - type: string
            - type: boolean
      required: [module, variable, type, value]

    XlvaskConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [xlvask] }
        variable: { type: string, enum: [enabled, password, synchronization_enabled, username] }
        type: { type: string, enum: [bool, string] }
        value:
          oneOf:
            - type: boolean
            - type: string
      required: [module, variable, type, value]

    EntraConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [Entra] }
        variable: { type: string, enum: [enabled, entra_client_id, entra_client_secret, entra_tenant_id] }
        type: { type: string, enum: [bool, string] }
        value:
          oneOf:
            - type: boolean
            - type: string
      required: [module, variable, type, value]

    LimbleConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [limble] }
        variable: { type: string, enum: [client_id, client_secret, enabled, webhooks_enabled] }
        type: { type: string, enum: [string, bool] }
        value:
          oneOf:
            - type: string
            - type: boolean
      required: [module, variable, type, value]

    OcrSpaceConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [ocrSpace] }
        variable: { type: string, enum: [api_key, enabled] }
        type: { type: string, enum: [string, bool] }
        value:
          oneOf:
            - type: string
            - type: boolean
      required: [module, variable, type, value]

    OpenAiConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [openAI] }
        variable: { type: string, enum: [api_key, enabled] }
        type: { type: string, enum: [string, bool] }
        value:
          oneOf:
            - type: string
            - type: boolean
      required: [module, variable, type, value]

    LicensePlateRecognizerConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [licenseplaterecognizer] }
        variable: { type: string, enum: [api_key, enabled] }
        type: { type: string, enum: [string, bool] }
        value:
          oneOf:
            - type: string
            - type: boolean
      required: [module, variable, type, value]

    VirkdataConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [virkdata] }
        variable: { type: string, enum: [enabled, monthly_limit, secret_key] }
        type: { type: string, enum: [bool, int, string] }
        value:
          oneOf:
            - type: boolean
            - type: integer
            - type: string
      required: [module, variable, type, value]

    ShellyConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [shelly] }
        variable: { type: string, enum: [enabled, secret_key, server_url] }
        type: { type: string, enum: [bool, string] }
        value:
          oneOf:
            - type: boolean
            - type: string
      required: [module, variable, type, value]

    SelfServeConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [selfserve] }
        variable: { type: string, enum: [enabled, minute_product] }
        type: { type: string, enum: [bool, int] }
        value:
          oneOf:
            - type: boolean
            - type: integer
      required: [module, variable, type, value]

    EconomicConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/EconomicConfigEntry' } }
          required: [data]

    RecaptchaConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/RecaptchaConfigEntry' } }
          required: [data]

    EmailConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/EmailConfigEntry' } }
          required: [data]

    BackupsConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/BackupsConfigEntry' } }
          required: [data]

    BirdConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/BirdConfigEntry' } }
          required: [data]

    MotorApiConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/MotorApiConfigEntry' } }
          required: [data]

    StripeConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/StripeConfigEntry' } }
          required: [data]

    FxRatesApiConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/FxRatesApiConfigEntry' } }
          required: [data]

    GatewayApiConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/GatewayApiConfigEntry' } }
          required: [data]

    XlvaskConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/XlvaskConfigEntry' } }
          required: [data]

    EntraConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/EntraConfigEntry' } }
          required: [data]

    LimbleConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/LimbleConfigEntry' } }
          required: [data]

    OcrSpaceConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/OcrSpaceConfigEntry' } }
          required: [data]

    OpenAiConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/OpenAiConfigEntry' } }
          required: [data]

    LicensePlateRecognizerConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/LicensePlateRecognizerConfigEntry' } }
          required: [data]

    VirkdataConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/VirkdataConfigEntry' } }
          required: [data]

    ShellyConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/ShellyConfigEntry' } }
          required: [data]

    SelfServeConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/SelfServeConfigEntry' } }
          required: [data]

    ModuleConfigEntry:
      type: object
      properties:
        module:
          type: string
          description: Module name
        variable:
          type: string
          description: Configuration variable key
        type:
          type: string
          description: Stored value type in module_config
        value:
          $ref: '#/components/schemas/ModuleConfigValue'
      required:
        - module
        - variable
        - type
        - value

    ModuleConfigListResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/ModuleConfigEntry'
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    ModuleConfigUpdateResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: boolean
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    ModuleConfigTestResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: string
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    User:
      type: object
      properties:
        id:
          type: integer
          description: User ID
        customer_number:
          type: integer
          description: e-conomic customer number
        display_name:
          type: string
          description: User's display name
        group_id:
          type: integer
          description: User group/role ID
        phone_country_code:
          type: integer
          description: Phone country code
        phone:
          type: integer
          description: Phone number
        email:
          type: string
          format: email
          description: Email address
        sms_notifications_enabled:
          type: boolean
          description: SMS notifications enabled
        email_notifications_enabled:
          type: boolean
          description: Email notifications enabled
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    SubuserGrant:
      type: object
      properties:
        id:
          type: integer
        billing_customer_number:
          type: integer
          description: e-conomic customer number
        subuser:
          type: integer
          description: Subuser ID
        enabled:
          type: boolean
        note:
          type: string
          nullable: true
        permissions:
          type: array
          description: List of permission node keys
          items:
            type: string
            example: BOOKINGS_LIST
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
        deleted_at:
          type: string
          format: date-time
          nullable: true

    SubuserGrantCreateRequest:
      type: object
      required:
        - customer_number
        - subuser_id
      properties:
        customer_number:
          type: integer
          description: e-conomic customer number
        subuser_id:
          type: integer
          description: Subuser ID to grant permissions for
        enabled:
          type: boolean
          default: true
        note:
          type: string
          nullable: true
          maxLength: 65535
        permissions:
          type: array
          description: Optional list of permission keys; defaults will be applied if omitted
          items:
            type: string

    SubuserGrantUpdateRequest:
      type: object
      properties:
        enabled:
          type: boolean
        note:
          type: string
          nullable: true
          maxLength: 65535
        permissions:
          type: array
          items:
            type: string

    SubuserGrantSummary:
      type: object
      description: Summary of a subuser grant grouped by billing customer number
      properties:
        billing_customer_number:
          type: integer
          description: e-conomic customer number this grant applies to
        permissions:
          type: array
          description: List of permission node keys enabled for this customer
          items:
            type: string

    SubuserSelf:
      type: object
      description: Authenticated subuser profile with enabled grants
      properties:
        id:
          type: integer
        username:
          type: string
        name:
          type: string
          nullable: true
        email:
          type: string
          format: email
          nullable: true
        phone_country_code:
          type: integer
          nullable: true
        phone:
          type: integer
          nullable: true
        grants:
          type: array
          description: Enabled, non-deleted grants for the subuser grouped by billing customer number
          items:
            $ref: '#/components/schemas/SubuserGrantSummary'
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
        suspended_at:
          type: string
          format: date-time
          nullable: true
        two_factor_enabled:
          type: boolean
          description: Indicates if 2FA is enabled for this account

    PermissionNode:
      type: object
      properties:
        key:
          type: string
          description: Permission node key
        name:
          type: string
        description:
          type: string
        type:
          type: string
          description: Permission type (e.g., TOGGLE)
        default:
          type: boolean

    PermissionNodeGroup:
      type: object
      properties:
        group:
          type: string
        description:
          type: string
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/PermissionNode'

    UserCreate:
      type: object
      required:
        - customer_number
        - password
      properties:
        customer_number:
          type: integer
        password:
          type: string
          format: password
        display_name:
          type: string
        group_id:
          type: integer
        email:
          type: string
          format: email
        phone:
          type: integer
        phone_country_code:
          type: integer

    UserUpdate:
      type: object
      properties:
        id:
          type: integer
        customer_number:
          type: integer
        display_name:
          type: string
        group_id:
          type: integer
        email:
          type: string
          format: email
        phone:
          type: integer
        phone_country_code:
          type: integer

    Order:
      type: object
      properties:
        id:
          type: integer
          description: Order ID
        customer_id:
          type: integer
          description: Customer number
        customer_name:
          type: string
          description: Customer name
        user_id:
          type: integer
          description: User ID
        cashier_id:
          type: integer
          description: Cashier user ID

        cashier_name:
          type: string
          description: Cashier name
        department_id:
          type: integer
          description: Department ID
        status:
          type: string
          description: Order status
        total_net_amount:
          type: number
          format: float
          description: Total order amount
        po:
          type: string
          description: Purchase order number
          nullable: true
        lane:
          type: string
          description: Lane information
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CollectedInvoiceEconomicCompareResponse:
      type: object
      description: Result of comparing a collected invoice with its E-conomic counterpart
      properties:
        collected_invoice_id:
          type: integer
          description: The internal collected invoice ID
          example: 123
        draft_id:
          type: integer
          nullable: true
          description: E-conomic draft invoice ID, if present
          example: 456
        booked_id:
          type: integer
          nullable: true
          description: E-conomic booked invoice ID, if present
          example: 28368
        warnings:
          type: array
          description: List of warnings detected during comparison
          items:
            type: string
          example:
            - "Total amount mismatch for draft invoice ID 456: E-Conomic total is 867.5, internal total is 694"
        draft_total:
          type: number
          format: float
          nullable: true
          description: Total amount from the E-conomic draft (gross)
          example: 867.5
        booked_total:
          type: number
          format: float
          nullable: true
          description: Total amount from the E-conomic booked invoice (gross minus VAT if applicable)
          example: 694
        difference:
          type: number
          format: float
          nullable: true
          description: draft_total minus booked_total when both are available
          example: 0
        internal_total:
          type: number
          format: float
          description: Internal total amount for the collected invoice
          example: 694
        order_ids:
          type: array
          description: List of order IDs included in the collected invoice
          items:
            type: integer
          example: [38679, 39210]
      required:
        - collected_invoice_id
        - internal_total
        - order_ids

    SelfServeLaneStatus:
      type: object
      properties:
        id:
          type: integer
          description: Lane ID
        status:
          type: string
          description: Current lane status (e.g., IDLE, OCCUPIED)
        mode:
          type: string
          description: Current lane mode (e.g., AUTOMATIC, MANUAL)
        state:
          type: string
          description: Current lane state (e.g., READY, WASHING)
        wash_start_time:
          type: integer
          description: Timestamp when the wash started (0 if not washing)
          nullable: true
        elapsed_wash_time:
          type: integer
          description: Elapsed wash time in seconds
          nullable: true
        license_plate:
          type: string
          description: License plate of the vehicle in the lane
          nullable: true
        customer_number:
          type: integer
          description: Customer number associated with the current lane use
          nullable: true

    SelfServeConfig:
      type: object
      properties:
        enabled:
          type: boolean
          description: Whether the self-serve module is enabled
        minute_product:
          type: integer
          description: The product ID used for minute-based billing

    SelfserveLaneService:
      type: string
      description: Allowed self-serve lane service name
      enum:
        - MACHINE

    DepartmentSelfserveQuestion:
      type: object
      properties:
        id:
          type: integer
          description: Question ID
        department:
          type: integer
          description: Department ID
        lane:
          type: integer
          description: Lane ID
        product:
          type: integer
          description: Product ID
        condition_id:
          type: integer
          description: Question condition object ID
          nullable: true
        question:
          type: string
          description: Question text
        description:
          type: string
          description: Question description
        order_priority:
          type: integer
          description: Display order priority (lower numbers shown first)
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DepartmentSelfserveTask:
      type: object
      properties:
        id:
          type: integer
          description: Task ID
        department:
          type: integer
          description: Department ID
        lane:
          type: integer
          description: Lane ID
        product:
          type: integer
          description: Product ID
        condition_id:
          type: integer
          description: Condition ID (if conditional task)
          nullable: true
        task:
          type: string
          description: Task text
        description:
          type: string
          description: Task description
        order_priority:
          type: integer
          description: Display order priority (lower numbers shown first)
        services:
          type: array
          description: Services enabled by this task. Each item must be a valid service enum name.
          items:
            $ref: '#/components/schemas/SelfserveLaneService'
          default: []
        buttons:
          type: array
          description: Dynamic image button IDs enabled by this task.
          items:
            type: integer
          default: []
        dynamic_images_vehicle_type:
          type: integer
          nullable: true
          description: Optional vehicle type selection override for the machine UI.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DepartmentSelfserveCondition:
      type: object
      properties:
        id:
          type: integer
          description: Condition ID
        department:
          type: integer
          description: Department ID
        lane:
          type: integer
          description: Lane ID
        product:
          type: integer
          description: Product ID
        condition_id:
          type: integer
          description: Optional condition ID
          nullable: true
        name:
          type: string
          description: Condition name
        description:
          type: string
          description: Condition description
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DepartmentSelfserveConditionRule:
      type: object
      properties:
        id:
          type: integer
          description: Rule ID
        condition_id:
          type: integer
          description: Condition object ID
        type:
          type: string
          description: Condition type (e.g., IS_TRUE, IS_FALSE)
        object_type:
          type: string
          description: The object type to which the condition applies (e.g., question, task, etc.)
        object_id:
          type: integer
          description: The object id to which the condition applies
        name:
          type: string
          description: Condition name
        description:
          type: string
          description: Condition description
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DepartmentSelfserveVehicleCondition:
      type: object
      properties:
        id:
          type: integer
          description: Vehicle condition ID
        department:
          type: integer
          description: Department ID
        lane:
          type: integer
          description: Lane ID
        customer_id:
          type: integer
          description: Customer ID
          nullable: true
        reg:
          type: string
          description: Vehicle registration number
        question:
          type: integer
          description: Question ID
        value:
          type: boolean
          description: Answer value
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type: string
          format: date-time
          nullable: true

    OrderCreate:
      type: object
      required:
        - customer_id
        - department_id
      properties:
        customer_id:
          type: integer
        department_id:
          type: integer
        cashier_id:
          type: integer
        po:
          type: string
        lane:
          type: string

    OrderUpdate:
      type: object
      properties:
        id:
          type: integer
        customer_id:
          type: integer
        department_id:
          type: integer
        status:
          type: string
        po:
          type: string
        lane:
          type: string

    OrderItem:
      type: object
      properties:
        id:
          type: integer
        order_id:
          type: integer
        product_id:
          type: integer
        product_name:
          type: string
        quantity:
          type: integer
        unit_price:
          type: number
          format: float
        discount:
          type: number
          format: float
        total_price:
          type: number
          format: float

    OrderItemCreate:
      type: object
      required:
        - order_id
        - product_id
        - quantity
      properties:
        order_id:
          type: integer
        product_id:
          type: integer
        quantity:
          type: integer
        discount:
          type: number
          format: float

    OrderItemUpdate:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
        quantity:
          type: integer
        discount:
          type: number
          format: float

    Department:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
        economic_department_id:
          type: integer
        visible:
          type: boolean
        dimension:
          type: integer
        branding:
          type: integer
        longitude:
          type: number
          format: float
        latitude:
          type: number
          format: float
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DepartmentGuest:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        longitude:
          type: number
          format: float
        latitude:
          type: number
          format: float
        address:
          type: string
          description: Department address (same as description)
        self_serve_enabled:
          type: boolean
        lanes:
          type: array
          items:
            $ref: '#/components/schemas/DepartmentLaneGuest'

    DepartmentLaneGuest:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        status:
          type: string
        products:
          type: array
          items:
            type: integer
        machine_available:
          type: boolean

    DepartmentCreate:
      type: object
      required:
        - name
        - economic_department_id
      properties:
        name:
          type: string
        description:
          type: string
        economic_department_id:
          type: integer
        visible:
          type: boolean
        longitude:
          type: number
          format: float
        latitude:
          type: number
          format: float

    DepartmentUpdate:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
        visible:
          type: boolean
        longitude:
          type: number
          format: float
        latitude:
          type: number
          format: float

    DepartmentLane:
      type: object
      properties:
        id:
          type: integer
        department:
          type: integer
        name:
          type: string
        relay_in_id:
          type: string
        relay_out_id:
          type: string
        relay_machine_id:
          type: string
        dynamic_image_id:
          type: integer
          nullable: true
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DepartmentLaneCreate:
      type: object
      required:
        - department
        - name
      properties:
        department:
          type: integer
        name:
          type: string
        relay_in_id:
          type: string
        relay_out_id:
          type: string
        relay_machine_id:
          type: string
        dynamic_image_id:
          type: integer
          nullable: true
          minimum: 1

    DepartmentLaneUpdate:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
        department:
          type: integer
        name:
          type: string
        relay_in_id:
          type: string
        relay_out_id:
          type: string
        relay_machine_id:
          type: string
        dynamic_image_id:
          type: integer
          nullable: true
          minimum: 1

    DepartmentGate:
      type: object
      properties:
        id:
          type: integer
        department:
          type: integer
        is_entrance:
          type: boolean
        is_exit:
          type: boolean
        name:
          type: string
        config:
          $ref: '#/components/schemas/DepartmentGateConfig'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DepartmentGateConfig:
      type: object
      required:
        - type
      properties:
        type:
          type: string
          example: PHONE_CALL
        phone_number:
          type: string
          nullable: true
          example: +4512345678
        call_duration_threshold:
          type: integer
          nullable: true
          example: 10
      description: |
        Configuration for the department gate.
        If type is 'PHONE_CALL', 'phone_number' and 'call_duration_threshold' are required.

    DepartmentGateCreate:
      type: object
      required:
        - department
        - is_entrance
        - is_exit
        - name
        - config
      properties:
        department:
          type: integer
        is_entrance:
          type: boolean
        is_exit:
          type: boolean
        name:
          type: string
        config:
          $ref: '#/components/schemas/DepartmentGateConfig'

    DepartmentGateUpdate:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
        is_entrance:
          type: boolean
        is_exit:
          type: boolean
        name:
          type: string
        config:
          $ref: '#/components/schemas/DepartmentGateConfig'

    DepartmentRelay:
      type: object
      properties:
        id:
          type: integer
        department:
          type: integer
        relay_id:
          type: string
        name:
          type: string
        type:
          type: string
          enum: [SWITCH, TRIGGER]
        config:
          $ref: '#/components/schemas/DepartmentRelayConfig'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DepartmentRelayConfig:
      type: object
      properties:
        what_happens:
          type: string
          nullable: true
          example: open_gate
        webhook_token:
          type: string
          nullable: true
          example: secret_token
      description: |
        Configuration for the department relay.
        If the relay 'type' is 'TRIGGER', 'what_happens' and 'webhook_token' are required.

    DepartmentRelayCreate:
      type: object
      required:
        - department
        - relay_id
        - name
        - type
        - config
      properties:
        department:
          type: integer
        relay_id:
          type: string
        name:
          type: string
        type:
          type: string
          enum: [SWITCH, TRIGGER]
        config:
          $ref: '#/components/schemas/DepartmentRelayConfig'

    DepartmentRelayUpdate:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
        relay_id:
          type: string
        name:
          type: string
        type:
          type: string
          enum: [SWITCH, TRIGGER]
        config:
          $ref: '#/components/schemas/DepartmentRelayConfig'

    Product:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
        price:
          type: number
          format: float
        category_id:
          type: integer
        category_name:
          type: string
        visible:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ProductCreate:
      type: object
      required:
        - name
        - price
        - category_id
      properties:
        name:
          type: string
        description:
          type: string
        price:
          type: number
          format: float
        category_id:
          type: integer
        visible:
          type: boolean

    ProductUpdate:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
        price:
          type: number
          format: float
        category_id:
          type: integer
        visible:
          type: boolean

    Category:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CategoryCreate:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        description:
          type: string

    CategoryUpdate:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string

    ModuleActionLog:
      type: object
      properties:
        id:
          type: integer
          description: Log ID
        module:
          type: string
          description: The module name
        action:
          type: string
          description: The action name
        status_code:
          type: integer
          description: HTTP status code
        data:
          type: object
          description: The action data (JSON decoded)
        created_at:
          type: string
          format: date-time
          description: Log creation timestamp

    Booking:
      type: object
      properties:
        id:
          type: integer
        customer_id:
          type: integer
        department_id:
          type: integer
        booking_time:
          type: string
          format: date-time
        status:
          type: string
        created_at:
          type: string
          format: date-time

    BookingUpdate:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
        status:
          type: string
        booking_time:
          type: string
          format: date-time

    Vehicle:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
          description: Internal user ID owning the customer account
        reg:
          type: string
          description: Vehicle registration number
        customer_id:
          type: integer
        customer_name:
          type: string
        type:
          type: integer
          description: Product ID representing the vehicle wash type
        reference:
          type: string
          nullable: true
          description: Optional external reference/label
        wash_subscription:
          type: boolean
        barred:
          type: boolean
          description: True if the associated customer is barred
        addons:
          type: object
          properties:
            enabled: { type: integer }
            available: { type: integer }
            list:
              type: array
              items:
                type: object
        last_order_id:
          type: integer
          nullable: true
        xlvask:
          type: object
          nullable: true
          description: XL Vask vehicle data when available
        vehicle_types:
          type: array
          items:
            type: object
        created_at:
          type: string
          format: date-time

    Notification:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        title:
          type: string
        message:
          type: string
        read:
          type: boolean
        created_at:
          type: string
          format: date-time

    NotificationCreate:
      type: object
      required:
        - user_id
        - title
        - message
      properties:
        user_id:
          type: integer
        title:
          type: string
        message:
          type: string

    Permission:
      type: object
      properties:
        name:
          type: string
          description: Permission identifier
        description:
          type: string
          description: Human-readable description

    GoalsCriteria:
      type: object
      description: Goal evaluation criteria
      properties:
        type:
          type: string
          description: Criteria type
          enum: [PRODUCT, REVENUE, VISITS, NONE]
          example: PRODUCT
        target:
          type: number
          description: Target value for the goal
          example: 100
        label:
          type: string
          description: Optional short label/title for this goal criteria (max 255 characters)
          example: Q1 Revenue Goal
        start:
          type: string
          format: date-time
          description: Start of the evaluation window (ISO 8601)
        end:
          type: string
          format: date-time
          description: End of the evaluation window (ISO 8601)
        users:
          type: array
          description: List of user customer numbers included in the criteria
          items: { type: integer }
        departments:
          type: array
          description: List of department IDs included in the criteria
          items: { type: integer }
        products:
          type: array
          description: List of product IDs included in the criteria
          items: { type: integer }
        progress_alert_frequency:
          type: string
          description: |
            Frequency of progress alerts for the goal.
            Accepted values: DAILY, WEEKLY, MONTHLY, CHANGED, NONE.
            The canonical field name is snake_case `progress_alert_frequency`.
            For backward-compatibility the API also accepts camelCase `progressAlertFrequency` on input.
          enum: [DAILY, WEEKLY, MONTHLY, CHANGED, NONE]
          example: DAILY
        progress_alert_destination:
          type: string
          description: |
            Destination/channel where progress alerts should be delivered.
            Accepted values: SLACK, EMAIL, SMS, NONE.
            The canonical field name is snake_case `progress_alert_destination`.
            For backward-compatibility the API also accepts camelCase `progressAlertDestination` on input.
          enum: [SLACK, EMAIL, SMS, NONE]
          example: NONE
        progress_alert_progress_type:
          type: string
          description: |
            What part of the progress should be included in alert messages.
            Accepted values: ALL, PERCENTAGE_ONLY, COUNT_ONLY, COUNT_AND_TARGET, NONE.
            The canonical field name is snake_case `progress_alert_progress_type`.
            For backward-compatibility the API also accepts camelCase `progressAlertProgressType` on input.
          enum: [ALL, PERCENTAGE_ONLY, COUNT_ONLY, COUNT_AND_TARGET, NONE]
          example: ALL
        progress_alert_style:
          type: string
          description: |
            Presentation style of the alert.
            Accepted values: DEPARTMENT_COMPARE, COLLECTIVE, SINGLE_DEPARTMENT, NONE.
            The canonical field name is snake_case `progress_alert_style`.
            For backward-compatibility the API also accepts camelCase `progressAlertStyle` on input.
          enum: [DEPARTMENT_COMPARE, COLLECTIVE, SINGLE_DEPARTMENT, NONE]
          example: NONE
        progress_alert_format:
          type: string
          nullable: true
          description: |
            Optional custom template for the alert body. Supports tokens `{label}`, `{percent}`, `{count}`, `{target}`, `{timeframe}`, `{departments}`, `{prefix}`, `{body}`.
            Max length depends on destination: 160 characters for SMS; 1024 characters for EMAIL/SLACK/other.
            The canonical field name is snake_case `progress_alert_format`.
            For backward-compatibility the API also accepts camelCase `progressAlertFormat` on input.
        progress_alert_weekdays:
          type: array
          description: |
            Weekdays on which progress alerts should be sent.
            Use one or more of: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY.
            The canonical field name is snake_case `progress_alert_weekdays`.
            For backward-compatibility the API also accepts camelCase `progressAlertWeekdays` on input.
          items:
            type: string
            enum: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
          example: [MONDAY, WEDNESDAY, FRIDAY]
        progress_alert_time_of_day:
          type: string
          nullable: true
          description: |
            Time of day (with timezone) when progress alerts should be sent.
            Format: `HH:MMZ` or `HH:MM±HH:MM` (24-hour clock with UTC offset). Examples: `14:30Z`, `09:15+02:00`, `18:45-05:00`.
            The canonical field name is snake_case `progress_alert_time_of_day`.
            For backward-compatibility the API also accepts camelCase `progressAlertTimeOfDay` on input.
          pattern: '^([01]\d|2[0-3]):[0-5]\d(?:Z|[+-](?:[01]\d|2[0-3]):?[0-5]\d)$'
          example: "14:30+02:00"
        department_daily_targets:
          type: object
          description: |
            Optional per-department custom daily targets. Keys are department IDs and values are non-negative numbers representing the target per operating day for that department.
            If omitted, the daily target is split evenly across selected departments. The canonical field name is snake_case `department_daily_targets`.
            For backward-compatibility the API also accepts camelCase `departmentDailyTargets` on input.
          x-additionalPropertiesName: department_id
          additionalProperties:
            type: number
            minimum: 0
          example:
            "12": 3
            "15": 5

    GoalProgressDetails:
      type: object
      properties:
        count:
          type: number
          description: Current progress value
        target:
          type: number
          description: Target value for the period
        date_from:
          type: string
          nullable: true
          description: Inclusive period start datetime (ISO-8601), null when no lower bound applies
          example: "2026-01-01T00:00:00+00:00"
        date_end:
          type: string
          nullable: true
          description: Inclusive period end datetime (ISO-8601), null when no upper bound applies
          example: "2026-02-26T23:59:59+00:00"

    DepartmentGoalProgress:
      type: object
      title: Department progress details
      description: |
        Goal progress details for a single department across multiple timeframes.
        Timeframes are clamped to the goal timeframe (never before goal start and never after goal end).
      properties:
        all:
          $ref: '#/components/schemas/GoalProgressDetails'
        today:
          $ref: '#/components/schemas/GoalProgressDetails'
        week:
          $ref: '#/components/schemas/GoalProgressDetails'
        month:
          $ref: '#/components/schemas/GoalProgressDetails'
        year:
          $ref: '#/components/schemas/GoalProgressDetails'
        to_date:
          $ref: '#/components/schemas/GoalProgressDetails'

    DepartmentGoal:
      type: object
      properties:
        id:
          type: integer
        created_by:
          type: integer
          description: ID of the user who created the goal
        departments:
          type: array
          items: { type: integer }
        criteria:
          $ref: '#/components/schemas/GoalsCriteria'
        progress:
          type: object
          description: |
            Goal progress details for various timeframes.
            `year` starts at January 1 of the current year or the goal start, whichever is later.
            `to_date` starts at the goal start and ends at today (also clamped by goal end).
          properties:
            all:
              $ref: '#/components/schemas/GoalProgressDetails'
            today:
              $ref: '#/components/schemas/GoalProgressDetails'
            week:
              $ref: '#/components/schemas/GoalProgressDetails'
            month:
              $ref: '#/components/schemas/GoalProgressDetails'
            year:
              $ref: '#/components/schemas/GoalProgressDetails'
            to_date:
              $ref: '#/components/schemas/GoalProgressDetails'
            departmental_distribution:
              type: object
              description: |
                Progress details broken down by department. Keys are department IDs.
                Includes `all`, `today`, `week`, `month`, `year`, and `to_date` timeframes.
              x-additionalPropertiesName: department_id
              additionalProperties:
                $ref: '#/components/schemas/DepartmentGoalProgress'
              example:
                "12":
                  all:
                    count: 15
                    target: 100
                  today:
                    count: 2
                    target: 5
                  week:
                    count: 10
                    target: 35
                  month:
                    count: 15
                    target: 100
                  year:
                    count: 15
                    target: 100
                  to_date:
                    count: 15
                    target: 100
        created_at:
          type: string
          description: Creation timestamp
        updated_at:
          type: string
          description: Update timestamp

    DepartmentGoalCreate:
      type: object
      required: [departments, criteria]
      properties:
        departments:
          type: array
          items: { type: integer }
        criteria:
          $ref: '#/components/schemas/GoalsCriteria'

    DepartmentGoalUpdate:
      type: object
      required: [id]
      properties:
        id:
          type: integer
        departments:
          type: array
          items: { type: integer }
        criteria:
          $ref: '#/components/schemas/GoalsCriteria'
    Passkey:
      type: object
      properties:
        id:
          type: integer
        credential_id:
          type: string
          description: Base64URL-encoded credential ID
        name:
          type: string
          nullable: true
        algorithm:
          type: string
          example: ES256
        transports:
          type: array
          items:
            type: string
          example: ["usb", "nfc", "ble", "internal"]
        sign_count:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    PasskeyCreateRequest:
      type: object
      required: [credential_id, public_key, algorithm, transports]
      properties:
        credential_id:
          type: string
          description: Base64URL-encoded credential ID returned from WebAuthn
        public_key:
          type: string
          description: Base64URL-encoded public key (COSE or PEM as stored)
        algorithm:
          type: string
          example: ES256
        transports:
          type: array
          items:
            type: string
        name:
          type: string
          nullable: true
    PasskeyRenameRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string

    BirdVoiceCall:
      type: object
      properties:
        id:
          type: string
          example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
        channelId:
          type: string
          example: "a2545e48-fe8c-5741-9bdc-42a081076bc9"
        originator:
          type: object
          properties:
            number:
              type: object
              properties:
                type: { type: string, example: "pstn" }
                number: { type: string, example: "+4532330288" }
                countryIsoCode: { type: string, example: "DK" }
        receiver:
          type: object
          properties:
            contact:
              type: object
              properties:
                id: { type: string, example: "18229adf-af8c-404a-b036-ae193c22e33c" }
                identifierKey: { type: string, example: "phonenumber" }
                identifierValue: { type: string, example: "+4542331128" }
            number:
              type: object
              properties:
                type: { type: string, example: "pstn" }
                number: { type: string, example: "+4542331128" }
                countryIsoCode: { type: string, example: "DK" }
        from:
          type: string
          example: "+4532330288"
        to:
          type: string
          example: "+4542331128"
        direction:
          type: string
          example: "outgoing"
        status:
          type: string
          example: "completed"
        type:
          type: string
          example: "pstn"
        duration:
          type: integer
          example: 3
        hangupCauseCode:
          type: integer
          example: 16
        hangupSource:
          type: string
          example: "callee"
        sipInsights:
          type: object
          properties:
            hangupSipCode: { type: string, example: "200" }
        qualityInsights:
          type: object
          properties:
            mos: { type: string, example: "4.50" }
            pdd: { type: string, example: "2.15" }
        price:
          type: object
          properties:
            currencyCode: { type: string, example: "EUR" }
            amount: { type: string, example: "0.0079" }
        createdAt: { type: string, format: date-time, example: "2026-02-27T13:45:36.216Z" }
        updatedAt: { type: string, format: date-time, example: "2026-02-27T13:45:46.532Z" }
        ringingAt: { type: string, format: date-time, example: "2026-02-27T13:45:38.367Z" }
        answeredAt: { type: string, format: date-time, example: "2026-02-27T13:45:43.359Z" }
        endedAt: { type: string, format: date-time, example: "2026-02-27T13:45:46.368Z" }

    BirdVoiceCallListResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          type: object
          properties:
            nextPageToken:
              type: string
              example: "WzE3NzIxNTY5NTI0MDUsIjk5ZDU4M2VkLTQyMzAtNDExNy1hOTQ0LTllY2JjNzhmYWJlMSJd"
            results:
              type: array
              items: { $ref: '#/components/schemas/BirdVoiceCall' }
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []

    BirdVoiceCallSingleResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data: { $ref: '#/components/schemas/BirdVoiceCall' }
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []

    BirdTestOutboundCallResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          type: object
          properties:
            to: { type: string, example: "+45 42 33 11 28" }
            to_e164: { type: string, example: "+4542331128" }
            call_id: { type: string, nullable: true, example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09" }
            final_status: { type: string, nullable: true, example: "completed" }
            hangup_sent: { type: boolean, example: true }
            created_call: { $ref: '#/components/schemas/BirdVoiceCall' }
            last_call_snapshot: { $ref: '#/components/schemas/BirdVoiceCall' }
            hangup_response: { $ref: '#/components/schemas/BirdVoiceCall' }
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []

    BirdNumber:
      type: object
      properties:
        id:
          type: string
          example: "019c73dc-60f1-76c4-98b0-c4318706b938"
        workspaceId:
          type: string
          example: "3d5fae4f-9c2d-41aa-9840-28b18e6a94bc"
        type:
          type: string
          example: "national"
        country:
          type: string
          example: "DK"
        number:
          type: string
          example: "+4532330288"
        status:
          type: string
          example: "active"
        capabilities:
          type: object
          properties:
            voice: { $ref: '#/components/schemas/BirdNumberCapability' }
            sms: { $ref: '#/components/schemas/BirdNumberCapability' }
            mms: { $ref: '#/components/schemas/BirdNumberCapability' }
            fax: { $ref: '#/components/schemas/BirdNumberCapability' }
            whatsapp: { $ref: '#/components/schemas/BirdNumberCapability' }
        monthlyRecurringPrice:
          type: object
          properties:
            currencyCode: { type: string, example: "EUR" }
            amount: { type: integer, example: 1000000 }
            exponent: { type: integer, example: -6 }
        complianceRequirements:
          type: array
          items:
            type: object
            additionalProperties: true
        configurations:
          type: object
          additionalProperties: true
        createdAt: { type: string, format: date-time, example: "2026-02-19T03:05:48.529Z" }
        updatedAt: { type: string, format: date-time, example: "2026-02-19T03:08:17.753Z" }
        activatedAt: { type: string, format: date-time, nullable: true, example: "2026-02-19T03:05:48.529Z" }
        deactivatedAt: { type: string, format: date-time, nullable: true }
        deactivatesAt: { type: string, format: date-time, nullable: true }
        subscription:
          type: object
          additionalProperties: true
        endpointSubscription:
          type: object
          additionalProperties: true
        requirements:
          type: array
          items:
            type: object
            additionalProperties: true
        whatsApp:
          type: object
          additionalProperties: true
        endpoint:
          type: object
          additionalProperties: true

    BirdNumberCapability:
      type: object
      properties:
        inbound: { type: boolean }
        outbound: { type: boolean }

    BirdNumberListResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          type: object
          properties:
            results:
              type: array
              items: { $ref: '#/components/schemas/BirdNumber' }
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []

    BirdNumberSingleResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data: { $ref: '#/components/schemas/BirdNumber' }
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []
