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/api
      description: Local development server

security:
    - BearerAuth: []

tags:
    - name: Authentication
      description: User and employee authentication endpoints
    - name: Security
      description: Account security and passkey management endpoints
    - name: Users
      description: User management and customer operations
    - name: Search
      description: System-wide search endpoints
    - 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: Error Reports
      description: Authenticated application error reporting
    - name: Plate Scans
      description: License plate scanning operations
    - name: Config
      description: Module configuration management
    - name: Release Manager
      description: Release channel, deployment, and operation 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:
  /error-reports:
    post:
      tags:
        - Error Reports
      summary: Submit an authenticated user error report
      operationId: submitErrorReport
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ErrorReportSubmissionRequest'
      responses:
        '201':
          description: Error report submitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorReportResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /superuser/error-reports:
    get:
      tags:
        - Error Reports
      summary: List error reports for superusers
      operationId: listSuperuserErrorReports
      security:
        - BearerAuth: []
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [open, resolved, all]
            default: open
        - name: q
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Error reports retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorReportListResponse'
        '403':
          description: Missing superuser error report permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /superuser/error-reports/{id}:
    get:
      tags:
        - Error Reports
      summary: Get an error report detail
      operationId: getSuperuserErrorReport
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Error report retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorReportResponse'
        '404':
          description: Error report not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /superuser/error-reports/{id}/status:
    patch:
      tags:
        - Error Reports
      summary: Mark an error report open or resolved
      operationId: updateSuperuserErrorReportStatus
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ErrorReportStatusUpdateRequest'
      responses:
        '200':
          description: Error report status updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorReportResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Missing superuser error report resolve permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # 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
            format: uuid
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallCreateRequest'
      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
            format: uuid
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 1000
        - in: query
          name: pageToken
          schema:
            type: string
        - in: query
          name: startAt
          schema:
            type: string
            format: date-time
        - in: query
          name: endAt
          schema:
            type: string
            format: date-time
        - in: query
          name: status
          schema:
            type: string
        - in: query
          name: type
          schema:
            type: string
        - in: query
          name: to
          schema:
            type: string
        - in: query
          name: from
          schema:
            type: string
        - in: query
          name: duration
          schema:
            type: integer
        - in: query
          name: direction
          schema:
            type: string
        - in: query
          name: id
          schema:
            type: string
            format: uuid
        - in: query
          name: tag
          schema:
            oneOf:
              - type: string
              - type: array
                items:
                  type: string
      responses:
        '200':
          description: A list of calls
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallListResponse' }

  /bird/voice/calls/log:
    get:
      tags:
        - Bird
      summary: List workspace call log entries
      operationId: birdListVoiceCallsLog
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 1000
        - in: query
          name: pageToken
          schema:
            type: string
        - in: query
          name: startAt
          schema:
            type: string
            format: date-time
        - in: query
          name: endAt
          schema:
            type: string
            format: date-time
        - in: query
          name: channelId
          schema:
            oneOf:
              - type: string
              - type: array
                items:
                  type: string
                  format: uuid
        - in: query
          name: status
          schema:
            type: string
        - in: query
          name: type
          schema:
            type: string
        - in: query
          name: to
          schema:
            type: string
        - in: query
          name: from
          schema:
            type: string
        - in: query
          name: duration
          schema:
            type: integer
        - in: query
          name: direction
          schema:
            type: string
        - in: query
          name: id
          schema:
            type: string
            format: uuid
        - in: query
          name: tag
          schema:
            oneOf:
              - type: string
              - type: array
                items:
                  type: string
      responses:
        '200':
          description: Workspace call log entries
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallsLogResponse' }

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

  /bird/voice/calls/{id}/answer:
    post:
      tags:
        - Bird
      summary: Answer an incoming voice call by ID
      operationId: birdAnswerVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallAnswerRequest'
      responses:
        '200':
          description: Answer command accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' }

  /bird/voice/calls/{id}/ringing:
    post:
      tags:
        - Bird
      summary: Mark voice call as ringing by ID
      operationId: birdRingingVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallRingingRequest'
      responses:
        '200':
          description: Ringing command accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' }

  /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
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallHangupRequest'
      responses:
        '200':
          description: Hangup requested
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' }

  /bird/voice/calls/{id}/playback:
    post:
      tags:
        - Bird
      summary: Playback media on a voice call by ID
      operationId: birdPlaybackVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallPlaybackRequest'
      responses:
        '200':
          description: Playback command accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' }

  /bird/voice/calls/{id}/say:
    post:
      tags:
        - Bird
      summary: Say a message on an active voice call and optionally hang up
      operationId: birdSayOnVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallSayRequest'
      responses:
        '200':
          description: Say command accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' }

  /bird/voice/calls/{id}/gather:
    post:
      tags:
        - Bird
      summary: Gather input from a voice call by ID
      operationId: birdGatherVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallGatherRequest'
      responses:
        '200':
          description: Gather command accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' }

  /bird/voice/calls/{id}/bridge:
    post:
      tags:
        - Bird
      summary: Bridge a voice call by ID
      operationId: birdBridgeVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallBridgeRequest'
      responses:
        '200':
          description: Bridge command accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallBridgeResponse' }

  /bird/voice/calls/{id}/record:
    post:
      tags:
        - Bird
      summary: Record call audio by ID
      operationId: birdRecordVoiceCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallRecordRequest'
      responses:
        '200':
          description: Record command accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' }

  /bird/voice/calls/{id}/recordings:
    post:
      tags:
        - Bird
      summary: Create a call recording session
      operationId: birdCreateVoiceCallRecording
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallRecordingCreateRequest'
      responses:
        '200':
          description: Recording session created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallRecordingSingleResponse' }
    get:
      tags:
        - Bird
      summary: List call recordings for a voice call
      operationId: birdListVoiceCallRecordings
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 1000
        - in: query
          name: pageToken
          schema:
            type: string
      responses:
        '200':
          description: List of call recordings
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallRecordingListResponse' }

  /bird/voice/calls/{id}/recordings/{recordingId}:
    get:
      tags:
        - Bird
      summary: Get a single call recording
      operationId: birdGetVoiceCallRecording
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
        - in: path
          name: recordingId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Call recording details
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallRecordingSingleResponse' }
    patch:
      tags:
        - Bird
      summary: Update call recording state
      operationId: birdUpdateVoiceCallRecording
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
        - in: path
          name: recordingId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdVoiceCallRecordingUpdateRequest'
      responses:
        '200':
          description: Recording update accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallRecordingSingleResponse' }

  /bird/voice/calls/{id}/insights:
    get:
      tags:
        - Bird
      summary: Get voice call insights
      operationId: birdGetVoiceCallInsights
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Voice call insights
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdVoiceCallInsightsResponse' }

  /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
            format: uuid
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdTestOutboundCallRequest'
      responses:
        '200':
          description: Test call created and either hung up or timed out
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BirdTestOutboundCallResponse' }

  /bird/voice/calls/webhook/inbound:
    post:
      tags:
        - Bird
      summary: Process inbound Bird voice call lifecycle
      operationId: birdInboundVoiceCallWebhook
      description: >
        Stateful inbound-call webhook that owns department and gate selection for phone-controlled
        gates. The preferred Bird Flow Builder integration is the native-flow mode:
        send top-level `callId`, `channelId`, and `workspaceId` to fetch IVR prompt data,
        then submit the selected DTMF digits as `keys` in a follow-up request. In this mode
        the webhook returns a plain `200 OK` JSON body with `prompt`, `stage`, and `gather`
        settings that Bird native voice steps can consume directly. For backward compatibility,
        the webhook also supports the older `{ payload, request, waitConditions }` contract and
        returns a raw `202 Accepted` Bird `callCommand` gather envelope that resumes on
        `call_command_gather_finished`. The Bird Flow itself must answer the inbound call before
        invoking this HTTP step; the backend answer attempt is only a best-effort fallback.
        Department options are generated from `department_gates` records with `config.type=PHONE_CALL`,
        ordered by `departments.order_priority`, and support multi-digit DTMF selections such as `10#`.
        After a department is chosen, the webhook returns compact gate options, for example `Press 1 for exit`
        when exit is the only available phone-controlled gate for that department. When a valid gate is confirmed,
        the webhook opens the gate through the corresponding `department_gates` phone-call record and returns
        a `200 OK` completion result.
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
          description: Bird Workspace identifier (fallbacks to payload/state/module configuration)
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
          description: Bird Channel identifier (fallbacks to payload/state/module configuration)
        - in: query
          name: callId
          schema:
            type: string
          required: false
          description: Bird Call identifier (fallbacks to payload fields)
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdInboundCallWebhookRequest'
      responses:
        '200':
          description: Native-flow gather data or gate action result for this webhook invocation
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/BirdInboundCallWebhookFlowGatherResponse'
                  - $ref: '#/components/schemas/BirdInboundCallWebhookActionResultResponse'
        '202':
          description: Gather command accepted and returned to Bird
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdInboundCallWebhookGatherAcceptedResponse'
        '400':
          description: Malformed Bird webhook payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdInboundCallWebhookTransportErrorResponse'

  # Bird Numbers
  /bird/numbers:
    get:
      tags:
        - Bird
      summary: List your numbers
      operationId: birdListNumbers
      parameters:
        - in: query
          name: workspaceId
          required: false
          schema:
            type: string
            format: uuid
          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
            format: uuid
          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
            format: uuid
          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
            format: uuid
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdFlashCallCreateRequest'
      responses:
        '200':
          description: Flash call created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdFlashCallSingleResponse'
    get:
      tags:
        - Bird
      summary: List flash calls
      operationId: birdListFlashCalls
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
          description: Bird Workspace identifier (falls back to module configuration if omitted)
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
          description: Bird Channel identifier (falls back to module configuration if omitted)
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 1000
        - in: query
          name: pageToken
          schema:
            type: string
        - in: query
          name: startAt
          schema:
            type: string
            format: date-time
        - in: query
          name: endAt
          schema:
            type: string
            format: date-time
        - in: query
          name: status
          schema:
            type: string
        - in: query
          name: to
          schema:
            type: string
        - in: query
          name: from
          schema:
            type: string
        - in: query
          name: duration
          schema:
            type: integer
        - in: query
          name: id
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: A list of flash calls
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdFlashCallListResponse'

  /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
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Flash call details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdFlashCallSingleResponse'
    post:
      tags:
        - Bird
      summary: Complete/end a flash call by ID
      operationId: birdEndFlashCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdFlashCallEndRequest'
      responses:
        '200':
          description: Flash call completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdFlashCallSingleResponse'

  /bird/voice/flash-calls/hangup:
    post:
      tags:
        - Bird
      summary: Hang up flash calls using payload criteria
      operationId: birdHangupFlashCall
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdFlashCallHangupRequest'
      responses:
        '200':
          description: Flash call hangup accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdFlashCallHangupResponse'

  /bird/voice/flash-calls/end:
    post:
      tags:
        - Bird
      summary: Compatibility alias for flash hangup endpoint
      description: Deprecated alias for `/bird/voice/flash-calls/hangup`.
      deprecated: true
      operationId: birdEndFlashCallByNumbers
      parameters:
        - in: query
          name: workspaceId
          schema:
            type: string
            format: uuid
          required: false
        - in: query
          name: channelId
          schema:
            type: string
            format: uuid
          required: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirdFlashCallHangupRequest'
      responses:
        '200':
          description: Flash call hangup accepted (alias)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirdFlashCallHangupResponse'

  # Subusers (public registration + setup)
  /superuser/subusers/{subuser_id}:
    patch:
      tags:
        - Subusers
      summary: Update a chauffeur account profile as a superuser
      operationId: updateSuperuserSubuserProfile
      security:
        - BearerAuth: []
      parameters:
        - name: subuser_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserSubuserProfileUpdateRequest'
      responses:
        '200':
          description: Chauffeur account profile updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  subuser:
                    $ref: '#/components/schemas/SuperuserSubuserAccount'
        '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/subusers/{subuser_id}/password:
    post:
      tags:
        - Subusers
      summary: Set a chauffeur account password as a superuser
      operationId: setSuperuserSubuserPassword
      security:
        - BearerAuth: []
      parameters:
        - name: subuser_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserSubuserPasswordRequest'
      responses:
        '200':
          description: Chauffeur password set
          content:
            application/json:
              schema:
                type: object
                properties:
                  subuser:
                    $ref: '#/components/schemas/SuperuserSubuserAccount'
        '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/subusers/{subuser_id}/login-link:
    post:
      tags:
        - Subusers
      summary: Create a direct chauffeur login QR path
      description: Creates a chauffeur session and returns a `/login/qr` path scoped to the selected customer grant.
      operationId: createSuperuserSubuserLoginLink
      security:
        - BearerAuth: []
      parameters:
        - name: subuser_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserSubuserLoginLinkRequest'
      responses:
        '200':
          description: Direct login path created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSubuserLoginLinkResponse'
        '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/subusers/{subuser_id}/password-guide/{channel}/send:
    post:
      tags:
        - Subusers
      summary: Send a chauffeur new-password guide with a pre-authorized login link
      operationId: sendSuperuserSubuserPasswordGuideLink
      security:
        - BearerAuth: []
      parameters:
        - name: subuser_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
        - name: channel
          in: path
          required: true
          schema: { type: string, enum: [email, phone] }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserSubuserLoginLinkRequest'
      responses:
        '200':
          description: Password-guide delivery attempted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSubuserContactLinkSendResponse'
        '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/subusers/{subuser_id}/login-link/{channel}/send:
    post:
      tags:
        - Subusers
      summary: Send a chauffeur pre-authorized login link
      operationId: sendSuperuserSubuserLoginLink
      security:
        - BearerAuth: []
      parameters:
        - name: subuser_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
        - name: channel
          in: path
          required: true
          schema: { type: string, enum: [email, phone] }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserSubuserLoginLinkRequest'
      responses:
        '200':
          description: Login-link delivery attempted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSubuserContactLinkSendResponse'
        '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/subusers/{subuser_id}/verification/{channel}/send:
    post:
      tags:
        - Subusers
      summary: Send a chauffeur contact verification code as a superuser
      operationId: sendSuperuserSubuserVerificationCode
      security:
        - BearerAuth: []
      parameters:
        - name: subuser_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
        - name: channel
          in: path
          required: true
          schema: { type: string, enum: [email, phone] }
      responses:
        '200':
          description: Verification delivery attempted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubuserContactVerificationAdminSendResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /superuser/subusers/{subuser_id}/verification/{channel}:
    patch:
      tags:
        - Subusers
      summary: Set a chauffeur contact verification state as a superuser
      operationId: setSuperuserSubuserVerificationState
      security:
        - BearerAuth: []
      parameters:
        - name: subuser_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
        - name: channel
          in: path
          required: true
          schema: { type: string, enum: [email, phone] }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserSubuserVerificationStateRequest'
      responses:
        '200':
          description: Verification state updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSubuserVerificationStateResponse'
        '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/users/{user_id}/subusers:
    get:
      tags:
        - Subusers
      summary: List subusers for a superuser customer account
      description: Returns paginated chauffeur accounts for the customer number resolved from the selected user. Each chauffeur appears once; visible customer access grants are returned in `grants`.
      operationId: listSuperuserUserSubusers
      security:
        - BearerAuth: []
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
        - 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
          schema: { type: boolean }
      responses:
        '200':
          description: User-scoped subuser list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SubuserManagementRow'
        '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/users/{user_id}/subusers/summary:
    get:
      tags:
        - Subusers
      summary: Summarize subusers for a superuser customer account
      operationId: summarizeSuperuserUserSubusers
      security:
        - BearerAuth: []
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: User-scoped subuser summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubuserManagementSummary'
        '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/users/{user_id}/subusers/invite:
    post:
      tags:
        - Subusers
      summary: Invite a subuser for a superuser customer account
      operationId: inviteSuperuserUserSubuser
      security:
        - BearerAuth: []
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, phone_country_code, phone]
              properties:
                name: { type: string, minLength: 3, maxLength: 255 }
                phone_country_code: { type: integer }
                phone: { type: integer }
                note: { type: string, nullable: true, maxLength: 65535 }
                enabled: { type: boolean, default: true }
                permission_template_key:
                  type: string
                  enum: [deactivated, driver, booking_coordinator, fleet_admin]
                permissions:
                  type: array
                  items: { type: string }
      responses:
        '200':
          description: Driver invited or linked
        '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/users/{user_id}/subusers/{subuser_id}/invite/resend:
    post:
      tags:
        - Subusers
      summary: Resend a user-scoped subuser invite
      operationId: resendSuperuserUserSubuserInvite
      security:
        - BearerAuth: []
      parameters:
        - name: user_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
        - name: subuser_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      responses:
        '200':
          description: Invite resent
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /superuser/users/{user_id}/subusers/grants/{grant_id}:
    patch:
      tags:
        - Subusers
      summary: Update a user-scoped subuser grant
      operationId: updateSuperuserUserSubuserGrant
      security:
        - BearerAuth: []
      parameters:
        - name: user_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
        - name: grant_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubuserGrantUpdateRequest'
      responses:
        '200':
          description: Grant updated
        '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' }

  /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
                    email_verified:
                      type: boolean
                    email_verified_at:
                      type: string
                      format: date-time
                      nullable: true
                    phone_country_code:
                      type: integer
                      nullable: true
                    phone:
                      type: integer
                      nullable: true
                    phone_verified:
                      type: boolean
                    phone_verified_at:
                      type: string
                      format: date-time
                      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/me/verification:
    get:
      tags:
        - Subusers
      summary: Get current chauffeur contact verification status
      operationId: getCurrentSubuserVerification
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Current chauffeur verification status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubuserContactVerificationStatus'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /subusers/me/verification/{channel}/send:
    post:
      tags:
        - Subusers
      summary: Send a verification code for the current chauffeur
      operationId: sendCurrentSubuserVerificationCode
      security:
        - BearerAuth: []
      parameters:
        - name: channel
          in: path
          required: true
          schema: { type: string, enum: [email, phone] }
      responses:
        '200':
          description: Verification delivery attempted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubuserContactVerificationSendResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /subusers/me/verification/{channel}/verify:
    post:
      tags:
        - Subusers
      summary: Verify a current chauffeur contact code
      operationId: verifyCurrentSubuserContact
      security:
        - BearerAuth: []
      parameters:
        - name: channel
          in: path
          required: true
          schema: { type: string, enum: [email, phone] }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubuserContactVerificationVerifyRequest'
      responses:
        '200':
          description: Contact value verified
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubuserContactVerificationVerifyResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /subusers/{subuser_id}/verification/{channel}/send:
    post:
      tags:
        - Subusers
      summary: Send a verification code for an own-customer chauffeur
      operationId: sendManagedSubuserVerificationCode
      security:
        - BearerAuth: []
      parameters:
        - name: subuser_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
        - name: channel
          in: path
          required: true
          schema: { type: string, enum: [email, phone] }
      responses:
        '200':
          description: Verification delivery attempted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubuserContactVerificationManagedSendResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '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' }

  /subusers/permission-templates:
    get:
      tags:
        - Subusers
      summary: List simplified subuser permission templates
      description: Returns backend-owned driver access profiles and grouped capability metadata for subuser grants.
      operationId: listSubuserPermissionTemplates
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Permission templates fetched
          content:
            application/json:
              schema:
                type: object
                properties:
                  templates:
                    type: array
                    items:
                      $ref: '#/components/schemas/SubuserPermissionTemplate'
                  groups:
                    type: array
                    items:
                      $ref: '#/components/schemas/SubuserPermissionGroup'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '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
                      runtime_config:
                        type: object
                        properties:
                          economic:
                            type: object
                            properties:
                              transaction_draft_customer_number:
                                type: integer
                                nullable: true
                              default_distribution_department_id:
                                type: integer
                            additionalProperties: false
                        additionalProperties: true
        '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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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"
                ean:
                  type: string
                  description: Optional EAN used for e-invoicing in e-conomic
                  maxLength: 13
                  pattern: '^[0-9]{1,13}$'
                  example: "5790001234567"
                g_recaptcha_response:
                  type: string
                  description: reCAPTCHA verification token
      responses:
        '200':
          description: Matching e-conomic customer already existed and local registration was completed
          content:
            application/json:
              schema: {}
        '201':
          description: Customer registered successfully
          content:
            application/json:
              schema: {}
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          description: Customer number conflict between the submitted phone number and the existing or created e-conomic customer
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /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
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}
    delete:
      tags:
        - Users
      summary: Delete customer default department
      operationId: deleteCustomerDefaultDepartment
      parameters:
        - name: customer_number
          in: query
          schema: {type: integer}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /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}
                superuser_new_customer_email_notifications_enabled: {type: boolean}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /user/permissions:
    get:
      tags:
        - Users
      summary: Get user permissions
      operationId: getUserPermissions
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /customers:
    get:
      tags:
        - Users
      summary: List customers
      operationId: listCustomers
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
        - name: barred
          in: query
          required: false
          description: Optional e-conomic barred customer filter.
          schema:
            type: string
            enum: ['true', 'false', 'barred', 'active', '1', '0']
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /superuser/user/discounts:
    get:
      tags:
        - Users
      summary: Get user discounts and product fixed prices
      operationId: getUserDiscounts
      parameters:
        - name: user_id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}
    post:
      tags:
        - Users
      summary: Set user discount or product fixed price
      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}
                fixed_price:
                  type: integer
                  nullable: true
                  minimum: 0
                  description: Optional product-only fixed price. Omit to preserve the current fixed price, send null to clear it.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
        - name: confirmed
          in: query
          required: false
          description: Must be true to delete an order that is completed or has active items or attachments.
          schema:
            type: boolean
      responses:
        '200':
          description: Order deleted successfully
          content:
            application/json:
              schema: {}
        '409':
          description: Order deletion requires explicit confirmation
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        example: Order deletion requires confirmation
                      requires_confirmation:
                        type: boolean
                        example: true
                      protected_reasons:
                        type: array
                        items:
                          type: string
                          enum: [completed, order_items, attachments]
                      order_item_count:
                        type: integer
                        example: 2
                      attachment_count:
                        type: integer
                        example: 1
                      completed_at:
                        type: string
                        nullable: true
                  meta:
                    type: object
                  includes:
                    type: object
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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:
        '200':
          description: Order item added successfully
          content:
            application/json:
              schema: {}
        '400':
          description: Invalid order item or product blocked by an active customer rule
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CustomerRuleProductRestrictedResponse'
                  - $ref: '#/components/schemas/Error'
    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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}
    delete:
      tags:
        - Departments
      summary: Remove category from department
      operationId: removeDepartmentCategory
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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 step tokens in order. Accepts 0-indexed button IDs, "reset", "start", and "program_picker" as CSV, JSON array, or repeated query params.
          schema:
            oneOf:
              - type: string
              - type: array
                items:
                  oneOf:
                    - type: integer
                    - type: string
        - 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
          content:
            application/json:
              schema: {}
        '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/machine-types:
    get:
      tags:
        - Self-Serve
      summary: List reusable self-serve machine types
      operationId: listSelfserveMachineTypes
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - $ref: '#/components/parameters/SearchParam'
      responses:
        '200':
          description: Successfully retrieved machine types
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/SelfserveMachineType'
                  - type: array
                    items:
                      $ref: '#/components/schemas/SelfserveMachineType'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      tags:
        - Self-Serve
      summary: Add reusable self-serve machine type
      operationId: addSelfserveMachineType
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                description:
                  type: string
                  nullable: true
      responses:
        '200':
          description: Successfully added machine type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveMachineType'
    put:
      tags:
        - Self-Serve
      summary: Update reusable self-serve machine type
      operationId: updateSelfserveMachineType
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                  nullable: true
      responses:
        '200':
          description: Successfully updated machine type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveMachineType'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
        - Self-Serve
      summary: Delete reusable self-serve machine type
      operationId: deleteSelfserveMachineType
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successfully deleted machine type
          content:
            application/json:
              schema:
                type: string
                example: Machine type deleted
        '404':
          $ref: '#/components/responses/NotFound'

  /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. Questions are typically shared across departments and lanes by omitting department, lane, and product, which default to 0.
      operationId: addSelfserveQuestion
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - question
                - description
              properties:
                department:
                  type: integer
                  default: 0
                lane:
                  type: integer
                  default: 0
                product:
                  type: integer
                  default: 0
                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
        - name: machine_type_id
          in: query
          description: Filter by reusable machine type ID
          schema:
            type: integer
        - name: machine_type_id
          in: query
          description: Filter by reusable machine type 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. Either provide a reusable machine_type_id or a legacy department/lane/product scope.
      operationId: addSelfserveCondition
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - description
              properties:
                department:
                  type: integer
                  default: 0
                lane:
                  type: integer
                  default: 0
                product:
                  type: integer
                  default: 0
                machine_type_id:
                  type: integer
                  nullable: true
                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
                machine_type_id:
                  type: integer
                  nullable: true
                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
                vehicle_type:
                  type: integer
                  nullable: true
                  description: Optional product/vehicle type override used when refreshing the self-serve summary.
                vehicle_type_id:
                  type: integer
                  nullable: true
                  description: Alias for vehicle_type.
                activate_machine:
                  type: boolean
                  default: true
                  description: Whether the session synchronization may enable the machine relay. User wash-start saves answers with false.
                sync_relay_state:
                  type: boolean
                  default: true
                  description: Whether the answer mutation should synchronize live relay state.
      responses:
        '200':
          description: Successfully added vehicle condition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveVehicleConditionMutationResponse'
        '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
                vehicle_type:
                  type: integer
                  nullable: true
                  description: Optional product/vehicle type override used when refreshing the self-serve summary.
                vehicle_type_id:
                  type: integer
                  nullable: true
                  description: Alias for vehicle_type.
                activate_machine:
                  type: boolean
                  default: true
                  description: Whether the session synchronization may enable the machine relay.
                sync_relay_state:
                  type: boolean
                  default: true
                  description: Whether the mutation should synchronize live relay state.
      responses:
        '200':
          description: Successfully updated vehicle condition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentSelfserveVehicleConditionMutationResponse'
        '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: object
                properties:
                  message:
                    type: string
                    example: Condition deleted
                  selfserve:
                    allOf:
                      - $ref: '#/components/schemas/SelfserveWashSummary'
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /department/selfserve/vehicle/allowed:
    get:
      tags:
        - Self-Serve
      summary: Check whether self-serve is allowed for a vehicle on a lane
      description: Customers with own self-serve permissions may evaluate any registration plate for their wash. Persisted self-serve answers are only applied when they are scoped to the authenticated customer.
      operationId: getSelfserveVehicleAllowed
      parameters:
        - name: lane_id
          in: query
          required: true
          schema:
            type: integer
        - name: reg
          in: query
          required: true
          schema:
            type: string
        - name: vehicle_type_id
          in: query
          required: false
          description: Optional vehicle type override used when no vehicle is found by registration plate.
          schema:
            type: integer
            minimum: 0
        - name: vehicle_type
          in: query
          required: false
          description: Backward-compatible alias of `vehicle_type_id`.
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: Successfully evaluated self-serve eligibility
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveVehicleAllowedResponse'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /department/selfserve/washes/summary:
    get:
      tags:
        - Self-Serve
      summary: Get self-serve wash summary
      operationId: getSelfserveWashSummary
      parameters:
        - name: session_id
          in: query
          required: false
          schema:
            type: integer
        - name: lane_id
          in: query
          required: false
          schema:
            type: integer
        - name: reg
          in: query
          required: false
          schema:
            type: string
        - name: vehicle_type_id
          in: query
          required: false
          description: Optional vehicle type override used to refresh summary data for unknown or reassigned plates.
          schema:
            type: integer
            minimum: 0
        - name: vehicle_type
          in: query
          required: false
          description: Backward-compatible alias of `vehicle_type_id`.
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: Successfully retrieved self-serve wash summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveWashSummary'
        '403':
          $ref: '#/components/responses/Forbidden'
        '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. Either provide a reusable machine_type_id or a legacy department/lane/product scope.
      operationId: addSelfserveTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - task
                - description
              properties:
                department:
                  type: integer
                  default: 0
                lane:
                  type: integer
                  default: 0
                product:
                  type: integer
                  default: 0
                machine_type_id:
                  type: integer
                  nullable: true
                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
                machine_type_id:
                  type: integer
                  nullable: true
                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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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'

  /department/selfserve/studio/graph:
    get:
      tags:
        - Self-Serve
      summary: Get all-in-one self-serve studio graph
      description: Returns the replacement studio workspace graph backed by the schema_version 2 draft config. Conditions own grouped expression trees directly; standalone rule nodes are omitted from v2 graphs.
      operationId: getSelfserveStudioGraph
      parameters:
        - name: department
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Studio graph returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveStudioGraph'
    put:
      tags:
        - Self-Serve
      summary: Bulk save all-in-one self-serve studio graph changes
      description: Creates, updates, deletes, connects, disconnects, reorders, and upserts self-serve answer paths by editing the schema_version 2 draft config JSON. Condition connections create expression predicates; Path Editor upserts create normal generated condition and task nodes; layout remains separate from runtime behavior.
      operationId: saveSelfserveStudioGraph
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelfserveStudioGraphSaveRequest'
      responses:
        '200':
          description: Studio graph saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveStudioGraph'
        '422':
          $ref: '#/components/responses/BadRequest'

  /department/selfserve/studio/layout:
    put:
      tags:
        - Self-Serve
      summary: Save self-serve studio canvas layout
      description: Persists canvas-only node positions and viewport state. Layout does not affect runtime wash behavior.
      operationId: saveSelfserveStudioLayout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelfserveStudioLayoutSaveRequest'
      responses:
        '200':
          description: Layout saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveStudioLayout'

  /department/selfserve/studio/validate:
    post:
      tags:
        - Self-Serve
      summary: Validate self-serve studio graph
      operationId: validateSelfserveStudioGraph
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department]
              properties:
                department:
                  type: integer
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveStudioValidation'

  /department/selfserve/studio/simulate:
    post:
      tags:
        - Self-Serve
      summary: Simulate self-serve studio runtime
      operationId: simulateSelfserveStudioGraph
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department, lane_id, reg]
              properties:
                department: { type: integer }
                lane_id: { type: integer }
                reg: { type: string }
                customer_number: { type: integer, nullable: true }
                vehicle_type_id: { type: integer, nullable: true }
                config_source:
                  type: string
                  enum: [draft, published]
                  default: draft
                answer_overrides:
                  type: array
                  items:
                    type: object
                    required: [question_id]
                    properties:
                      question_id: { type: integer }
                      value:
                        type: boolean
                        nullable: true
                include_hardware:
                  type: boolean
                  default: true
                mode:
                  type: string
                  enum: [full_dry_run]
                  default: full_dry_run
      responses:
        '200':
          description: Simulator result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveStudioSimulationResponse'

  /department/selfserve/studio/path-outcomes:
    post:
      tags:
        - Self-Serve
      summary: Project grouped self-serve studio question path outcomes
      description: Enumerates feasible yes/no answer paths for the selected studio scope and groups terminal paths by resulting tasks, services, and dry-run signal timeline. No live hardware commands are sent.
      operationId: projectSelfserveStudioPathOutcomes
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelfserveStudioPathOutcomesRequest'
      responses:
        '200':
          description: Grouped path outcomes returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveStudioPathOutcomesResponse'
        '422':
          $ref: '#/components/responses/BadRequest'

  /department/selfserve/studio/path-outcomes/stream:
    post:
      tags:
        - Self-Serve
      summary: Stream self-serve studio question path outcome progress
      description: Streams newline-delimited JSON progress events while enumerating the complete feasible yes/no answer path space. Progress events contain the same response shape as the final result with partial outcomes and paths.
      operationId: streamSelfserveStudioPathOutcomes
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelfserveStudioPathOutcomesRequest'
      responses:
        '200':
          description: Newline-delimited path outcome progress events
          content:
            application/x-ndjson:
              schema:
                type: string
        '422':
          $ref: '#/components/responses/BadRequest'

  /department/selfserve/studio/path-confirmations:
    post:
      tags:
        - Self-Serve
      summary: Confirm or reset a projected self-serve studio path
      description: Stores confirmation for a projected terminal path using its stable path and result signatures. Projections report confirmed, unconfirmed, or stale when the resulting tasks, buttons, services, or signals change.
      operationId: confirmSelfserveStudioPath
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelfserveStudioPathConfirmationRequest'
      responses:
        '200':
          description: Path confirmation updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveStudioPathConfirmation'
        '422':
          $ref: '#/components/responses/BadRequest'

  /department/selfserve/studio/publish:
    post:
      tags:
        - Self-Serve
      summary: Publish self-serve studio draft
      operationId: publishSelfserveStudioDraft
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department]
              properties:
                department: { type: integer }
      responses:
        '200':
          description: Published version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveConfigVersion'

  /department/selfserve/studio/rollback:
    post:
      tags:
        - Self-Serve
      summary: Roll back self-serve studio to an earlier version
      operationId: rollbackSelfserveStudioDraft
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department, target_version_id]
              properties:
                department: { type: integer }
                target_version_id: { type: integer }
      responses:
        '200':
          description: Rollback version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveConfigVersion'

  /department/selfserve/studio/gateway-action:
    post:
      tags:
        - Self-Serve
      summary: Run permission-gated edge gateway action from studio
      operationId: runSelfserveStudioGatewayAction
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department, gateway_id, action]
              properties:
                department: { type: integer }
                gateway_id: { type: integer }
                action:
                  type: string
                  enum: [discovery, discover, update, uninstall, cancel, rotate_credentials, bindings]
                confirm:
                  type: boolean
                  description: Required for dangerous gateway actions such as uninstall and credential rotation.
                operation_id: { type: integer, nullable: true }
                request:
                  type: object
                  additionalProperties: true
                bindings:
                  type: array
                  items:
                    type: object
                    additionalProperties: true
      responses:
        '200':
          description: Gateway action result
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  # 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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  # 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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  # 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
          content:
            application/json:
              schema: {}
        '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, nullable: true}
                    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/booking-confirmation/resend:
    post:
      tags:
        - Bookings
      summary: Resend order booking confirmation
      description: Resends the customer booking confirmation email for an order booking. Requires `resend_booking_confirmations` and access to the booking's department.
      operationId: resendOrderBookingConfirmation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
      responses:
        '200':
          description: Booking confirmation resent successfully
          content:
            application/json:
              schema: {}
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /order-bookings/completion-confirmation/resend:
    post:
      tags:
        - Bookings
      summary: Resend order booking completion confirmation
      description: Resends the customer completion confirmation email with the wash certificate for a completed order booking. Requires `complete_bookings` and access to the booking's department.
      operationId: resendOrderBookingCompletionConfirmation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: {type: integer}
      responses:
        '200':
          description: Completion confirmation resent successfully
          content:
            application/json:
              schema: {}
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }

  /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
          content:
            application/json:
              schema: {}

  /admin/bookings/sync:
    post:
      tags:
        - Bookings
      summary: Sync booking from external system
      operationId: syncBooking
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /superuser/bookings/sync/all:
    post:
      tags:
        - Bookings
      summary: Sync all bookings from external system
      operationId: syncAllBookings
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  # 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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
    post:
      tags:
        - Invoices
      summary: Create collected invoice
      description: Create a new collected invoice
      operationId: createCollectedInvoice
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '201':
          description: Collected invoice created successfully
          content:
            application/json:
              schema: {}
    put:
      tags:
        - Invoices
      summary: Update collected invoice
      description: Update a collected invoice
      operationId: updateCollectedInvoice
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: Collected invoice updated successfully
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /collected-invoices/bulk-actions/preview:
    post:
      tags:
        - Invoices
      summary: Preview selected collected invoice bulk action
      description: Preview a selected invoice collection bulk action before any mutation is allowed.
      operationId: previewCollectedInvoiceBulkAction
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - action
                - invoice_collection_ids
              properties:
                action:
                  type: string
                  enum:
                    - remove_customer_rule_violations
                    - merge_collections
                    - split_by_month
                    - reset_hidden_item_prices
                    - queue_economic
                invoice_collection_ids:
                  type: array
                  minItems: 1
                  items:
                    type: integer
                options:
                  type: object
                  additionalProperties: true
                  properties:
                    target_invoice_collection_id:
                      type: integer
                      description: Required for merge_collections and must be one of the selected invoice collection ids.
                locale:
                  type: string
                  example: da
      responses:
        '200':
          description: Bulk action preview created successfully
          content:
            application/json:
              schema: {}
  /collected-invoices/bulk-actions/apply:
    post:
      tags:
        - Invoices
      summary: Apply selected collected invoice bulk action
      description: Applies a previously previewed bulk action only when the preview still matches the selection and confirmation_text matches the localized confirmation phrase.
      operationId: applyCollectedInvoiceBulkAction
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - preview_id
                - action
                - invoice_collection_ids
                - confirmation_text
              properties:
                preview_id:
                  type: string
                action:
                  type: string
                  enum:
                    - remove_customer_rule_violations
                    - merge_collections
                    - split_by_month
                    - reset_hidden_item_prices
                    - queue_economic
                invoice_collection_ids:
                  type: array
                  minItems: 1
                  items:
                    type: integer
                options:
                  type: object
                  additionalProperties: true
                  properties:
                    target_invoice_collection_id:
                      type: integer
                confirmation_text:
                  type: string
                  example: Bekræft
                locale:
                  type: string
                  example: da
      responses:
        '200':
          description: Bulk action applied successfully
          content:
            application/json:
              schema: {}

  /collected-invoices/economic:
    post:
      tags:
        - Invoices
      summary: Export collected invoice to e-conomic
      description: |
        Exports collected invoice to e-conomic.
        Uses async queue when available, otherwise falls back to synchronous processing.
      operationId: queueCollectedInvoiceEconomicTransfer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id:
                  type: integer
                  minimum: 1
                send_as_is:
                  type: boolean
                  default: false
      responses:
        '200':
          description: Collected invoice export processed synchronously (fallback)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferSynchronousFallbackResponse'
        '202':
          description: Collected invoice transfer queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse'
        '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' }

  /collected-invoices/stripe/book:
    post:
      tags:
        - Invoices
      summary: Export Stripe collected invoice to e-conomic
      description: |
        Exports a Stripe-backed collected invoice to e-conomic.
        Uses async queue when available, otherwise falls back to synchronous processing.
      operationId: queueStripeCollectedInvoiceEconomicTransfer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id:
                  type: integer
                  minimum: 1
      responses:
        '200':
          description: Stripe collected invoice export processed synchronously (fallback)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferSynchronousFallbackResponse'
        '202':
          description: Stripe collected invoice transfer queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse'
        '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' }

  /collected-invoices/economic/queue:
    get:
      tags:
        - Invoices
      summary: List collected-invoice e-conomic transfer queue jobs
      operationId: listCollectedInvoiceEconomicQueueJobs
      parameters:
        - name: status
          in: query
          required: false
          description: Comma-separated queue statuses to filter by.
          style: form
          explode: false
          schema:
            type: array
            items:
              $ref: '#/components/schemas/EconomicTransferQueueStatus'
            uniqueItems: true
            example: [QUEUED, FAILED]
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Queue jobs retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueListResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /collected-invoices/economic/queue/monitor:
    get:
      tags:
        - Invoices
      summary: Monitor current-user visible collected-invoice e-conomic transfer queue jobs
      operationId: monitorCollectedInvoiceEconomicQueueJobs
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: Queue monitor state retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueMonitorResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /collected-invoices/economic/queue/status:
    get:
      tags:
        - Invoices
      summary: Get collected-invoice e-conomic transfer queue job status
      operationId: getCollectedInvoiceEconomicQueueJobStatus
      parameters:
        - name: job_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Queue job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueStatusResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /collected-invoices/economic/queue/retry:
    post:
      tags:
        - Invoices
      summary: Retry failed collected-invoice queue job
      operationId: retryCollectedInvoiceEconomicQueueJob
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [job_id]
              properties:
                job_id:
                  type: integer
                  minimum: 1
      responses:
        '200':
          description: Queue job retried
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueRetryResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /collected-invoices/economic/queue/dismiss:
    post:
      tags:
        - Invoices
      summary: Clear one completed or failed collected-invoice queue job for the current user
      operationId: dismissCollectedInvoiceEconomicQueueJob
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [job_id]
              properties:
                job_id:
                  type: integer
                  minimum: 1
      responses:
        '200':
          description: Queue job cleared
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueDismissResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /collected-invoices/economic/queue/dismiss-terminal:
    post:
      tags:
        - Invoices
      summary: Clear all visible completed or failed collected-invoice queue jobs for the current user
      operationId: dismissCollectedInvoiceEconomicTerminalQueueJobs
      responses:
        '200':
          description: Terminal queue jobs cleared
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueDismissTerminalResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /collected-invoices/economic/queue/run:
    post:
      tags:
        - Invoices
      summary: Run one collected-invoice queue batch immediately
      operationId: runCollectedInvoiceEconomicQueueBatch
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                limit:
                  type: integer
                  minimum: 1
                  maximum: 10
                  default: 10
      responses:
        '200':
          description: Queue batch processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueRunResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /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'

  /collected-invoices/economic/pdf:
    get:
      tags:
        - Invoices
      summary: Download a collected invoice e-conomic PDF
      description: |
        Resolves the requested collected invoice context and returns a presigned URL for the
        draft or booked e-conomic invoice PDF.
      operationId: downloadCollectedInvoiceEconomicPdf
      parameters:
        - name: collected_invoice_id
          in: query
          required: true
          description: The internal collected invoice ID
          schema:
            type: integer
            minimum: 1
        - name: type
          in: query
          required: true
          description: Which e-conomic invoice PDF to download
          schema:
            type: string
            enum:
              - draft
              - booked
      responses:
        '200':
          description: PDF URL resolved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectedInvoiceEconomicPdfResponse'
        '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' }

  /collected-invoices/economic/v2/details:
    get:
      tags:
        - Invoices
      summary: Get deep V2 e-conomic invoice details
      description: |
        Returns normalized internal lines and best-effort fetched draft/booked e-conomic lines
        for a collected invoice, including department distributions and warnings.
      operationId: getCollectedInvoiceEconomicV2Details
      parameters:
        - name: collected_invoice_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Details resolved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectedInvoiceEconomicV2DetailsResponse'
        '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' }

  /collected-invoices/economic/v2/compare:
    get:
      tags:
        - Invoices
      summary: Compare internal invoice with draft/booked (V2)
      operationId: compareCollectedInvoiceEconomicV2
      parameters:
        - name: collected_invoice_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Comparison completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectedInvoiceEconomicV2CompareResponse'
              examples:
                exactMatch:
                  summary: Exact match between internal and draft/booked
                  value:
                    collected_invoice_id: 123
                    warnings: []
                    comparison:
                      totals:
                        internal_net_total: 694
                      targets:
                        draft:
                          target: draft
                          status: exact_match
                          overall_match: true
                        booked:
                          target: booked
                          status: exact_match
                          overall_match: true
                partialMismatch:
                  summary: Partial mismatch with line and department differences
                  value:
                    collected_invoice_id: 123
                    warnings:
                      - Non-billable line count differs
                    comparison:
                      totals:
                        internal_net_total: 694
                      targets:
                        draft:
                          target: draft
                          status: partial_mismatch
                          overall_match: false
                          mismatch_reasons:
                            - quantity_mismatch
                            - department_total_mismatch
                missingBooked:
                  summary: Missing booked target
                  value:
                    collected_invoice_id: 123
                    comparison:
                      totals:
                        internal_net_total: 694
                      targets:
                        booked:
                          target: booked
                          status: missing_target
                          overall_match: false
        '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' }

  /collected-invoices/economic/v2/compare/bulk:
    post:
      tags:
        - Invoices
      summary: Bulk compare collected invoices against draft/booked (V2)
      operationId: compareCollectedInvoiceEconomicV2Bulk
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [collected_invoice_ids]
              properties:
                collected_invoice_ids:
                  type: array
                  minItems: 1
                  maxItems: 200
                  items:
                    type: integer
                    minimum: 1
      responses:
        '200':
          description: Bulk comparison completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectedInvoiceEconomicV2CompareBulkResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /collected-invoices/economic/v2/revenue-statistics:
    get:
      tags:
        - Invoices
      summary: Get overall booked revenue statistics from e-conomic (V2)
      description: |
        Aggregates booked e-conomic revenue across invoices and lines, with optional filters
        for date range, customer(s), department(s), currency, and barred-customer status.
      operationId: getCollectedInvoiceEconomicV2RevenueStatistics
      parameters:
        - name: dateFrom
          in: query
          required: false
          description: Start date (inclusive), defaults to first day of current month.
          schema:
            type: string
            format: date
        - name: dateTo
          in: query
          required: false
          description: End date (inclusive), defaults to today.
          schema:
            type: string
            format: date
        - name: customer_numbers
          in: query
          required: false
          description: Comma-separated customer numbers to include.
          schema:
            type: string
            example: "42493959,42493960"
        - name: department_numbers
          in: query
          required: false
          description: Comma-separated department numbers to include.
          schema:
            type: string
            example: "75,10"
        - name: currency
          in: query
          required: false
          description: Restrict to a specific invoice currency.
          schema:
            type: string
            example: "DKK"
        - name: barred
          in: query
          required: false
          description: Filter by e-conomic customer barred status.
          schema:
            type: string
            enum: [all, barred, active]
            default: all
        - name: max_pages
          in: query
          required: false
          description: Safety cap for paginated e-conomic reads.
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 10
      responses:
        '200':
          description: Revenue statistics resolved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CollectedInvoiceEconomicV2RevenueStatisticsResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '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
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoicingPeriodResponseEnvelope'

  /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
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoicingFixedPricingDistributionResponse'

  /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
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoicingWashSubscriptionsDistributionResponse'

  /superuser/invoicing/period/distribution/v2/all:
    get:
      tags:
        - Invoices
      summary: Get version-aware historical distribution (all)
      operationId: getInvoicingPeriodDistributionV2All
      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: Version-aware historical distribution (all categories)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoicingDistributionV2AllResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /superuser/invoicing/period/distribution/v2/fixed-pricing:
    get:
      tags:
        - Invoices
      summary: Get version-aware historical fixed pricing distribution
      operationId: getInvoicingPeriodDistributionV2FixedPricing
      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: Version-aware fixed pricing distribution
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoicingDistributionV2FixedPricingResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /superuser/invoicing/period/distribution/v2/wash-subscriptions:
    get:
      tags:
        - Invoices
      summary: Get version-aware historical wash subscription distribution
      operationId: getInvoicingPeriodDistributionV2WashSubscriptions
      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: Version-aware wash subscription distribution
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoicingDistributionV2WashSubscriptionsResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /superuser/invoicing/period/distribution/v2/customer-prices:
    get:
      tags:
        - Invoices
      summary: Get version-aware historical customer-price discount distribution
      operationId: getInvoicingPeriodDistributionV2CustomerPrices
      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: Version-aware customer-price discount distribution
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoicingDistributionV2CustomerPricesResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /superuser/invoicing/period/distribution/v2/booked-department-75:
    get:
      tags:
        - Invoices
      summary: Get booked e-conomic department 75 redistribution
      operationId: getInvoicingPeriodDistributionV2BookedDepartment75
      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: Actual booked e-conomic department 75 net amounts redistributed to internal departments
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75Response'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /superuser/customers/pricing-history:
    get:
      tags:
        - Invoices
      summary: Get customer versioned pricing/subscription/discount timeline
      operationId: getCustomerPricingHistoryV2
      parameters:
        - name: customer_number
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
        - 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: Customer timeline resolved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerPricingHistoryResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /superuser/users/{user_id}/vehicles:
    get:
      tags: [Vehicles]
      summary: List vehicles for a selected superuser user
      operationId: listSuperuserUserVehicles
      parameters:
        - name: user_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
      responses:
        '200':
          description: User-scoped vehicles retrieved
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Vehicle'
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Vehicles]
      summary: Add a vehicle for a selected superuser user
      operationId: addSuperuserUserVehicle
      parameters:
        - name: user_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reg, type]
              properties:
                reg: { type: string, minLength: 2, maxLength: 12 }
                type: { type: integer }
                wash_subscription: { type: boolean, default: false }
                reference: { type: string, nullable: true, maxLength: 255 }
                customer_id:
                  type: integer
                  description: Optional guard value; must match the selected user's customer number.
      responses:
        '200':
          description: Vehicle created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Vehicle'
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Vehicles]
      summary: Edit a vehicle for a selected superuser user
      operationId: editSuperuserUserVehicle
      parameters:
        - name: user_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: { type: integer, minimum: 1 }
                reg: { type: string, minLength: 2, maxLength: 12 }
                type: { type: integer }
                wash_subscription: { type: boolean }
                reference: { type: string, nullable: true, maxLength: 255 }
                customer_id:
                  type: integer
                  description: Optional guard value; moving vehicles between customers is not allowed here.
      responses:
        '200':
          description: Vehicle updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Vehicle'
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Vehicles]
      summary: Delete a vehicle for a selected superuser user
      operationId: deleteSuperuserUserVehicle
      parameters:
        - name: user_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
        - name: id
          in: query
          required: true
          schema: { type: integer, minimum: 1 }
      responses:
        '200': { description: Vehicle deleted }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /superuser/users/{user_id}/vehicles/summary:
    get:
      tags: [Vehicles]
      summary: Summarize vehicles for a selected superuser user
      operationId: summarizeSuperuserUserVehicles
      parameters:
        - name: user_id
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      responses:
        '200':
          description: User-scoped vehicle summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VehicleManagementSummary'
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # 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]
              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
                  default: false
                  description: Defaults to false when omitted.
                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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

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

  # 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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  # 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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}
    delete:
      tags:
        - Orders
      summary: Delete Stripe payment intent
      operationId: deleteStripePaymentIntent
      parameters:
        - name: id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

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

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

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

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

  # 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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      api_commit_sha:
                        type: string
                        description: Running API commit SHA, or unknown when unavailable.

  /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
          content:
            application/json:
              schema: {}
        '403':
          $ref: '#/components/responses/Forbidden'

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

  /worker/debug/off:
    get:
      tags:
        - Worker
      summary: Disable worker debug
      operationId: disableWorkerDebug
      responses:
        '200':
          description: Worker debug disabled
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  # 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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /numberplatescans/post:
    get:
      tags:
        - Plate Scans
      summary: Get post-scan results
      operationId: getPlateScanPostResults
      responses:
        '200':
          description: Post-scan results retrieved successfully
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /relay/button/press/post:
    get:
      tags:
        - Plate Scans
      summary: Record machine start button press webhook
      operationId: addButtonPress
      parameters:
        - name: token
          in: query
          required: false
          schema: {type: string}
        - name: lane_id
          in: query
          required: false
          schema:
            type: integer
        - name: reg
          in: query
          required: false
          schema:
            type: string
      responses:
        '201':
          description: Button press recorded and linked to a self-serve wash session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MachineButtonPressWebhookResponse'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      tags:
        - Plate Scans
      summary: Record machine start button press webhook
      operationId: addButtonPressPost
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                lane_id:
                  type: integer
                reg:
                  type: string
      responses:
        '201':
          description: Button press recorded and linked to a self-serve wash session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MachineButtonPressWebhookResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /relay/machine/on/post:
    get:
      tags:
        - Plate Scans
      summary: Record Shelly machine ON signal webhook
      description: Accepts Shelly Cloud webhook/query parameters for input.toggle_on or switch.on and records the physical machine start signal for self-serve billing.
      operationId: recordShellyMachineOnSignal
      parameters:
        - name: token
          in: query
          required: false
          schema: {type: string}
        - name: lane_id
          in: query
          required: false
          schema:
            type: integer
        - name: relay_id
          in: query
          required: false
          schema:
            type: string
        - name: event
          in: query
          required: false
          schema:
            type: string
            enum: [input.toggle_on, switch.on]
        - name: reg
          in: query
          required: false
          schema:
            type: string
      responses:
        '201':
          description: Machine ON signal recorded and linked to a self-serve wash session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MachineButtonPressWebhookResponse'
        '202':
          description: Shelly signal was recognized but ignored
        '400':
          $ref: '#/components/responses/BadRequest'
    post:
      tags:
        - Plate Scans
      summary: Record Shelly machine ON signal webhook
      description: Accepts Shelly Cloud JSON webhook payloads for input.toggle_on or switch.on and records the physical machine start signal for self-serve billing.
      operationId: recordShellyMachineOnSignalPost
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                lane_id:
                  type: integer
                relay_id:
                  type: string
                event:
                  type: string
                  enum: [input.toggle_on, switch.on]
                component:
                  type: string
                  example: input:0
                state:
                  type: boolean
                output:
                  type: boolean
                reg:
                  type: string
      responses:
        '201':
          description: Machine ON signal recorded and linked to a self-serve wash session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MachineButtonPressWebhookResponse'
        '202':
          description: Shelly signal was recognized but ignored
        '400':
          $ref: '#/components/responses/BadRequest'

  # Module - e-conomic Endpoints
  /economic/customers/import:
    post:
      tags:
        - Modules
      summary: Import e-conomic customers
      description: Import customers from e-conomic
      operationId: importEconomicCustomers
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: Customers imported successfully
          content:
            application/json:
              schema: {}

  /economic/departments:
    get:
      tags:
        - Modules
      summary: Get e-conomic departments
      operationId: getEconomicDepartments
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /economic/products:
    get:
      tags:
        - Modules
      summary: Get e-conomic products
      operationId: getEconomicProducts
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
    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}
                ean:
                  type: string
                  description: Optional EAN used for e-invoicing in e-conomic
                  maxLength: 13
                  pattern: '^[0-9]{1,13}$'
                  example: "5790001234567"
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /economic/invoice/draft/export:
    post:
      tags:
        - Modules
      summary: Export draft invoice to e-conomic
      description: Exports draft invoice using queue processing when available, with synchronous fallback when queue dependencies are unavailable.
      operationId: queueDraftInvoiceExportToEconomic
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [order_id]
              properties:
                order_id:
                  type: integer
                  minimum: 1
      responses:
        '200':
          description: Draft invoice export processed synchronously (fallback)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferSynchronousFallbackResponse'
        '202':
          description: Draft invoice export queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse'
        '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' }

  /economic/invoice/draft/export/status:
    get:
      tags:
        - Modules
      summary: Get queued draft export job status
      operationId: getDraftInvoiceExportQueueStatus
      parameters:
        - name: job_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Queue job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueStatusResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /economic/invoice/draft/export/retry:
    post:
      tags:
        - Modules
      summary: Retry failed draft export queue job
      operationId: retryDraftInvoiceExportQueueJob
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [job_id]
              properties:
                job_id:
                  type: integer
                  minimum: 1
      responses:
        '200':
          description: Queue job retried
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueRetryResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /economic/invoice/export:
    post:
      tags:
        - Modules
      summary: Export invoice to e-conomic
      description: Exports booked invoice using queue processing when available, with synchronous fallback when queue dependencies are unavailable.
      operationId: queueInvoiceExportToEconomic
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [order_id]
              properties:
                order_id:
                  type: integer
                  minimum: 1
      responses:
        '200':
          description: Invoice export processed synchronously (fallback)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferSynchronousFallbackResponse'
        '202':
          description: Invoice export queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse'
        '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' }

  /economic/invoice/export/status:
    get:
      tags:
        - Modules
      summary: Get queued invoice export job status
      operationId: getInvoiceExportQueueStatus
      parameters:
        - name: job_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Queue job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueStatusResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /economic/invoice/export/retry:
    post:
      tags:
        - Modules
      summary: Retry failed invoice export queue job
      operationId: retryInvoiceExportQueueJob
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [job_id]
              properties:
                job_id:
                  type: integer
                  minimum: 1
      responses:
        '200':
          description: Queue job retried
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicTransferQueueRetryResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  # 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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /modules/stripe/invoice:
    post:
      tags:
        - Modules
      summary: Create Stripe invoice
      description: Create an invoice in Stripe
      operationId: createStripeInvoice
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '201':
          description: Stripe invoice created successfully
          content:
            application/json:
              schema: {}

  /modules/stripe/terminal/readers:
    get:
      tags:
        - Modules
      summary: List Stripe terminal readers
      operationId: listStripeTerminalReaders
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /modules/stripe/terminal/locations:
    get:
      tags:
        - Modules
      summary: List Stripe terminal locations
      operationId: listStripeTerminalLocations
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  # Module - Backup Endpoints
  /modules/backup/backups:
    get:
      tags:
        - Modules
      summary: List backups
      operationId: listBackupModules
      parameters:
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - name: offset
          in: query
          required: false
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupListResponse'
    post:
      tags:
        - Modules
      summary: Queue backup
      operationId: createBackupModule
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name: {type: string}
                description: {type: string}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupJobQueuedResponse'

  /modules/backup/jobs/{id}:
    get:
      tags: [Modules]
      summary: Get backup job status
      operationId: getBackupJob
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupJobResponse'

  /modules/backup/backups/{backup_uuid}/verify:
    post:
      tags: [Modules]
      summary: Queue backup verification
      operationId: verifyBackup
      parameters:
        - name: backup_uuid
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupJobQueuedResponse'

  /modules/backup/backups/{backup_uuid}/restore/preview:
    post:
      tags: [Modules]
      summary: Preview production restore from backup
      operationId: previewBackupRestore
      parameters:
        - name: backup_uuid
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupRestorePreviewResponse'

  /modules/backup/backups/{backup_uuid}/restore:
    post:
      tags: [Modules]
      summary: Queue production restore from backup
      operationId: restoreBackup
      parameters:
        - name: backup_uuid
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BackupRestoreRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupJobQueuedResponse'

  /modules/backup/restore-audit:
    get:
      tags: [Modules]
      summary: List backup restore audit entries
      operationId: listBackupRestoreAudit
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BackupRestoreAuditListResponse'

  # 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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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'

  /modules/usage/summary:
    get:
      tags:
        - Modules
      summary: Module usage and quota summary
      description: Returns registered module usage metrics, quota status, provider snapshots, and derived operational statistics.
      operationId: getModuleUsageSummary
      parameters:
        - in: query
          name: module
          required: false
          schema:
            type: string
        - in: query
          name: period
          required: false
          schema:
            type: string
            enum: [all, day, month, provider, all_time]
        - in: query
          name: status
          required: false
          schema:
            type: string
            enum: [ok, near_limit, exhausted, unlimited, unknown, disabled]
        - in: query
          name: date
          required: false
          schema:
            type: string
            format: date
      responses:
        '200':
          description: Module usage summary returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleUsageSummary'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /modules/usage/{moduleKey}:
    get:
      tags:
        - Modules
      summary: Module usage details
      operationId: getModuleUsageDetail
      parameters:
        - in: path
          name: moduleKey
          required: true
          schema:
            type: string
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - in: query
          name: date
          required: false
          schema:
            type: string
            format: date
      responses:
        '200':
          description: Module usage detail returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleUsageDetail'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /modules/quotas/{moduleKey}/{metricKey}:
    patch:
      tags:
        - Modules
      summary: Update module quota setting
      description: Updates quota enforcement settings and writable module-config hard limits for a registered metric.
      operationId: updateModuleQuotaSetting
      parameters:
        - in: path
          name: moduleKey
          required: true
          schema:
            type: string
        - in: path
          name: metricKey
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ModuleQuotaUpdateRequest'
      responses:
        '200':
          description: Module quota setting updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleUsageMetric'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/BadRequest' }

  # 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/wash/in-progress:
    get:
      tags:
        - Modules
      summary: Get in-progress self-serve wash customer and vehicle details
      description: |
        Returns the current open self-serve wash session details for a lane (if any),
        including resolved customer and vehicle details.
      operationId: getSelfServeLaneWashInProgress
      parameters:
        - name: lane_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: In-progress wash details resolved
          content:
            application/json:
              schema:
                type: object
                properties:
                  lane_id:
                    type: integer
                  in_progress:
                    type: boolean
                  session:
                    type: object
                    nullable: true
                    properties:
                      id:
                        type: integer
                      status:
                        type: string
                      reg:
                        type: string
                      customer_number:
                        type: integer
                        nullable: true
                      subuser_id:
                        type: integer
                        nullable: true
                      vehicle_id:
                        type: integer
                        nullable: true
                      vehicle_type_id:
                        type: integer
                        nullable: true
                      included_minutes:
                        type: integer
                        nullable: true
                      machine_type_id:
                        type: integer
                        nullable: true
                      machine_relay_enabled:
                        type: boolean
                      machine_relay_enabled_at:
                        type: string
                        nullable: true
                      machine_start_triggered:
                        type: boolean
                      machine_start_triggered_at:
                        type: string
                        nullable: true
                      wash_started_at:
                        type: string
                        nullable: true
                      created_at:
                        type: string
                      updated_at:
                        type: string
                        nullable: true
                  customer:
                    type: object
                    nullable: true
                    properties:
                      id:
                        type: integer
                        nullable: true
                      customer_number:
                        type: integer
                        nullable: true
                      display_name:
                        type: string
                        nullable: true
                      email:
                        type: string
                        nullable: true
                      phone_country_code:
                        type: integer
                        nullable: true
                      phone:
                        type: string
                        nullable: true
                  subuser:
                    type: object
                    nullable: true
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
                        nullable: true
                      username:
                        type: string
                        nullable: true
                  vehicle:
                    type: object
                    nullable: true
                    properties:
                      id:
                        type: integer
                      customer_id:
                        type: integer
                      type:
                        type: integer
                      reg:
                        type: string
                      reference:
                        type: string
                        nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /modules/self-serve/lane/wash/my-active-wash:
    get:
      tags:
        - Modules
      summary: Get the authenticated customer's active self-serve wash
      description: |
        Returns the latest active self-serve wash for the authenticated customer,
        without requiring the frontend to know or poll a lane id. Driver sessions
        are scoped to the authenticated subuser under the selected customer number.
      operationId: getMyActiveSelfServeWash
      responses:
        '200':
          description: Active self-serve wash details resolved
          content:
            application/json:
              schema:
                type: object
                properties:
                  lane_id:
                    type: integer
                  status:
                    type: string
                  in_progress:
                    type: boolean
                  elapsed_minutes:
                    type: integer
                  session:
                    type: object
                    nullable: true
                    properties:
                      subuser_id:
                        type: integer
                        nullable: true
                  customer:
                    type: object
                    nullable: true
                  subuser:
                    type: object
                    nullable: true
                  vehicle:
                    type: object
                    nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /modules/self-serve/sessions:
    get:
      tags:
        - Modules
      summary: List self-serve wash sessions
      description: Retrieve paginated self-serve wash sessions with search, filters, ordering, and active/open-only support.
      operationId: listSelfServeSessions
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - $ref: '#/components/parameters/SearchParam'
        - $ref: '#/components/parameters/FiltersParam'
        - name: order
          in: query
          required: false
          schema:
            type: string
          example: id:DESC
        - name: open_only
          in: query
          required: false
          schema:
            type: boolean
      responses:
        '200':
          description: Self-serve wash sessions retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  allOf:
                    - $ref: '#/components/schemas/SelfserveWashSession'
                    - type: object
                      properties:
                        elapsed_minutes:
                          type: integer
                        open:
                          type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /modules/self-serve/sessions/{id}:
    get:
      tags:
        - Modules
      summary: Get self-serve wash session detail
      operationId: getSelfServeSessionDetail
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Self-serve wash session detail retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveWashSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /modules/self-serve/lane/force/stop:
    post:
      tags:
        - Modules
      summary: Force stop a self-serve wash session
      description: |
        Clears the current self-serve wash session and lane runtime with RESET behavior only.
        This administrative action does not signal relays or gates. When billing is requested,
        only elapsed-minute billing is attempted before runtime is cleared.
      operationId: forceStopSelfServeLane
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - lane_id
                - bill
              properties:
                lane_id:
                  type: integer
                session_id:
                  type: integer
                  nullable: true
                bill:
                  type: boolean
                reason:
                  type: string
                  nullable: true
      responses:
        '200':
          description: Self-serve wash force stopped successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfserveForceStopResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'

  /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.
        Property gate commands (`OPEN_PROPERTY_ACCESS_GATE`, `OPEN_PROPERTY_EXIT_GATE`) are also supported here.
        Property gate commands require the matching explicit command permissions.
        Operator callers require the base command permission plus the command-specific permission. Authenticated
        customers with `add_own_department_selfserve_vehicle_conditions` may send `START` on enabled self-serve
        lanes. Customer `STOP` requires `list_own_department_selfserve_vehicle_conditions` and the customer's active
        self-serve wash in the target department.
      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, OPEN_PROPERTY_ACCESS_GATE, OPEN_PROPERTY_EXIT_GATE]
                license_plate:
                  type: string
                  description: Required for START command
                wash_type:
                  type: string
                  enum: [Manual, Machine]
                  description: Optional customer-selected wash type for START. When provided, Manual and Machine start actions use this explicit choice instead of inferring mode from allowed services.
                wash_mode:
                  type: string
                  enum: [manual, machine]
                  description: Lowercase alias for wash_type accepted by backend clients.
                customer_number:
                  type: integer
                  description: Required for START and RESERVE commands. The authenticated customer's number is applied server-side when omitted by user clients.
                defer_relay_side_effects:
                  type: boolean
                  default: false
                  description: For START, open the entrance gate as part of the command but defer cleaner and machine relay activation to explicit relay endpoints.
      responses:
        '200':
          description: Command sent successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfServeLaneStatus'
        '400':
          description: Command execution failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 'Failed to execute command: Failed to open property access gate.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /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.
        Operator callers require `modules_selfserve_lane_services_set_allowed`; authenticated customers with
        `add_own_department_selfserve_vehicle_conditions` may update their enabled self-serve lane before
        confirming a wash start.
      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/gate/open:
    post:
      tags:
        - Modules
      summary: Open a self-serve lane gate
      description: |
        Opens either the ENTRANCE or EXIT gate relay for a self-serve lane.
        Failures return a sanitized gate-specific message.
      operationId: openSelfServeLaneGate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - lane_id
                - gate
              properties:
                lane_id:
                  type: integer
                gate:
                  type: string
                  enum: [ENTRANCE, EXIT]
      responses:
        '200':
          description: Lane gate opened
          content:
            application/json:
              schema:
                type: object
                properties:
                  lane_id:
                    type: integer
                  gate:
                    type: string
                    enum: [ENTRANCE, EXIT]
                  opened:
                    type: boolean
                  state:
                    type: string
        '400':
          description: Gate open failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Failed to open entrance gate.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /modules/self-serve/lane/relay/machine/status:
    get:
      tags:
        - Modules
      summary: Get MACHINE relay status for a lane
      description: |
        Reads the current Shelly MACHINE relay status (`on`/`off`) for the given lane.
      operationId: getSelfServeLaneMachineRelayStatus
      parameters:
        - name: lane_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: MACHINE relay status retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfServeLaneMachineRelayStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /modules/self-serve/lane/relay/machine_program_picker/status:
    get:
      tags:
        - Modules
      summary: Get MACHINE_PROGRAM_PICKER relay status for a lane
      description: |
        Reads the current Shelly MACHINE_PROGRAM_PICKER relay status (`on`/`off`) for the given lane.
      operationId: getSelfServeLaneMachineProgramPickerRelayStatus
      parameters:
        - name: lane_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: MACHINE_PROGRAM_PICKER relay status retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfServeLaneMachineRelayStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /modules/self-serve/lane/relay/machine_program_picker/set:
    post:
      tags:
        - Modules
      summary: Set MACHINE_PROGRAM_PICKER relay status for a lane
      description: |
        Sets the Shelly MACHINE_PROGRAM_PICKER relay state for the lane to on or off and returns the latest status.
      operationId: setSelfServeLaneMachineProgramPickerRelayStatus
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - lane_id
                - on
              properties:
                lane_id:
                  type: integer
                on:
                  type: boolean
      responses:
        '200':
          description: MACHINE_PROGRAM_PICKER relay status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  lane_id:
                    type: integer
                  relay:
                    type: string
                    enum: [MACHINE_PROGRAM_PICKER]
                  requested_on:
                    type: boolean
                  relay_id:
                    type: string
                  online:
                    type: boolean
                  on:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /modules/self-serve/lane/relay/machine_cleaner/status:
    get:
      tags:
        - Modules
      summary: Get MACHINE_CLEANER relay status for a lane
      description: |
        Reads the current Shelly MACHINE_CLEANER relay status (`on`/`off`) for the given lane.
      operationId: getSelfServeLaneMachineCleanerRelayStatus
      parameters:
        - name: lane_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: MACHINE_CLEANER relay status retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SelfServeLaneMachineRelayStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /modules/self-serve/lane/relay/machine_cleaner/set:
    post:
      tags:
        - Modules
      summary: Set MACHINE_CLEANER relay status for a lane
      description: |
        Sets the Shelly MACHINE_CLEANER relay state for the lane to on or off and returns the latest status.
      operationId: setSelfServeLaneMachineCleanerRelayStatus
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - lane_id
                - on
              properties:
                lane_id:
                  type: integer
                on:
                  type: boolean
      responses:
        '200':
          description: MACHINE_CLEANER relay status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  lane_id:
                    type: integer
                  relay:
                    type: string
                    enum: [MACHINE_CLEANER]
                  requested_on:
                    type: boolean
                  relay_id:
                    type: string
                  online:
                    type: boolean
                  on:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /modules/self-serve/lane/relay/machine/set:
    post:
      tags:
        - Modules
      summary: Set MACHINE relay status for a lane
      description: |
        Sets the Shelly MACHINE relay state for the lane to on or off and returns the latest status.
      operationId: setSelfServeLaneMachineRelayStatus
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - lane_id
                - on
              properties:
                lane_id:
                  type: integer
                on:
                  type: boolean
      responses:
        '200':
          description: MACHINE relay status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  lane_id:
                    type: integer
                  relay:
                    type: string
                    enum: [MACHINE]
                  requested_on:
                    type: boolean
                  relay_id:
                    type: string
                  online:
                    type: boolean
                  on:
                    type: boolean
        '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. Operator callers require
        `modules_selfserve_lane_relay_enable_machine`; authenticated customers with
        `list_own_department_selfserve_vehicle_conditions` may enable it only for their active self-serve wash.
      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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}


  /modules/weatherapi/current:
    get:
      tags:
        - Modules
      summary: Get current weather
      description: Get current weather data from WeatherAPI for a location query
      operationId: weatherApiCurrent
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
          description: Location query (e.g. city, postal code, or latitude,longitude)
      responses:
        '200':
          description: Current weather retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WeatherApiObjectResponse'

  /modules/weatherapi/forecast:
    get:
      tags:
        - Modules
      summary: Get weather forecast
      description: Get forecast weather data from WeatherAPI
      operationId: weatherApiForecast
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
          description: Location query (e.g. city, postal code, or latitude,longitude)
        - name: days
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 14
          description: Number of forecast days
      responses:
        '200':
          description: Forecast weather retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WeatherApiObjectResponse'

  /modules/weatherapi/search:
    get:
      tags:
        - Modules
      summary: Search weather locations
      description: Search location suggestions from WeatherAPI
      operationId: weatherApiSearch
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
          description: Search text
      responses:
        '200':
          description: Location search results retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WeatherApiObjectResponse'

  /modules/workfeed/employees:
    get:
      tags:
        - Modules
      summary: List Workfeed employees
      description: List employees from Workfeed (`GET /companies/{CompanyID}/employees`)
      operationId: workfeedListEmployees
      responses:
        '200':
          description: Workfeed employees retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkfeedEmployeeListResponse'

  /modules/workfeed/employees/{id}:
    get:
      tags:
        - Modules
      summary: Get Workfeed employee
      description: Retrieve a single Workfeed employee by identifier
      operationId: workfeedGetEmployee
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Workfeed employee identifier
      responses:
        '200':
          description: Workfeed employee retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkfeedEmployeeSingleResponse'

  /modules/workfeed/shifts:
    get:
      tags:
        - Modules
      summary: List Workfeed shifts
      description: List Workfeed shifts (`GET /companies/{CompanyID}/shifts`)
      operationId: workfeedListShifts
      parameters:
        - name: startFrom
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: Only return shifts starting on or after this timestamp (ISO 8601)
        - name: startTo
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: Only return shifts starting before this timestamp (ISO 8601)
        - name: employeeID
          in: query
          required: false
          schema:
            type: string
          description: Filter shifts by employee ID
        - name: released
          in: query
          required: false
          schema:
            type: boolean
          description: Filter by released/published status
      responses:
        '200':
          description: Workfeed shifts retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkfeedShiftListResponse'

  /modules/workfeed/shifts/{id}:
    get:
      tags:
        - Modules
      summary: Get Workfeed shift
      description: Retrieve a single Workfeed shift by identifier
      operationId: workfeedGetShift
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Workfeed shift identifier
      responses:
        '200':
          description: Workfeed shift retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkfeedShiftSingleResponse'

  /modules/workfeed/departments:
    get:
      tags:
        - Modules
      summary: List Workfeed departments
      description: List departments from Workfeed (`GET /companies/{CompanyID}/departments`)
      operationId: workfeedListDepartments
      responses:
        '200':
          description: Workfeed departments retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkfeedDepartmentListResponse'

  /departments/weather:
    get:
      tags:
        - Departments
      summary: Get department weather timeline
      description: Returns hourly weather, washes, Workfeed employee-hours, and productivity status aggregated across selected departments (server local time). Default range is start of yesterday (`00:00`) to end of today (`23:00`). Use `date_from` and `date_to` (`YYYY-MM-DD`) together to override the range. If department coordinates are missing/invalid or WeatherAPI cannot resolve the location, weather data falls back silently and timeline slots default to `mostly_clear`. Slots return `unknown` status when they have no evaluable employee-hours or when one or more selected departments are missing department weather targets.
      operationId: getDepartmentWeatherTimeline
      parameters:
        - name: id
          in: query
          required: false
          schema:
            type: array
            items:
              type: integer
              minimum: 1
            minItems: 1
            uniqueItems: true
          style: form
          explode: true
          description: Department ID list. Repeat `id` to select multiple departments (`?id=1&id=2`).
        - name: ids
          in: query
          required: false
          schema:
            type: string
            example: '1,2,3'
          description: Optional CSV alternative for department IDs. Merged with `id` if both are provided. At least one of `id` or `ids` must be provided.
        - name: date_from
          in: query
          required: false
          schema:
            type: string
            format: date
            example: '2026-03-23'
          description: Optional range start date (`YYYY-MM-DD`). Must be used together with `date_to`.
        - name: date_to
          in: query
          required: false
          schema:
            type: string
            format: date
            example: '2026-03-24'
          description: Optional range end date (`YYYY-MM-DD`, inclusive). Must be used together with `date_from`.
      responses:
        '200':
          description: Department weather timeline retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentWeatherTimelineResponse'
              example:
                success: true
                meta: []
                includes: []
                data:
                  - date: '2026-03-23'
                    time: '00:00'
                    current: false
                    weather: mostly_cloudy
                    washes: 1
                    hours: 2.0
                    status: degraded
                  - date: '2026-03-24'
                    time: '13:00'
                    current: true
                    weather: rain
                    washes: 0
                    hours: 2.5
                    status: unhealthy
                  - date: '2026-03-24'
                    time: '14:00'
                    current: false
                    weather: mostly_clear
                    washes: 0
                    hours: 1.0
                    status: unknown

  /departments/weather/targets:
    get:
      tags:
        - Departments
      summary: Get department weather status targets
      description: Returns department-specific weather productivity thresholds used by `/departments/weather` to classify `healthy`, `degraded`, and `unhealthy` statuses.
      operationId: getDepartmentWeatherTargets
      parameters:
        - name: id
          in: query
          required: false
          schema:
            type: array
            items:
              type: integer
              minimum: 1
            minItems: 1
            uniqueItems: true
          style: form
          explode: true
          description: Department ID list. Repeat `id` to select multiple departments (`?id=1&id=2`).
        - name: ids
          in: query
          required: false
          schema:
            type: string
            example: '1,2,3'
          description: Optional CSV alternative for department IDs. Merged with `id` if both are provided. At least one of `id` or `ids` must be provided.
      responses:
        '200':
          description: Department weather targets retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentWeatherTargetsResponse'
              example:
                success: true
                meta: []
                includes: []
                data:
                  - department_id: 1
                    degraded_threshold: 1.0
                    healthy_threshold: 1.3
                    configured: true
                  - department_id: 2
                    degraded_threshold: null
                    healthy_threshold: null
                    configured: false
    put:
      tags:
        - Departments
      summary: Upsert department weather status targets
      description: Creates or updates the weather productivity thresholds for one department. `healthy_threshold` must be greater than or equal to `degraded_threshold`.
      operationId: upsertDepartmentWeatherTarget
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentWeatherTargetUpsertRequest'
            example:
              department_id: 1
              degraded_threshold: 1.0
              healthy_threshold: 1.3
      responses:
        '200':
          description: Department weather targets updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/DepartmentWeatherTarget'
                    required: [data]
              example:
                success: true
                meta: []
                includes: []
                data:
                  department_id: 1
                  degraded_threshold: 1.0
                  healthy_threshold: 1.3
                  configured: true

  /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
          content:
            application/json:
              schema: {}

  # 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
          content:
            application/json:
              schema: {}
        '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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /orders/attachments/download:
    get:
      tags:
        - Attachments
      summary: Get order attachment download link
      description: Return a legacy HTTPS download link for a specific order attachment
      operationId: downloadOrderAttachment
      parameters:
        - name: order_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
        - name: attachment_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Attachment download link resolved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderAttachmentDownloadLinkResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '502':
          description: Attachment storage is unavailable

  /orders/attachments/content:
    get:
      tags:
        - Attachments
      summary: Stream authenticated order attachment content
      description: Streams an attachment after validating order scope and attachment ownership
      operationId: streamOrderAttachmentContent
      parameters:
        - name: order_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
        - name: attachment_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
        - name: disposition
          in: query
          required: false
          schema:
            type: string
            enum: [inline, attachment]
            default: inline
      responses:
        '200':
          description: Attachment content streamed successfully
          headers:
            Content-Disposition:
              schema:
                type: string
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
            image/*:
              schema:
                type: string
                format: binary
            application/pdf:
              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' }
        '502':
          description: Attachment storage is unavailable

  # 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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  # 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
  /superuser/customer-rules/product-restrictions:
    get:
      tags: [Superuser, Users]
      summary: List global customer-rule product restrictions
      operationId: listCustomerRuleProductRestrictions
      responses:
        '200':
          description: Rule collections and product catalog retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerRuleProductRestrictionListResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/customer-rules/product-restrictions/{attribute}:
    put:
      tags: [Superuser, Users]
      summary: Atomically replace one customer rule's product collections
      operationId: replaceCustomerRuleProductRestriction
      parameters:
        - name: attribute
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/CustomerProductImpactAttribute'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerRuleProductRestrictionUpdateRequest'
      responses:
        '200':
          description: Rule configuration replaced
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerRuleProductRestrictionResponse'
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /customer/attributes:
    get:
      tags:
        - Users
      summary: Get customer attributes
      description: Get custom attributes for a customer. Authenticated customer accounts may read their own attributes without list_customer_attributes.
      operationId: getCustomerAttributes
      parameters:
        - name: customer_number
          in: query
          schema:
            type: integer
        - name: user_id
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Customer attributes retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerAttributesResponse'
    post:
      tags:
        - Users
      summary: Add customer attribute
      description: Add a custom attribute to a customer
      operationId: addCustomerAttribute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerAttributeMutationRequest'
      responses:
        '200':
          description: Customer attribute added successfully
          content:
            application/json:
              schema: {}
    delete:
      tags:
        - Users
      summary: Delete customer attribute
      description: Remove a custom attribute from a customer
      operationId: deleteCustomerAttribute
      parameters:
        - name: user_id
          in: query
          schema: { type: integer }
        - name: customer_number
          in: query
          schema: { type: integer }
        - name: attribute
          in: query
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Customer attribute deleted successfully
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}
    post:
      tags:
        - Users
      summary: Add customer note
      description: Add a note to a customer
      operationId: addCustomerNote
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '201':
          description: Customer note added successfully
          content:
            application/json:
              schema: {}
    delete:
      tags:
        - Users
      summary: Delete customer note
      description: Remove a note from a customer
      operationId: deleteCustomerNote
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: Customer note deleted successfully
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /search/system:
    get:
      tags:
        - Search
      summary: System-wide search
      description: Search across supported entities with permission-aware filtering, strict relevance filtering, and a single capped result set. Customer records and customer-related entities are matched against a local e-conomic customer index (name/address/email/CVR) that is refreshed by cron. Results are ordered by relevance, with recent records preferred when relevance is comparable.
      operationId: systemWideSearchGet
      parameters:
        - in: query
          name: query
          required: true
          schema:
            type: string
          description: Free-text query to search for. Supports deterministic domain synonyms such as `rabat` -> `discount`.
        - in: query
          name: include_types
          required: false
          schema:
            type: array
            items:
              $ref: '#/components/schemas/SystemSearchEntityType'
          style: form
          explode: false
          description: Comma-separated list of entity types to include. Defaults to all allowed types.
        - in: query
          name: exclude_types
          required: false
          schema:
            type: array
            items:
              $ref: '#/components/schemas/SystemSearchEntityType'
          style: form
          explode: false
          description: Comma-separated list of entity types to exclude.
        - in: query
          name: include_associations
          required: false
          schema:
            type: boolean
            default: true
          description: Include associated objects when matching a primary entity such as a customer.
        - in: query
          name: max_results
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 50
          description: Maximum number of relevance-ranked results to return. Values above 50 are capped to 50.
      responses:
        '200':
          description: Search results returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemSearchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
        - Search
      summary: System-wide search
      description: Search across supported entities using JSON request payload. Customer-related matches include local e-conomic customer index fields. Results use strict relevance filtering and are ordered by relevance, with recent records preferred when relevance is comparable.
      operationId: systemWideSearchPost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SystemSearchRequest'
      responses:
        '200':
          description: Search results returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemSearchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /superuser/search/system/cache:
    delete:
      tags:
        - Search
      summary: Clear system search caches
      description: Clears query-result caches for system-wide search.
      operationId: clearSystemSearchCache
      responses:
        '200':
          description: Cache cleared successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemSearchCacheClearResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /superuser/search/system/cache/rebuild:
    post:
      tags:
        - Search
      summary: Queue system search cache rebuild
      description: Queues a document-index rebuild request and clears active query cache entries immediately.
      operationId: rebuildSystemSearchCache
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SystemSearchCacheRebuildRequest'
      responses:
        '200':
          description: Cache rebuild queued successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemSearchCacheRebuildResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /superuser/system/security/summary:
    get:
      tags:
        - Superuser
      summary: Superuser security summary
      operationId: getSuperuserSecuritySummary
      responses:
        '200':
          description: Security summary returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSecuritySummaryResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/system/security/settings:
    get:
      tags:
        - Superuser
      summary: List observe-mode security settings
      operationId: getSuperuserSecuritySettings
      responses:
        '200':
          description: Security settings returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSecuritySettingsResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    patch:
      tags:
        - Superuser
      summary: Update observe-mode security settings
      operationId: updateSuperuserSecuritySettings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserSecuritySettingsUpdateRequest'
      responses:
        '200':
          description: Security settings updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSecuritySettingsResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/system/security/firewall-rules:
    get:
      tags:
        - Superuser
      summary: List application firewall rules
      operationId: listSuperuserSecurityFirewallRules
      responses:
        '200':
          description: Firewall rules returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSecurityFirewallRulesResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags:
        - Superuser
      summary: Create application firewall rule
      operationId: createSuperuserSecurityFirewallRule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserSecurityFirewallRuleMutation'
      responses:
        '201':
          description: Firewall rule created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSecurityFirewallRuleResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/system/security/firewall-rules/{id}:
    patch:
      tags:
        - Superuser
      summary: Update application firewall rule
      operationId: updateSuperuserSecurityFirewallRule
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserSecurityFirewallRuleMutation'
      responses:
        '200':
          description: Firewall rule updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSecurityFirewallRuleResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    delete:
      tags:
        - Superuser
      summary: Delete application firewall rule
      operationId: deleteSuperuserSecurityFirewallRule
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Firewall rule deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericSuccessResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/system/security/incidents:
    get:
      tags:
        - Superuser
      summary: List security incidents
      operationId: listSuperuserSecurityIncidents
      responses:
        '200':
          description: Security incidents returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSecurityIncidentsResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/system/security/incidents/{id}:
    get:
      tags:
        - Superuser
      summary: Get security incident detail
      operationId: getSuperuserSecurityIncident
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Security incident returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSecurityIncidentResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags:
        - Superuser
      summary: Update security incident status
      operationId: updateSuperuserSecurityIncident
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [open, acknowledged, resolved, false_positive]
              required: [status]
      responses:
        '200':
          description: Security incident updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSecurityIncidentResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /superuser/system/security/incidents/{id}/notes:
    post:
      tags:
        - Superuser
      summary: Add security incident note
      operationId: addSuperuserSecurityIncidentNote
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: string
                  minLength: 1
                  maxLength: 4000
              required: [note]
      responses:
        '201':
          description: Security incident note added successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSecurityIncidentResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /superuser/system/status:
    get:
      tags:
        - Superuser
      summary: Aggregated system status snapshot
      description: Returns a read-only snapshot of runtime health, dependency connectivity, module configuration/probe status, and active user session activity for the superuser dashboard.
      operationId: getSuperuserSystemStatus
      parameters:
        - in: query
          name: force
          required: false
          schema:
            type: boolean
            default: false
          description: Bypass cached external module probes for this request.
      responses:
        '200':
          description: System status snapshot returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserSystemStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /superuser/replication:
    get:
      tags:
        - Superuser
      summary: Database, Redis, and MinIO replication topology
      operationId: getSuperuserReplication
      parameters:
        - in: query
          name: refresh
          required: false
          schema:
            type: boolean
            default: false
          description: Refresh host connectivity and replication status before returning the topology.
      responses:
        '200':
          description: Replication topology returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserReplicationResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/replication/databases:
    post:
      tags:
        - Superuser
      summary: Retired database replication host creation
      deprecated: true
      operationId: addSuperuserDatabaseReplicationHost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserReplicationHostCreateRequest'
      responses:
        '410': { $ref: '#/components/responses/Gone' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/replication/redis:
    post:
      tags:
        - Superuser
      summary: Retired Redis replication host creation
      deprecated: true
      operationId: addSuperuserRedisReplicationHost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserReplicationHostCreateRequest'
      responses:
        '410': { $ref: '#/components/responses/Gone' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/replication/minio:
    post:
      tags:
        - Superuser
      summary: Retired MinIO replication host creation
      deprecated: true
      operationId: addSuperuserMinioReplicationHost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserReplicationHostCreateRequest'
      responses:
        '410': { $ref: '#/components/responses/Gone' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/replication/compose-template:
    post:
      tags:
        - Superuser
      summary: Retired replication Docker Compose template generation
      deprecated: true
      operationId: generateSuperuserReplicationComposeTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserReplicationComposeTemplateRequest'
      responses:
        '410': { $ref: '#/components/responses/Gone' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/replication/test-credentials:
    post:
      tags:
        - Superuser
      summary: Retired replication host credential test
      deprecated: true
      operationId: testSuperuserReplicationCredentials
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserReplicationUnsavedCredentialTestRequest'
      responses:
        '410': { $ref: '#/components/responses/Gone' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/replication/{kind}/{id}/test:
    post:
      tags:
        - Superuser
      summary: Retired replication host connectivity test
      deprecated: true
      operationId: testSuperuserReplicationHost
      parameters:
        - $ref: '#/components/parameters/SuperuserReplicationKindParam'
        - $ref: '#/components/parameters/SuperuserReplicationHostIdParam'
      responses:
        '410': { $ref: '#/components/responses/Gone' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/replication/{kind}/{id}/provision:
    post:
      tags:
        - Superuser
      summary: Retired replication host provisioning
      deprecated: true
      operationId: provisionSuperuserReplicationHost
      parameters:
        - $ref: '#/components/parameters/SuperuserReplicationKindParam'
        - $ref: '#/components/parameters/SuperuserReplicationHostIdParam'
      responses:
        '410': { $ref: '#/components/responses/Gone' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/replication/{kind}/{id}/promote:
    post:
      tags:
        - Superuser
      summary: Retired replication host promotion
      deprecated: true
      operationId: promoteSuperuserReplicationHost
      parameters:
        - $ref: '#/components/parameters/SuperuserReplicationKindParam'
        - $ref: '#/components/parameters/SuperuserReplicationHostIdParam'
      responses:
        '410': { $ref: '#/components/responses/Gone' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/replication/{kind}/{id}:
    patch:
      tags:
        - Superuser
      summary: Retired replication host rename
      deprecated: true
      operationId: renameSuperuserReplicationHost
      parameters:
        - $ref: '#/components/parameters/SuperuserReplicationKindParam'
        - $ref: '#/components/parameters/SuperuserReplicationHostIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserReplicationHostRenameRequest'
      responses:
        '410': { $ref: '#/components/responses/Gone' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    delete:
      tags:
        - Superuser
      summary: Retired replication host removal
      deprecated: true
      operationId: removeSuperuserReplicationHost
      parameters:
        - $ref: '#/components/parameters/SuperuserReplicationKindParam'
        - $ref: '#/components/parameters/SuperuserReplicationHostIdParam'
      responses:
        '410': { $ref: '#/components/responses/Gone' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify:
    get:
      tags:
        - Superuser
      summary: Coolify-managed replicated infrastructure state
      operationId: getSuperuserCoolify
      responses:
        '200':
          description: Coolify summary returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/load-balancer:
    get:
      tags:
        - Superuser
      summary: Coolify public gateway Load Balancer state
      operationId: getSuperuserCoolifyLoadBalancer
      responses:
        '200':
          description: Load Balancer summary returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/load-balancer/reconcile:
    post:
      tags:
        - Superuser
      summary: Reconcile Coolify public gateway Load Balancer state
      operationId: reconcileSuperuserCoolifyLoadBalancer
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                dry_run:
                  type: boolean
                  default: true
                enforce:
                  type: boolean
                  default: false
      responses:
        '200':
          description: Load Balancer reconcile result returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerReconcileResponse'
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/load-balancer/routes/deploy:
    post:
      tags:
        - Superuser
      summary: Deploy the Coolify API route for the public gateway host
      operationId: deploySuperuserCoolifyGatewayRoutes
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                dry_run:
                  type: boolean
                  default: true
                enforce:
                  type: boolean
                  default: false
      responses:
        '200':
          description: Gateway application route deploy result returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyGatewayRouteDeployResponse'
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/load-balancer/api/deploy:
    post:
      tags:
        - Superuser
      summary: Deploy the latest Coolify API code for the public gateway host
      operationId: deploySuperuserCoolifyGatewayApiCode
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                dry_run:
                  type: boolean
                  default: true
                enforce:
                  type: boolean
                  default: false
                deploy_routes:
                  type: boolean
                  default: true
      responses:
        '200':
          description: Gateway API code deployment result returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyGatewayRouteDeployResponse'
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/gateways:
    get:
      tags:
        - Superuser
      summary: List Coolify public gateway Load Balancer targets
      operationId: listSuperuserCoolifyGateways
      responses:
        '200':
          description: Gateway targets returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyGatewaysResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags:
        - Superuser
      summary: Create or update a Coolify public gateway Load Balancer target
      operationId: saveSuperuserCoolifyGateway
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserCoolifyGatewaySaveRequest'
      responses:
        '201':
          description: Gateway target saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyGatewayResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/gateways/{id}/test:
    post:
      tags:
        - Superuser
      summary: Probe a Coolify public gateway target
      operationId: testSuperuserCoolifyGateway
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Gateway target probe result returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyOperationResponse'
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/instances:
    post:
      tags:
        - Superuser
      summary: Create Coolify API connection
      operationId: createSuperuserCoolifyInstance
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserCoolifyInstanceCreateRequest'
      responses:
        '201':
          description: Coolify instance created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyInstanceResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/instances/{id}/test:
    post:
      tags:
        - Superuser
      summary: Test Coolify API connection
      operationId: testSuperuserCoolifyInstance
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Coolify connection test returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyOperationResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /superuser/coolify/instances/{id}/placement:
    get:
      tags:
        - Superuser
      summary: Discover Coolify placement options
      operationId: discoverSuperuserCoolifyInstancePlacement
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Coolify project, environment, and server options returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyPlacementResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /superuser/coolify/targets:
    get:
      tags:
        - Superuser
      summary: List Coolify-managed replication targets
      operationId: listSuperuserCoolifyTargets
      parameters:
        - in: query
          name: kind
          required: false
          schema:
            type: string
            enum: [database, redis, minio]
      responses:
        '200':
          description: Coolify targets returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyTargetsResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags:
        - Superuser
      summary: Create Coolify-managed passive replication target
      operationId: createSuperuserCoolifyTarget
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuperuserCoolifyTargetCreateRequest'
      responses:
        '201':
          description: Coolify target created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserCoolifyOperationResponse'
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/targets/{id}/reconcile:
    post:
      tags:
        - Superuser
      summary: Reconcile a passive Coolify target
      operationId: reconcileSuperuserCoolifyTarget
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200': { description: Reconcile completed or queued }
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/targets/{id}/deploy:
    post:
      tags:
        - Superuser
      summary: Deploy and provision a passive Coolify target
      operationId: deploySuperuserCoolifyTarget
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200': { description: Deploy and provision flow completed or queued }
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/targets/{id}/restart:
    post:
      tags:
        - Superuser
      summary: Restart a passive Coolify target
      operationId: restartSuperuserCoolifyTarget
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200': { description: Restart requested }
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/targets/{id}/failover:
    post:
      tags:
        - Superuser
      summary: Promote a Coolify-managed replica through replication failover
      operationId: failoverSuperuserCoolifyTarget
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200': { description: Failover action returned }
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /superuser/coolify/targets/{id}:
    delete:
      tags:
        - Superuser
      summary: Delete a Coolify target mapping with destructive confirmation
      operationId: deleteSuperuserCoolifyTarget
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [confirm]
              properties:
                confirm:
                  type: string
                  description: Must equal delete-coolify-target-{id}.
                delete_resource:
                  type: boolean
                  default: false
      responses:
        '200': { description: Target deleted }
        '409': { $ref: '#/components/responses/Conflict' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  # 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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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: getRecaptchaModuleConfig
      responses:
        '200':
          description: reCAPTCHA configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecaptchaConfigListResponse'
    post:
      tags: [Config]
      summary: Update reCAPTCHA config
      operationId: updateRecaptchaConfig
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: Email configuration test completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigTestResponse'

  /slack/config:
    get:
      tags: [Config]
      summary: Get Slack config
      operationId: getSlackConfig
      responses:
        '200':
          description: Slack configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlackConfigListResponse'
    post:
      tags: [Config]
      summary: Update Slack config
      operationId: updateSlackConfig
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: Slack configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /slack/config/test:
    post:
      tags: [Config]
      summary: Test Slack customer registration webhook
      operationId: testSlackCustomerRegistrationWebhook
      responses:
        '200':
          description: Slack customer registration webhook test completed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlackConfigTestResponse'
        '400':
          description: Slack customer registration webhook URL is not configured
        '502':
          description: Slack customer registration webhook test failed

  /slack/config/internal-department-goal-progress:
    get:
      tags: [Config]
      summary: Get Slack internal department goal progress config
      operationId: getSlackInternalDepartmentGoalProgressConfig
      responses:
        '200':
          description: Slack internal department goal progress configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigResponse'
    post:
      tags: [Config]
      summary: Update Slack internal department goal progress config
      operationId: updateSlackInternalDepartmentGoalProgressConfig
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigUpdate'
      responses:
        '200':
          description: Slack internal department goal progress configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigResponse'

  /slack/config/internal-department-goal-progress/test:
    post:
      tags: [Config]
      summary: Test Slack internal department goal progress webhook
      operationId: testSlackInternalDepartmentGoalProgressWebhook
      responses:
        '200':
          description: Slack internal department goal progress webhook test completed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SlackConfigTestResponse'
        '400':
          description: Slack internal department goal progress webhook URL is not configured
        '502':
          description: Slack internal department goal progress webhook test failed

  /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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: FXRatesAPI configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'


  /weatherapi/config:
    get:
      tags: [Config]
      summary: Get WeatherAPI config
      operationId: getWeatherApiConfig
      responses:
        '200':
          description: WeatherAPI configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WeatherApiConfigListResponse'
    post:
      tags: [Config]
      summary: Update WeatherAPI config
      operationId: updateWeatherApiConfig
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: WeatherAPI configuration updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModuleConfigUpdateResponse'

  /workfeed/config:
    get:
      tags: [Config]
      summary: Get Workfeed config
      operationId: getWorkfeedConfig
      responses:
        '200':
          description: Workfeed configuration retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkfeedConfigListResponse'
              examples:
                default:
                  summary: Workfeed module configuration
                  value:
                    success: true
                    data:
                      - module: workfeed
                        variable: enabled
                        type: bool
                        value: true
                      - module: workfeed
                        variable: api_url
                        type: string
                        value: https://europe-west1-production-eu-327a3.cloudfunctions.net/api
                      - module: workfeed
                        variable: api_key
                        type: string
                        value: wf_live_xxxxxxxxxxxxxxxxx
                      - module: workfeed
                        variable: CompanyID
                        type: string
                        value: "123456"
                    meta: []
                    includes: []
    post:
      tags: [Config]
      summary: Update Workfeed config
      operationId: updateWorkfeedConfig
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      responses:
        '200':
          description: Workfeed 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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
      requestBody:
        required: false
        content:
          application/json:
            schema: {}
      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
          content:
            application/json:
              schema: {}
        '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}
                address: {type: string, nullable: true}
                phone_country_code: {type: integer, nullable: true}
                phone: {type: integer, nullable: true}
                email: {type: string, nullable: true}
                website: {type: string, nullable: true}
                banner: {type: string, nullable: true}
                logo: {type: string, nullable: true}
                favicon: {type: string, nullable: true}
                signature: {type: string, nullable: true}
      responses:
        '200':
          description: Branding option added successfully
          content:
            application/json:
              schema: {}
        '400':
          $ref: '#/components/responses/BadRequest'
        '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, nullable: true}
                description: {type: string, nullable: true}
                cvr: {type: integer, nullable: true}
                address: {type: string, nullable: true}
                phone_country_code: {type: integer, nullable: true}
                phone: {type: integer, nullable: true}
                email: {type: string, nullable: true}
                website: {type: string, nullable: true}
                banner: {type: string, nullable: true}
                logo: {type: string, nullable: true}
                favicon: {type: string, nullable: true}
                signature: {type: string, nullable: true}
      responses:
        '200':
          description: Branding option updated successfully
          content:
            application/json:
              schema: {}
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /roles:
    get:
      tags:
        - Roles
      summary: List roles
      operationId: listRoles
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /roles/limited-backoffice-permission-templates:
    get:
      tags:
        - Roles
      summary: List limited backoffice permission templates
      operationId: listLimitedBackofficeRolePermissionTemplates
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    key: {type: string}
                    label: {type: string}
                    description: {type: string}
                    permissions:
                      type: array
                      items: {type: string}

  /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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema: {}

  /modules/washcertificates:
    get:
      tags:
        - Modules
      summary: List wash certificates
      operationId: listWashCertificates
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /modules/xlvask/services/usage/orders:
    get:
      tags:
        - Modules
      summary: Get XLVask usage orders
      operationId: getXlvaskUsageOrders
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /modules/xlvask/services/usage/orders/fast-link:
    get:
      tags:
        - Modules
      summary: Get XLVask usage orders fast link
      operationId: getXlvaskUsageOrdersFastLink
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /superuser/department:
    get:
      tags:
        - Departments
      summary: List departments (superuser)
      operationId: listSuperuserDepartments
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}

  /superuser/departments/{id}/overview:
    get:
      tags:
        - Departments
      summary: Get superuser department overview
      description: Returns the selected department metadata and operational overview metrics for a superuser without requiring scoped department access.
      operationId: getSuperuserDepartmentOverview
      parameters:
        - name: id
          in: path
          required: true
          schema: {type: integer}
        - name: date
          in: query
          required: true
          schema: {type: string}
        - name: date_to
          in: query
          required: false
          schema: {type: string}
      responses:
        '200':
          description: Superuser department overview loaded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuperuserDepartmentOverviewResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /superuser/department/branding:
    put:
      tags:
        - Departments
      summary: Set department branding
      description: Assign an existing branding option to a department, or clear the department branding by sending a null branding_id.
      operationId: setDepartmentBranding
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [department_id, branding_id]
              properties:
                department_id: {type: integer}
                branding_id: {type: integer, nullable: true}
      responses:
        '200':
          description: Department branding updated successfully
          content:
            application/json:
              schema: {}
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /superuser/department/customer-pricing:
    get:
      tags:
        - Departments
      summary: Get department-specific customer pricing
      description: Returns customer price overrides and the department product catalog when custom-only pricing is enabled for the department.
      operationId: getSuperuserDepartmentCustomerPricing
      parameters:
        - name: department_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
        - name: user_id
          in: query
          required: false
          description: Internal user id. Provide either user_id or customer_number.
          schema:
            type: integer
            minimum: 1
        - name: customer_number
          in: query
          required: false
          description: Customer number. Provide either user_id or customer_number.
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Department customer pricing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentCustomerPricingResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
    put:
      tags:
        - Departments
      summary: Replace department-specific customer pricing
      description: Replaces the complete override set for one customer in one custom-only department.
      operationId: setSuperuserDepartmentCustomerPricing
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentCustomerPricingUpdateRequest'
      responses:
        '200':
          description: Department customer pricing updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentCustomerPricingResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /limited-backoffice/departments/{departmentId}/customer-pricing:
    get:
      tags:
        - Limited Backoffice
      summary: Get limited-backoffice department customer pricing
      description: Returns department-specific customer pricing for an assigned custom-only department.
      operationId: getLimitedBackofficeDepartmentCustomerPricing
      parameters:
        - name: departmentId
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
        - name: user_id
          in: query
          required: false
          description: Internal user id. Provide either user_id or customer_number.
          schema:
            type: integer
            minimum: 1
        - name: customer_number
          in: query
          required: false
          description: Customer number. Provide either user_id or customer_number.
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Department customer pricing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentCustomerPricingResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
    put:
      tags:
        - Limited Backoffice
      summary: Replace limited-backoffice department customer pricing
      description: Replaces the complete override set for one customer in an assigned custom-only department.
      operationId: setLimitedBackofficeDepartmentCustomerPricing
      parameters:
        - name: departmentId
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentCustomerPricingLimitedUpdateRequest'
      responses:
        '200':
          description: Department customer pricing updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentCustomerPricingResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /departments/daily-reports:
    get:
      tags:
        - Departments
      summary: List daily reports
      operationId: listDailyReports
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}
    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
          content:
            application/json:
              schema: {}

  /departments/daily-reports/overview:
    get:
      tags:
        - Departments
      summary: Get daily report overview
      operationId: getDailyReportOverview
      parameters:
        - name: date
          in: query
          required: true
          schema: {type: string}
        - name: date_to
          in: query
          required: false
          schema: {type: string}
        - name: department_ids
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentDailyReportOverviewResponse'

  /departments/daily-reports/product-targets:
    put:
      tags:
        - Departments
      summary: Set daily report product target
      description: Requires set_department_daily_report_product_targets and department_access_:department_id. Send a null target_percentage to clear the target.
      operationId: setDailyReportProductTarget
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentDailyReportProductTargetRequest'
      responses:
        '200':
          description: Daily report product target updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentDailyReportProductTargetResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /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
          content:
            application/json:
              schema: {}

  /departments/daily-reports/complaints:
    get:
      tags:
        - Departments
      summary: List or fetch daily report customer complaints
      operationId: listDailyReportComplaints
      parameters:
        - name: id
          in: query
          required: false
          schema: {type: integer}
        - name: page
          in: query
          required: false
          schema: {type: integer}
        - name: limit
          in: query
          required: false
          schema: {type: integer}
        - name: search
          in: query
          required: false
          schema: {type: string}
        - name: filters
          in: query
          required: false
          schema: {type: string}
        - name: order
          in: query
          required: false
          schema: {type: string}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentDailyReportComplaintCollectionResponse'
    post:
      tags:
        - Departments
      summary: Create daily report customer complaint
      operationId: createDailyReportComplaint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentDailyReportComplaintCreateRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentDailyReportComplaintResponse'
    put:
      tags:
        - Departments
      summary: Update daily report customer complaint
      operationId: updateDailyReportComplaint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DepartmentDailyReportComplaintUpdateRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentDailyReportComplaintResponse'
    delete:
      tags:
        - Departments
      summary: Delete daily report customer complaint
      operationId: deleteDailyReportComplaint
      parameters:
        - name: id
          in: query
          required: true
          schema: {type: integer}
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentDailyReportComplaintDeleteResponse'

  /departments/daily-reports/complaints/customers:
    get:
      tags:
        - Departments
      summary: Search selectable customers for daily report complaints
      operationId: searchDailyReportComplaintCustomers
      parameters:
        - name: search
          in: query
          required: true
          schema:
            type: string
            minLength: 2
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 10
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentDailyReportComplaintCustomerSearchResponse'

  /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
          content:
            application/json:
              schema: {}

  /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
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentDailyReportTransactionCountResponse'

  /departments/daily-reports/outside-hours-trend:
    get:
      tags:
        - Departments
      summary: Get outside-hours trend for daily reports
      operationId: getDailyReportOutsideHoursTrend
      parameters:
        - name: date
          in: query
          required: true
          schema: {type: string}
        - name: date_to
          in: query
          required: true
          schema: {type: string}
        - name: department_ids
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendResponse'

  /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
          content:
            application/json:
              schema: {}

  # 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
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    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
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /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
          content:
            application/json:
              schema: {}
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - Security
      summary: Delete a passkey
      operationId: deletePasskey
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Passkey deleted
          content:
            application/json:
              schema: {}
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /superuser/releases/operations:
    get:
      tags:
        - Release Manager
      summary: List release operation runs
      operationId: listReleaseOperations
      parameters:
        - in: query
          name: channel_id
          schema:
            type: integer
        - in: query
          name: operation_type
          schema:
            type: string
        - in: query
          name: status
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 200
      responses:
        '200':
          description: Release operation runs
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /superuser/releases/operations/{id}:
    get:
      tags:
        - Release Manager
      summary: Get release operation details
      operationId: getReleaseOperation
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Release operation details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /superuser/releases/test-runs:
    post:
      tags:
        - Release Manager
      summary: Run Release Manager diagnostics
      operationId: runReleaseTest
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '202':
          description: Release test operation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'

  /superuser/releases/channels/{id}/sync:
    post:
      tags:
        - Release Manager
      summary: Sync latest branch commits into a release channel
      operationId: syncReleaseChannel
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '202':
          description: Channel sync operation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'

  /superuser/releases/issues/actions:
    post:
      tags:
        - Release Manager
      summary: Run a Release Manager issue action
      operationId: runReleaseIssueAction
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                issue_key:
                  type: string
                action_id:
                  type: string
                inputs:
                  type: object
                  additionalProperties: true
                confirm:
                  type: boolean
              additionalProperties: true
      responses:
        '200':
          description: Issue action result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

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
    IdParam:
      name: id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
    SuperuserReplicationKindParam:
      name: kind
      in: path
      required: true
      schema:
        type: string
        enum: [databases, redis, minio]
    SuperuserReplicationHostIdParam:
      name: id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1

  responses:
    BadRequest:
      description: Bad request - Invalid input parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: Conflict - Request could not be completed due to current resource state
      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'
    Gone:
      description: Gone - This operation has been retired
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServiceUnavailable:
      description: Service unavailable - Required async queue dependencies are unavailable
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequests:
      description: Too many requests - Rate limit or resend cooldown exceeded
      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

    GenericSuccessResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          additionalProperties: true
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    CustomerProductImpactAttribute:
      type: string
      enum:
        - restrictAdditionalServices
        - restrictTankCleaning
        - restrictSpotFree
        - restrictInteriorCleaning
        - onlyTankCleaning

    CustomerRuleProductCollection:
      type: object
      required: [id, name, sort_order, product_ids]
      properties:
        id: { type: integer }
        name: { type: string, minLength: 1, maxLength: 191 }
        sort_order: { type: integer }
        product_ids:
          type: array
          uniqueItems: true
          items: { type: integer }

    CustomerRuleProductRestriction:
      type: object
      required: [attribute, version, collections, disabled_product_ids]
      properties:
        attribute:
          $ref: '#/components/schemas/CustomerProductImpactAttribute'
        version: { type: integer, minimum: 1 }
        collections:
          type: array
          items:
            $ref: '#/components/schemas/CustomerRuleProductCollection'
        disabled_product_ids:
          type: array
          uniqueItems: true
          items: { type: integer }

    CustomerRuleProductRestrictionUpdateRequest:
      type: object
      required: [version, collections]
      properties:
        version: { type: integer, minimum: 1 }
        collections:
          type: array
          items:
            $ref: '#/components/schemas/CustomerRuleProductCollectionInput'

    CustomerRuleProductCollectionInput:
      type: object
      required: [name, sort_order, product_ids]
      properties:
        id: { type: integer, nullable: true }
        name: { type: string, minLength: 1, maxLength: 191 }
        sort_order: { type: integer }
        product_ids:
          type: array
          uniqueItems: true
          items: { type: integer }

    CustomerRuleProduct:
      type: object
      required: [id, name, category_id, category_name, active]
      properties:
        id: { type: integer }
        name: { type: string }
        category_id: { type: integer }
        category_name: { type: string }
        active: { type: boolean }

    CustomerRuleProductRestrictionListResponse:
      type: object
      required: [success, data]
      properties:
        success: { type: boolean }
        data:
          type: object
          required: [rules, products]
          properties:
            rules:
              type: array
              items:
                $ref: '#/components/schemas/CustomerRuleProductRestriction'
            products:
              type: array
              items:
                $ref: '#/components/schemas/CustomerRuleProduct'

    CustomerRuleProductRestrictionResponse:
      type: object
      required: [success, data]
      properties:
        success: { type: boolean }
        data:
          $ref: '#/components/schemas/CustomerRuleProductRestriction'

    CustomerAttribute:
      type: object
      required: [id, user_id, attribute, product_restriction]
      properties:
        id: { type: integer }
        user_id: { type: integer }
        attribute: { type: string }
        created_at: { type: string, nullable: true }
        product_restriction:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/CustomerRuleProductRestriction'

    CustomerAttributesResponse:
      type: object
      required: [success, data]
      properties:
        success: { type: boolean }
        data:
          type: array
          items:
            $ref: '#/components/schemas/CustomerAttribute'

    CustomerAttributeMutationRequest:
      type: object
      required: [attribute]
      properties:
        user_id: { type: integer }
        customer_number: { type: integer }
        attribute: { type: string }
      anyOf:
        - required: [user_id]
        - required: [customer_number]

    CustomerRuleProductRestrictedResponse:
      type: object
      required: [success, data]
      properties:
        success: { type: boolean, enum: [false] }
        data:
          type: object
          required: [code, message, product_id, rules, collections]
          properties:
            code: { type: string, enum: [CUSTOMER_RULE_PRODUCT_RESTRICTED] }
            message: { type: string }
            product_id: { type: integer }
            rules:
              type: array
              items: { $ref: '#/components/schemas/CustomerProductImpactAttribute' }
            collections:
              type: array
              items: { type: integer }

    SuperuserSecuritySummaryResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            settings:
              $ref: '#/components/schemas/SuperuserSecuritySettingsPayload'
            firewall:
              type: object
              properties:
                active_rules: { type: integer }
                block_rules: { type: integer }
                watch_rules: { type: integer }
            incidents:
              type: object
              properties:
                open: { type: integer }
                acknowledged: { type: integer }
                resolved: { type: integer }
                recent:
                  type: array
                  items:
                    $ref: '#/components/schemas/SuperuserSecurityIncident'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserSecuritySettingsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SuperuserSecuritySettingsPayload'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserSecuritySettingsPayload:
      type: object
      properties:
        mode:
          type: string
          enum: [observe]
        rules:
          type: array
          items:
            $ref: '#/components/schemas/SuperuserSecurityPolicyRule'
        available_rule_keys:
          type: array
          items:
            type: string
        exemption_permission:
          type: string

    SuperuserSecuritySettingsUpdateRequest:
      type: object
      properties:
        rules:
          type: array
          items:
            type: object
            properties:
              rule_key:
                type: string
                enum: [failed_login_attempts, bookings_created, vehicles_created, requests_per_ip, requests_per_customer]
              enabled:
                type: boolean
              threshold_count:
                type: integer
                minimum: 1
              window_seconds:
                type: integer
                minimum: 30
              exempt_permission_nodes:
                type: array
                items:
                  type: string
            required: [rule_key, enabled, threshold_count, window_seconds]
      required: [rules]

    SuperuserSecurityPolicyRule:
      type: object
      properties:
        id:
          type: integer
        rule_key:
          type: string
        label:
          type: string
        description:
          type: string
        enabled:
          type: boolean
        threshold_count:
          type: integer
        window_seconds:
          type: integer
        mode:
          type: string
          enum: [observe]
        subject_type:
          type: string
        exempt_permission_nodes:
          type: array
          items:
            type: string

    SuperuserSecurityFirewallRuleMutation:
      type: object
      properties:
        action:
          type: string
          enum: [allow, watch, block]
        target_type:
          type: string
          enum: [ip, cidr, customer, user, route]
        target_value:
          type: string
        route_pattern:
          type: string
          nullable: true
        priority:
          type: integer
          minimum: 1
          maximum: 10000
        reason:
          type: string
          nullable: true
        enabled:
          type: boolean
        expires_at:
          type: string
          nullable: true
      required: [action, target_type, target_value]

    SuperuserSecurityFirewallRule:
      allOf:
        - $ref: '#/components/schemas/SuperuserSecurityFirewallRuleMutation'
        - type: object
          properties:
            id:
              type: integer
            metadata:
              type: object
              additionalProperties: true
            created_by:
              type: integer
              nullable: true
            created_at:
              type: string
            updated_at:
              type: string
              nullable: true

    SuperuserSecurityFirewallRuleResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SuperuserSecurityFirewallRule'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserSecurityFirewallRulesResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            rules:
              type: array
              items:
                $ref: '#/components/schemas/SuperuserSecurityFirewallRule'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserSecurityIncident:
      type: object
      properties:
        id:
          type: integer
        incident_key:
          type: string
        type:
          type: string
        severity:
          type: string
          enum: [low, medium, high, critical]
        status:
          type: string
          enum: [open, acknowledged, resolved, false_positive]
        title:
          type: string
        source_ip:
          type: string
          nullable: true
        customer_number:
          type: integer
          nullable: true
        user_id:
          type: integer
          nullable: true
        subuser_id:
          type: integer
          nullable: true
        route_path:
          type: string
          nullable: true
        route_template:
          type: string
          nullable: true
        occurrence_count:
          type: integer
        metadata:
          type: object
          additionalProperties: true
        first_seen_at:
          type: string
        last_seen_at:
          type: string
        notes:
          type: array
          items:
            $ref: '#/components/schemas/SuperuserSecurityIncidentNote'

    SuperuserSecurityIncidentNote:
      type: object
      properties:
        id:
          type: integer
        incident_id:
          type: integer
        note:
          type: string
        created_by:
          type: integer
          nullable: true
        created_at:
          type: string

    SuperuserSecurityIncidentResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SuperuserSecurityIncident'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserSecurityIncidentsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            incidents:
              type: array
              items:
                $ref: '#/components/schemas/SuperuserSecurityIncident'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    DepartmentCustomerPricingUpdateRequest:
      type: object
      required:
        - department_id
        - overrides
      properties:
        department_id:
          type: integer
          minimum: 1
        user_id:
          type: integer
          minimum: 1
        customer_number:
          type: integer
          minimum: 1
        overrides:
          type: array
          items:
            $ref: '#/components/schemas/DepartmentCustomerPricingOverrideInput'

    DepartmentCustomerPricingLimitedUpdateRequest:
      type: object
      required:
        - overrides
      properties:
        user_id:
          type: integer
          minimum: 1
        customer_number:
          type: integer
          minimum: 1
        overrides:
          type: array
          items:
            $ref: '#/components/schemas/DepartmentCustomerPricingOverrideInput'

    DepartmentCustomerPricingOverrideInput:
      type: object
      required:
        - is_category
        - product_or_category_id
      properties:
        is_category:
          type: boolean
        product_or_category_id:
          oneOf:
            - type: integer
            - type: string
          description: Product id, category id, or `global` for the customer-wide department discount.
        discount:
          type: integer
          minimum: 0
          maximum: 100
        percentage:
          type: integer
          minimum: 0
          maximum: 100
        fixed_price:
          type: integer
          nullable: true
          minimum: 0

    DepartmentCustomerPricingOverride:
      allOf:
        - $ref: '#/components/schemas/DepartmentCustomerPricingOverrideInput'
        - type: object
          properties:
            id:
              type: integer
            department_id:
              type: integer
            user_id:
              type: integer
            percentage:
              type: integer
              minimum: 0
              maximum: 100
            created_at:
              type: string
            updated_at:
              type: string

    DepartmentCustomerPricingProduct:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
        category:
          type: integer
        apply_category_discount:
          type: boolean
        base_price:
          type: integer
        department_price:
          type: integer
          nullable: true
        effective_price:
          type: integer
        missing_department_price:
          type: boolean

    DepartmentCustomerPricingCategory:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
        products:
          type: array
          items:
            $ref: '#/components/schemas/DepartmentCustomerPricingProduct'

    DepartmentCustomerPricingResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            department:
              type: object
              properties:
                id:
                  type: integer
                name:
                  type: string
                description:
                  type: string
                custom_pricing_only:
                  type: boolean
            customer:
              type: object
              properties:
                id:
                  type: integer
                customer_number:
                  type: integer
                display_name:
                  type: string
            overrides:
              type: array
              items:
                $ref: '#/components/schemas/DepartmentCustomerPricingOverride'
            categories:
              type: array
              items:
                $ref: '#/components/schemas/DepartmentCustomerPricingCategory'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    ErrorReportSubmissionRequest:
      type: object
      required:
        - before_error
        - expected
        - actual
        - data_collection_accepted
      properties:
        before_error:
          type: string
          maxLength: 4000
          description: What the user was doing before the error occurred
        expected:
          type: string
          maxLength: 4000
          description: What the user expected would happen
        actual:
          type: string
          maxLength: 4000
          description: What actually happened
        data_collection_accepted:
          type: boolean
          description: Required acceptance of collecting diagnostic error data and a screenshot when one can be attached
        screenshot:
          type: string
          nullable: true
          description: Optional PNG, JPEG, or WebP data URI of the current app viewport. Reports are accepted without an attachment when capture or upload fails.
        route_path:
          type: string
          nullable: true
        page_url:
          type: string
          nullable: true
        release_trace_id:
          type: string
          nullable: true
        request_errors:
          type: array
          items:
            type: object
            additionalProperties: true
        vue_errors:
          type: array
          items:
            type: object
            additionalProperties: true
        context:
          type: object
          additionalProperties: true

    ErrorReportStatusUpdateRequest:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum: [open, resolved]
        resolution_note:
          type: string
          nullable: true
          maxLength: 2000

    ErrorReportResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/ErrorReport'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    ErrorReportListResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/ErrorReport'
            counts:
              type: object
              properties:
                open:
                  type: integer
                resolved:
                  type: integer
                all:
                  type: integer
            limit:
              type: integer
            offset:
              type: integer
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    ErrorReport:
      type: object
      properties:
        id:
          type: integer
        status:
          type: string
          enum: [open, resolved]
        reporter:
          type: object
          additionalProperties: true
        route_path:
          type: string
          nullable: true
        page_url:
          type: string
          nullable: true
        release_trace_id:
          type: string
          nullable: true
        frontend_version:
          type: string
          nullable: true
        api_version:
          type: string
          nullable: true
        screenshot:
          type: object
          nullable: true
          additionalProperties: true
        answers:
          type: object
          properties:
            before_error:
              type: string
            expected:
              type: string
            actual:
              type: string
        request_error_count:
          type: integer
        vue_error_count:
          type: integer
        request_errors:
          type: array
          items:
            type: object
            additionalProperties: true
        vue_errors:
          type: array
          items:
            type: object
            additionalProperties: true
        runtime_context:
          type: object
          additionalProperties: true
        resolved_at:
          type: string
          nullable: true
        resolved_by_user_id:
          type: integer
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
          nullable: true

    SuperuserSystemStatusResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SuperuserSystemStatusPayload'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    SuperuserReplicationResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SuperuserReplicationSummary'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserReplicationHostResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SuperuserReplicationHost'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserReplicationOperationResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          additionalProperties: true
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserCoolifyResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            generated_at:
              type: string
              format: date-time
            instances:
              type: array
              items:
                $ref: '#/components/schemas/SuperuserCoolifyInstance'
            targets:
              type: array
              items:
                $ref: '#/components/schemas/SuperuserCoolifyTarget'
            availability:
              type: object
              additionalProperties: true
            load_balancer:
              $ref: '#/components/schemas/SuperuserCoolifyLoadBalancer'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserCoolifyInstanceResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SuperuserCoolifyInstance'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserCoolifyTargetsResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/SuperuserCoolifyTarget'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserCoolifyOperationResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          additionalProperties: true
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserCoolifyLoadBalancerResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SuperuserCoolifyLoadBalancer'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserCoolifyLoadBalancerReconcileResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            ok:
              type: boolean
            dry_run:
              type: boolean
            mutated:
              type: boolean
            config:
              $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerConfig'
            load_balancer:
              $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerPublicState'
            drift:
              type: object
              additionalProperties: true
            planned:
              type: array
              items:
                type: object
                additionalProperties: true
            applied:
              type: array
              items:
                type: object
                additionalProperties: true
            skipped:
              type: array
              items:
                type: object
                additionalProperties: true
            errors:
              type: array
              items:
                type: object
                additionalProperties: true
            gateways:
              type: array
              items:
                $ref: '#/components/schemas/SuperuserCoolifyGateway'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserCoolifyGatewayRouteDeployResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            ok:
              type: boolean
            dry_run:
              type: boolean
            mutated:
              type: boolean
            public_host:
              type: string
            public_url:
              type: string
            planned:
              type: array
              items:
                type: object
                additionalProperties: true
            applied:
              type: array
              items:
                type: object
                additionalProperties: true
            skipped:
              type: array
              items:
                type: object
                additionalProperties: true
            errors:
              type: array
              items:
                type: object
                additionalProperties: true
            warnings:
              type: array
              items:
                type: string
            coverage:
              type: object
              additionalProperties: true
            gateways:
              type: array
              items:
                $ref: '#/components/schemas/SuperuserCoolifyGateway'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserCoolifyGatewaysResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/SuperuserCoolifyGateway'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserCoolifyGatewayResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SuperuserCoolifyGateway'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserCoolifyLoadBalancer:
      type: object
      properties:
        configured:
          type: boolean
        status:
          type: string
          enum: [not_configured, ok, degraded, down]
        config:
          $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerConfig'
        gateways:
          type: array
          items:
            $ref: '#/components/schemas/SuperuserCoolifyGateway'
        load_balancer:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerPublicState'
        drift:
          type: object
          additionalProperties: true
        last_error:
          type: string
          nullable: true

    SuperuserCoolifyLoadBalancerConfig:
      type: object
      properties:
        automation_enabled:
          type: boolean
        automation_mode:
          type: string
          enum: [report_only, enforce]
        load_balancer_id:
          type: string
        public_gateway_host:
          type: string
        token_set:
          type: boolean
        token_source:
          type: string
          nullable: true
        required_services:
          type: array
          items:
            $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerService'

    SuperuserCoolifyLoadBalancerPublicState:
      type: object
      properties:
        id:
          type: integer
          nullable: true
        name:
          type: string
        ipv4:
          type: string
          nullable: true
        ipv6:
          type: string
          nullable: true
        location:
          type: string
          nullable: true
        algorithm:
          type: string
          nullable: true
        targets:
          type: array
          items:
            type: string
        services:
          type: array
          items:
            $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerService'

    SuperuserCoolifyLoadBalancerService:
      type: object
      properties:
        protocol:
          type: string
          enum: [http, tcp]
        listen_port:
          type: integer
        destination_port:
          type: integer
        proxyprotocol:
          type: boolean

    SuperuserCoolifyGateway:
      type: object
      properties:
        id:
          type: integer
        instance_id:
          type: integer
          nullable: true
        hostname:
          type: string
        target_ip:
          type: string
        enabled:
          type: boolean
        priority:
          type: integer
        health_state:
          type: string
        lb_state:
          type: string
        last_probe:
          type: object
          nullable: true
          additionalProperties: true
        last_probed_at:
          type: string
          nullable: true
        last_reconciled_at:
          type: string
          nullable: true
        created_at:
          type: string
          nullable: true
        updated_at:
          type: string
          nullable: true

    SuperuserCoolifyGatewaySaveRequest:
      type: object
      required:
        - hostname
        - target_ip
      properties:
        id:
          type: integer
        instance_id:
          type: integer
          nullable: true
        hostname:
          type: string
        target_ip:
          type: string
        enabled:
          type: boolean
          default: true
        priority:
          type: integer
          default: 100

    SuperuserCoolifyInstance:
      type: object
      properties:
        id:
          type: integer
        label:
          type: string
        base_url:
          type: string
        api_token_set:
          type: boolean
        default_project_uuid:
          type: string
          nullable: true
        default_environment_uuid:
          type: string
          nullable: true
        default_environment_name:
          type: string
          nullable: true
        default_server_uuid:
          type: string
          nullable: true
        default_destination_uuid:
          type: string
          nullable: true
        status:
          type: string
        last_checked_at:
          type: string
          nullable: true
        last_error:
          type: string
          nullable: true

    SuperuserCoolifyPlacementResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            generated_at:
              type: string
            instance:
              $ref: '#/components/schemas/SuperuserCoolifyInstance'
            servers:
              type: array
              items:
                $ref: '#/components/schemas/SuperuserCoolifyPlacementServer'
            projects:
              type: array
              items:
                $ref: '#/components/schemas/SuperuserCoolifyPlacementProject'
            environments:
              type: array
              items:
                $ref: '#/components/schemas/SuperuserCoolifyPlacementEnvironment'
            destination_discovery_supported:
              type: boolean
            errors:
              type: object
              additionalProperties: true

    SuperuserCoolifyPlacementServer:
      type: object
      properties:
        id:
          type: integer
          nullable: true
        uuid:
          type: string
        name:
          type: string
        description:
          type: string
        ip:
          type: string
        user:
          type: string
        port:
          type: integer
          nullable: true
        proxy_type:
          type: string
        swarm_cluster:
          type: string
        is_reachable:
          type: boolean
          nullable: true
        is_usable:
          type: boolean
          nullable: true

    SuperuserCoolifyPlacementProject:
      type: object
      properties:
        id:
          type: integer
          nullable: true
        uuid:
          type: string
        name:
          type: string
        description:
          type: string

    SuperuserCoolifyPlacementEnvironment:
      type: object
      properties:
        id:
          type: integer
          nullable: true
        uuid:
          type: string
        name:
          type: string
        description:
          type: string
        project_id:
          type: integer
          nullable: true
        project_uuid:
          type: string
        project_name:
          type: string

    SuperuserCoolifyTarget:
      type: object
      properties:
        id:
          type: integer
        instance_id:
          type: integer
        instance_label:
          type: string
        kind:
          type: string
          enum: [database, redis, minio]
        label:
          type: string
        role:
          type: string
          enum: [replica]
        server_uuid:
          type: string
          nullable: true
        project_uuid:
          type: string
          nullable: true
        environment_uuid:
          type: string
          nullable: true
        environment_name:
          type: string
          nullable: true
        destination_uuid:
          type: string
          nullable: true
        resource_uuid:
          type: string
          nullable: true
        resource_type:
          type: string
        resource_name:
          type: string
          nullable: true
        deployment_status:
          type: string
        availability_state:
          type: string
          enum: [protected, degraded, failover_ready, failover_blocked, failing_over, destructive_action_required]
        last_reconcile_status:
          type: string
          nullable: true
        replication:
          type: object
          additionalProperties: true

    SuperuserCoolifyInstanceCreateRequest:
      type: object
      required:
        - label
        - base_url
        - api_token
      properties:
        label:
          type: string
        base_url:
          type: string
        api_token:
          type: string
          format: password

    SuperuserCoolifyTargetCreateRequest:
      allOf:
        - $ref: '#/components/schemas/SuperuserReplicationHostCreateRequest'
        - type: object
          required:
            - kind
            - host
          properties:
            instance_id:
              type: integer
            kind:
              type: string
              enum: [database, redis, minio]
            role:
              type: string
              enum: [replica]
              default: replica
            server_uuid:
              type: string
            project_uuid:
              type: string
            environment_uuid:
              type: string
            environment_name:
              type: string
            destination_uuid:
              type: string
            deploy:
              type: boolean
              default: false

    SuperuserReplicationComposeTemplateResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SuperuserReplicationComposeTemplate'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    SuperuserReplicationComposeTemplate:
      type: object
      properties:
        kind:
          type: string
          enum: [database, redis, minio]
        engine:
          type: string
          enum: [mariadb, redis, minio]
        role:
          type: string
          enum: [primary, replica]
        service_name:
          type: string
        host_port:
          type: integer
        console_port:
          type: integer
          nullable: true
        server_id:
          type: integer
          nullable: true
        compose:
          type: string
          description: Complete docker-compose.yml content with secret environment placeholders.
        env:
          type: string
          description: Example .env content for the placeholders used by compose.
        seed_command:
          type: string
          description: One-time MariaDB seed command to initialize a replica from the primary before provisioning.
        credentials:
          $ref: '#/components/schemas/SuperuserReplicationGeneratedCredentials'
        steps:
          type: array
          items:
            type: string

    SuperuserReplicationGeneratedCredentials:
      type: object
      properties:
        label:
          type: string
        host:
          type: string
        port:
          type: integer
        endpoint:
          type: string
        scheme:
          type: string
          enum: [http, https]
        buckets:
          type: array
          items:
            type: string
        console_port:
          type: integer
        replication_transfer_limit:
          type: string
          nullable: true
        space_headroom_percent:
          type: number
          format: float
        database:
          oneOf:
            - type: string
            - type: integer
        username:
          type: string
        password:
          type: string
          format: password
        admin_username:
          type: string
        admin_password:
          type: string
          format: password
        replication_username:
          type: string
        replication_password:
          type: string
          format: password
        ssl_mode:
          type: string
        allow_preseeded_replica:
          type: boolean

    SuperuserReplicationSummary:
      type: object
      properties:
        generated_at:
          type: string
          format: date-time
        database:
          $ref: '#/components/schemas/SuperuserReplicationKindSummary'
        redis:
          $ref: '#/components/schemas/SuperuserReplicationKindSummary'
        minio:
          $ref: '#/components/schemas/SuperuserReplicationKindSummary'
        write_freeze:
          type: object
          additionalProperties: true

    SuperuserReplicationKindSummary:
      type: object
      properties:
        primary:
          $ref: '#/components/schemas/SuperuserReplicationHost'
          nullable: true
        hosts:
          type: array
          items:
            $ref: '#/components/schemas/SuperuserReplicationHost'
        replication:
          $ref: '#/components/schemas/SuperuserReplicationStatus'

    SuperuserReplicationStatus:
      type: object
      properties:
        status:
          type: string
          enum: [ok, degraded, down, not_configured]
        min_percent:
          type: number
          format: float
        average_percent:
          type: number
          format: float
        replicas:
          type: array
          items:
            $ref: '#/components/schemas/SuperuserReplicationHost'
        blockers:
          type: array
          items:
            type: string

    SuperuserReplicationHost:
      type: object
      properties:
        id:
          type: integer
        kind:
          type: string
          enum: [database, redis, minio]
        label:
          type: string
        host:
          type: string
        port:
          type: integer
        database:
          oneOf:
            - type: string
            - type: integer
          nullable: true
        endpoint:
          type: string
          nullable: true
        scheme:
          type: string
          enum: [http, https]
          nullable: true
        buckets:
          type: array
          items:
            type: string
        console_port:
          type: integer
          nullable: true
        replication_transfer_limit:
          type: string
          nullable: true
          description: MinIO replication and seed bandwidth cap passed to mc --limit-upload/--limit-download, for example 25Mi. Use 0 to disable.
        space_headroom_percent:
          type: number
          format: float
          nullable: true
        role:
          type: string
          enum: [primary, replica, inactive]
        status:
          type: string
        replication_source_id:
          type: integer
          nullable: true
        replication_percent:
          type: number
          format: float
        last_status:
          type: object
          additionalProperties: true
        credential_summary:
          type: object
          additionalProperties: true
        deployment_provider:
          type: string
          enum: [manual, coolify]
        coolify:
          type: object
          nullable: true
          additionalProperties: true
        availability_state:
          type: string
          nullable: true
          enum: [protected, degraded, failover_ready, failover_blocked, failing_over, destructive_action_required]

    SuperuserReplicationHostCreateRequest:
      type: object
      required:
        - host
        - port
      properties:
        label:
          type: string
        host:
          type: string
          description: Hostname or MinIO endpoint. MinIO hosts may include http(s) scheme; the backend stores the host without scheme.
        endpoint:
          type: string
          description: Optional MinIO endpoint alias for host.
        port:
          type: integer
        database:
          oneOf:
            - type: string
            - type: integer
        username:
          type: string
          description: Database/Redis username or MinIO access key.
        password:
          type: string
          format: password
          description: Database/Redis password or MinIO secret key.
        scheme:
          type: string
          enum: [http, https]
          description: MinIO endpoint scheme.
        buckets:
          type: array
          items:
            type: string
          description: MinIO buckets to replicate.
        console_port:
          type: integer
          description: Optional MinIO console port for UI display.
        replication_transfer_limit:
          type: string
          description: Optional MinIO replication and seed bandwidth cap. Defaults to 25Mi. Use 0 to disable.
        space_headroom_percent:
          type: number
          format: float
          description: MinIO free-space headroom required before provisioning. Defaults to 20.
        admin_username:
          type: string
        admin_password:
          type: string
          format: password
        replication_username:
          type: string
        replication_password:
          type: string
          format: password
        ssl_mode:
          type: string
        deployment_provider:
          type: string
          enum: [manual, coolify]
        options:
          type: object
          properties:
            allow_preseeded_replica:
              type: boolean
              description: Allow configuring replication when the replica has already been safely seeded outside the orchestrator. Required for MariaDB, which does not support MySQL Clone.
            scheme:
              type: string
              enum: [http, https]
            buckets:
              type: array
              items:
                type: string
            console_port:
              type: integer
            replication_transfer_limit:
              type: string
            space_headroom_percent:
              type: number
              format: float
          additionalProperties: true

    SuperuserReplicationHostRenameRequest:
      type: object
      required:
        - label
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 128

    SuperuserReplicationUnsavedCredentialTestRequest:
      allOf:
        - $ref: '#/components/schemas/SuperuserReplicationHostCreateRequest'
        - type: object
          required:
            - kind
          properties:
            kind:
              type: string
              enum: [database, databases, mysql, redis, minio, s3, object-storage, object_storage]
            role:
              type: string
              enum: [primary, replica]

    SuperuserReplicationComposeTemplateRequest:
      type: object
      properties:
        kind:
          type: string
          enum: [database, databases, mysql, redis, minio, s3, object-storage, object_storage]
          default: database
        role:
          type: string
          enum: [primary, replica]
          default: replica
        service_name:
          type: string
        volume_name:
          type: string
        image:
          type: string
        database:
          type: string
          description: MariaDB database name to create on first startup.
        username:
          type: string
          description: MariaDB application username to create on first startup.
        password:
          type: string
          format: password
          description: Optional application password to reuse instead of generating one.
        admin_password:
          type: string
          format: password
          description: Optional MariaDB root password to reuse instead of generating one.
        replication_username:
          type: string
          description: Replication username to place in generated credentials.
        replication_password:
          type: string
          format: password
          description: Optional replication password to reuse instead of generating one.
        host_port:
          type: integer
          minimum: 1
          maximum: 65535
        server_id:
          type: integer
          minimum: 1
          description: MariaDB server-id. Must be unique across the primary and replicas.
        primary_host:
          type: string
          description: Redis primary host used when generating a Redis replica template.
        primary_port:
          type: integer
          minimum: 1
          maximum: 65535
          description: Redis primary port used when generating a Redis replica template.
        primary_password:
          type: string
          format: password
          description: Redis primary password used in the generated Redis replica .env file. If omitted, the .env keeps the value blank for manual entry.
        primary_username:
          type: string
          description: Optional Redis primary ACL username used in the generated Redis replica .env file. Leave blank or default for the default Redis user.
        buckets:
          type: array
          items:
            type: string
          description: MinIO buckets to create, version, and replicate.
        console_port:
          type: integer
          minimum: 1
          maximum: 65535
          description: MinIO console port exposed by the generated compose service.
        replication_transfer_limit:
          type: string
          description: MinIO replication and seed bandwidth cap included in generated credentials. Defaults to 25Mi. Use 0 to disable.

    SuperuserSystemStatusPayload:
      type: object
      properties:
        overall_status:
          $ref: '#/components/schemas/SuperuserSystemStatusEnum'
        generated_at:
          type: string
          format: date-time
        refresh_after_seconds:
          type: integer
        runtime:
          type: object
          properties:
            cpu:
              $ref: '#/components/schemas/SuperuserRuntimeMetric'
            memory:
              $ref: '#/components/schemas/SuperuserRuntimeMetric'
            disk:
              $ref: '#/components/schemas/SuperuserRuntimeMetric'
        dependencies:
          type: object
          properties:
            database:
              $ref: '#/components/schemas/SuperuserDependencyStatus'
            redis:
              $ref: '#/components/schemas/SuperuserDependencyStatus'
            minio:
              $ref: '#/components/schemas/SuperuserMinioDependencyStatus'
        modules:
          type: array
          items:
            $ref: '#/components/schemas/SuperuserModuleStatus'
        sessions:
          $ref: '#/components/schemas/SuperuserSessionStatus'
        warnings:
          type: array
          items:
            type: string
      required:
        - overall_status
        - generated_at
        - refresh_after_seconds
        - runtime
        - dependencies
        - modules
        - sessions
        - warnings

    SuperuserSystemStatusEnum:
      type: string
      enum: [ok, degraded, down]

    SuperuserModuleStatusEnum:
      type: string
      enum: [disabled, not_configured, configured, ok, degraded, down]

    ModuleUsageStatusEnum:
      type: string
      enum: [ok, near_limit, exhausted, unlimited, unknown, disabled]

    ModuleUsageMetric:
      type: object
      properties:
        module_key:
          type: string
        module_label:
          type: string
        metric_key:
          type: string
        metric_label:
          type: string
        unit:
          type: string
        period:
          type: string
          enum: [day, month, provider, all_time]
        scope_type:
          type: string
        scope_id:
          type: string
        source:
          type: string
          enum: [internal_counter, provider_snapshot, derived]
        primary:
          type: boolean
        writable_limit:
          type: boolean
        limit_source:
          type: string
          nullable: true
          enum: [module_config, provider]
        config_module:
          type: string
          nullable: true
        config_variable:
          type: string
          nullable: true
        used:
          type: number
          format: float
          nullable: true
        limit:
          type: number
          format: float
          nullable: true
        remaining:
          type: number
          format: float
          nullable: true
        usage_percent:
          type: number
          format: float
          nullable: true
        status:
          $ref: '#/components/schemas/ModuleUsageStatusEnum'
        enabled:
          type: boolean
        enforce_mode:
          type: string
          enum: [observe, block]
        soft_limit_percent:
          type: number
          format: float
        window:
          type: object
          properties:
            start:
              type: string
              format: date-time
              nullable: true
            end:
              type: string
              format: date-time
              nullable: true
            timezone:
              type: string
        updated_at:
          type: string
          format: date-time
          nullable: true
        history_available:
          type: boolean
        version:
          type: string
          nullable: true
        usage_available:
          type: boolean
          description: False when a provider responded but quota fields could not be read.
        unavailable_reason:
          type: string
          nullable: true
          enum: [unreadable_payload, missing_limit, missing_usage, invalid_limit]
        detected_keys:
          type: array
          items:
            type: string
      required:
        - module_key
        - metric_key
        - unit
        - period
        - source
        - status
        - enforce_mode

    ModuleUsageSummary:
      type: object
      properties:
        generated_at:
          type: string
          format: date-time
        filters:
          type: object
          additionalProperties: true
        modules:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
              label:
                type: string
              status:
                $ref: '#/components/schemas/ModuleUsageStatusEnum'
              metrics:
                type: array
                items:
                  $ref: '#/components/schemas/ModuleUsageMetric'
        metrics:
          type: array
          items:
            $ref: '#/components/schemas/ModuleUsageMetric'
      required:
        - generated_at
        - modules
        - metrics

    ModuleUsageDetail:
      type: object
      properties:
        generated_at:
          type: string
          format: date-time
        module_key:
          type: string
        metrics:
          type: array
          items:
            $ref: '#/components/schemas/ModuleUsageMetric'
        history:
          type: array
          items:
            type: object
            additionalProperties: true

    ModuleQuotaUpdateRequest:
      type: object
      properties:
        enabled:
          type: boolean
        enforce_mode:
          type: string
          enum: [observe, block]
        soft_limit_percent:
          type: number
          format: float
          minimum: 1
          maximum: 100
        limit:
          type: integer
          minimum: 0

    SuperuserModuleUsage:
      type: object
      properties:
        provider:
          type: string
        metric_key:
          type: string
          nullable: true
        unit:
          type: string
          nullable: true
        period:
          type: string
          nullable: true
        calls_used:
          type: number
          format: float
          minimum: 0
          nullable: true
        quota_calls:
          type: number
          format: float
          minimum: 0
          nullable: true
        calls_remaining:
          type: number
          format: float
          minimum: 0
          nullable: true
        usage_percent:
          type: number
          format: float
          minimum: 0
          nullable: true
        status:
          type: string
          nullable: true
        source:
          type: string
          nullable: true
        enforce_mode:
          type: string
          nullable: true
        version:
          type: string
          nullable: true
        usage_available:
          type: boolean
          nullable: true
        unavailable_reason:
          type: string
          nullable: true
          enum: [unreadable_payload, missing_limit, missing_usage, invalid_limit]
        detected_keys:
          type: array
          items:
            type: string
      required:
        - provider

    SuperuserRuntimeMetric:
      type: object
      properties:
        status:
          $ref: '#/components/schemas/SuperuserSystemStatusEnum'
        usage_percent:
          type: number
          format: float
          nullable: true
        used_bytes:
          type: integer
          nullable: true
        free_bytes:
          type: integer
          nullable: true
        total_bytes:
          type: integer
          nullable: true
        path:
          type: string
          nullable: true
        source:
          type: string
          nullable: true
        checked_at:
          type: string
          format: date-time

    SuperuserDependencyStatus:
      type: object
      properties:
        status:
          $ref: '#/components/schemas/SuperuserSystemStatusEnum'
        latency_ms:
          type: number
          format: float
          nullable: true
        database:
          oneOf:
            - type: integer
            - type: string
          nullable: true
        server_version:
          type: string
          nullable: true
        http_status:
          type: integer
          nullable: true
        checked_at:
          type: string
          format: date-time
        error:
          type: string
          nullable: true
        replication:
          $ref: '#/components/schemas/SuperuserReplicationStatus'

    SuperuserMinioDependencyStatus:
      type: object
      properties:
        status:
          $ref: '#/components/schemas/SuperuserSystemStatusEnum'
        latency_ms:
          type: number
          format: float
          nullable: true
        endpoint:
          type: string
          nullable: true
        http_status:
          type: integer
          nullable: true
        buckets:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              status:
                type: string
              error:
                type: string
                nullable: true
        checked_at:
          type: string
          format: date-time
        error:
          type: string
          nullable: true
        replication:
          $ref: '#/components/schemas/SuperuserReplicationStatus'

    SuperuserModuleStatus:
      type: object
      properties:
        key:
          type: string
        enabled:
          type: boolean
        configured:
          type: boolean
        probe_supported:
          type: boolean
        status:
          $ref: '#/components/schemas/SuperuserModuleStatusEnum'
        status_reason:
          type: string
          nullable: true
        status_reason_key:
          type: string
          nullable: true
        status_reason_params:
          type: object
          additionalProperties: true
        usage:
          $ref: '#/components/schemas/SuperuserModuleUsage'
        usage_metrics:
          type: array
          items:
            $ref: '#/components/schemas/ModuleUsageMetric'
        checked_at:
          type: string
          format: date-time
      required:
        - key
        - enabled
        - configured
        - probe_supported
        - status
        - checked_at

    SuperuserSessionStatus:
      type: object
      properties:
        active_window_minutes:
          type: integer
        active_users:
          type: integer
        active_sessions:
          type: integer
        recent_sessions:
          type: array
          items:
            type: object
            properties:
              session_kind:
                type: string
              principal_id:
                type: integer
              display_name:
                type: string
              context_label:
                type: string
                nullable: true
              customer_number_context:
                type: integer
                nullable: true
              device_type:
                type: string
              user_agent:
                type: string
              last_route:
                type: string
              first_seen_at:
                type: string
                format: date-time
                nullable: true
              last_seen_at:
                type: string
                format: date-time
                nullable: true
              active:
                type: boolean
      required:
        - active_window_minutes
        - active_users
        - active_sessions
        - recent_sessions

    SystemSearchEntityType:
      type: string
      enum:
        - objects
        - module_config
        - orders
        - order_items
        - customers
        - employees
        - users
        - subusers
        - customer_discounts
        - customer_fixed_prices
        - departments
        - permissions
        - roles
        - invoices
        - vehicles
        - bookings
        - bookings_new
        - branding
        - categories
        - currency_conversion_rates
        - customer_codes
        - customer_default_department
        - customer_notes
        - customer_vehicles_addons
        - department_categories
        - department_daily_reports
        - department_gates
        - department_goals
        - department_lanes
        - department_notification_sms
        - department_relays
        - department_selfserve_condition_rules
        - department_selfserve_conditions
        - department_selfserve_questions
        - department_selfserve_tasks
        - department_selfserve_vehicle_conditions
        - department_time_bookings_entries
        - department_time_bookings_opening_hours
        - department_time_bookings_types
        - department_variables
        - fxratesapi_conversion_rates
        - module_action_logs
        - motorapi_lookups
        - notifications
        - order_bookings
        - plate_scanners
        - plate_scans
        - product_options
        - products
        - stripe_module_customers
        - stripe_module_orders
        - stripe_payment_intents
        - subuser_grants
        - xlvask_customers
        - xlvask_potential_order_matches
        - xlvask_usage_log_wash_items
        - xlvask_usage_logs
        - xlvask_vehicle_types
        - xlvask_vehicles

    SystemSearchRequest:
      type: object
      required:
        - query
      properties:
        query:
          type: string
          description: Free-text query to search for. Customer lookups include local e-conomic index fields and deterministic lexical synonym expansion (for example `rabat` -> `discount`).
        include_types:
          type: array
          items:
            $ref: '#/components/schemas/SystemSearchEntityType'
          description: Limit search to these entity types.
        exclude_types:
          type: array
          items:
            $ref: '#/components/schemas/SystemSearchEntityType'
          description: Exclude these entity types from search.
        include_associations:
          type: boolean
          default: true
          description: Include associated records for matched core entities.
        max_results:
          type: integer
          minimum: 1
          maximum: 50
          default: 50
          description: Maximum number of relevance-ranked results to return. Values above 50 are capped to 50.

    SystemSearchResult:
      type: object
      properties:
        entity_type:
          $ref: '#/components/schemas/SystemSearchEntityType'
        entity_id:
          type: string
        title:
          type: string
        description:
          type: string
        customer_number:
          type: integer
          nullable: true
        department_id:
          type: integer
          nullable: true
        score:
          type: integer
        association_reason:
          type: string
          nullable: true
        payload:
          type: object
          additionalProperties: true
      required:
        - entity_type
        - entity_id
        - title
        - score

    SystemSearchMeta:
      type: object
      properties:
        query:
          type: string
        max_results:
          type: integer
        returned:
          type: integer
        truncated:
          type: boolean
        allowed_types:
          type: array
          items:
            $ref: '#/components/schemas/SystemSearchEntityType'
        cache:
          type: object
          properties:
            hit:
              type: boolean
          required: [hit]
      required:
        - query
        - max_results
        - returned
        - truncated
        - allowed_types
        - cache

    SystemSearchPayload:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/SystemSearchResult'
        grouped_results:
          type: object
          additionalProperties:
            type: array
            items:
              $ref: '#/components/schemas/SystemSearchResult'
        meta:
          $ref: '#/components/schemas/SystemSearchMeta'
      required:
        - results
        - grouped_results
        - meta

    SystemSearchResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/SystemSearchPayload'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    SystemSearchCacheClearResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            message:
              type: string
            query_cache_cleared:
              type: boolean
            intent_cache_cleared:
              type: boolean
          required:
            - message
            - query_cache_cleared
            - intent_cache_cleared
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    SystemSearchCacheRebuildRequest:
      type: object
      properties:
        scope:
          type: string
          enum: [all, types, dirty]
          default: all
        types:
          type: array
          items:
            $ref: '#/components/schemas/SystemSearchEntityType'

    SystemSearchCacheRebuildResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            message:
              type: string
            request:
              type: object
              properties:
                scope:
                  type: string
                  enum: [all, types, dirty]
                types:
                  type: array
                  items:
                    $ref: '#/components/schemas/SystemSearchEntityType'
                requested_at:
                  type: integer
              required:
                - scope
                - types
                - requested_at
            query_cache_cleared:
              type: boolean
            intent_cache_cleared:
              type: boolean
          required:
            - message
            - request
            - query_cache_cleared
            - intent_cache_cleared
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    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, invoiceDiscountLayoutNumber, paymentTermsNumber, transactionDraftCustomerNumber, defaultDepartmentId] }
        type: { type: string, enum: [string, int] }
        value:
          oneOf:
            - type: string
            - type: integer
          nullable: true
      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]

    SlackConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [Slack] }
        variable: { type: string, enum: [customer_registration_webhook_url, internal_department_goal_progress_webhook_url, internal_department_ids] }
        type: { type: string, enum: [string] }
        value:
          oneOf:
            - type: string
              example: https://hooks.slack.com/services/...
            - type: string
              example: '[1,2,3]'
      required: [module, variable, type, value]

    SlackConfigTestResult:
      type: object
      properties:
        configured: { type: boolean }
        sent: { type: boolean }
        message: { type: string }
      required: [configured, sent, message]

    SlackInternalDepartmentGoalProgressDepartment:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        order_priority: { type: integer }
      required: [id, name, order_priority]

    SlackInternalDepartmentGoalProgressConfig:
      type: object
      properties:
        internal_department_goal_progress_webhook_url:
          type: string
          example: https://hooks.slack.com/services/...
        internal_department_ids:
          type: array
          items: { type: integer }
          example: [1, 2, 3]
        departments:
          type: array
          items:
            $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressDepartment'
      required: [internal_department_goal_progress_webhook_url, internal_department_ids, departments]

    SlackInternalDepartmentGoalProgressConfigUpdate:
      type: object
      properties:
        internal_department_goal_progress_webhook_url:
          type: string
          example: https://hooks.slack.com/services/...
        internal_department_ids:
          type: array
          items: { type: integer }
          example: [1, 2, 3]
      required: [internal_department_goal_progress_webhook_url, internal_department_ids]

    BackupsConfigEntry:
      type: object
      properties:
        module: { type: string, enum: [Backups] }
        variable:
          type: string
          enum:
            - enabled
            - retention_recent_hours
            - retention_daily_days
            - retention_weekly_weeks
            - retention_monthly_months
            - app_data_enabled
            - verification_required
            - restore_enabled
        type: { type: string, enum: [bool, int] }
        value:
          oneOf:
            - type: boolean
            - type: integer
      required: [module, variable, type, value]

    BackupComponent:
      type: object
      properties:
        id: { type: integer }
        backup_uuid: { type: string }
        component_type: { type: string, enum: [database, object_bucket] }
        logical_name: { type: string }
        source_bucket: { type: string, nullable: true }
        storage_key: { type: string, nullable: true }
        manifest_key: { type: string, nullable: true }
        object_count: { type: integer }
        byte_size: { type: integer }
        content_sha256: { type: string, nullable: true }
        encrypted_sha256: { type: string, nullable: true }
        encryption_key_id: { type: string, nullable: true }
        status: { type: string, enum: [pending, stored, skipped, failed] }
        error_message: { type: string, nullable: true }

    BackupRecord:
      type: object
      properties:
        backup_uuid: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        source: { type: string, enum: [manual, scheduled, cron, pre_restore] }
        status: { type: string, enum: [queued, creating, created_unverified, available, failed, pruned, legacy_unverified] }
        schema_version: { type: integer }
        storage_bucket: { type: string }
        storage_prefix: { type: string }
        manifest_key: { type: string, nullable: true }
        manifest_sha256: { type: string, nullable: true }
        encryption_key_id: { type: string, nullable: true }
        component_count: { type: integer }
        object_count: { type: integer }
        total_bytes: { type: integer }
        requested_by_user_id: { type: integer, nullable: true }
        started_at: { type: string, nullable: true }
        completed_at: { type: string, nullable: true }
        verified_at: { type: string, nullable: true }
        expires_at: { type: string, nullable: true }
        last_error: { type: string, nullable: true }
        components:
          type: array
          items: { $ref: '#/components/schemas/BackupComponent' }

    BackupListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data:
              type: object
              properties:
                items:
                  type: array
                  items: { $ref: '#/components/schemas/BackupRecord' }
                legacy_items:
                  type: array
                  items: { type: object }
                total: { type: integer }
                limit: { type: integer }
                offset: { type: integer }
                has_more: { type: boolean }

    BackupJob:
      type: object
      properties:
        id: { type: integer }
        job_type: { type: string, enum: [create, verify, restore_preview, restore, prune] }
        backup_uuid: { type: string, nullable: true }
        status: { type: string, enum: [queued, running, succeeded, created_unverified, failed] }
        progress_percent: { type: integer }
        progress_message: { type: string, nullable: true }
        payload: { type: object }
        result: { type: object }
        actor_user_id: { type: integer, nullable: true }
        started_at: { type: string, nullable: true }
        completed_at: { type: string, nullable: true }
        error_message: { type: string, nullable: true }

    BackupJobQueued:
      type: object
      properties:
        job_id: { type: integer }
        backup_uuid: { type: string, nullable: true }
        status: { type: string }

    BackupJobQueuedResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { $ref: '#/components/schemas/BackupJobQueued' }

    BackupJobResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { $ref: '#/components/schemas/BackupJob' }

    BackupRestoreRequest:
      type: object
      properties:
        preview_id: { type: integer }
        confirmation_phrase: { type: string, example: RESTORE abc123 TO PRODUCTION }
        reason: { type: string }
      required: [preview_id, confirmation_phrase, reason]

    BackupRestorePreview:
      type: object
      properties:
        preview_id: { type: integer }
        backup_uuid: { type: string }
        backup_name: { type: string }
        target_environment: { type: string, enum: [production] }
        expires_at: { type: string }
        confirmation_phrase: { type: string }
        checks:
          type: array
          items:
            type: object
            properties:
              key: { type: string }
              status: { type: string, enum: [ok, failed] }
              message: { type: string }
        destructive_impact: { type: object }

    BackupRestorePreviewResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { $ref: '#/components/schemas/BackupRestorePreview' }

    BackupRestoreAuditEntry:
      type: object
      properties:
        id: { type: integer }
        restore_job_id: { type: integer, nullable: true }
        preview_job_id: { type: integer, nullable: true }
        backup_uuid: { type: string }
        actor_user_id: { type: integer, nullable: true }
        target_environment: { type: string }
        reason: { type: string, nullable: true }
        pre_restore_backup_uuid: { type: string, nullable: true }
        status: { type: string }
        started_at: { type: string, nullable: true }
        completed_at: { type: string, nullable: true }
        error_message: { type: string, nullable: true }

    BackupRestoreAuditListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data:
              type: object
              properties:
                items:
                  type: array
                  items: { $ref: '#/components/schemas/BackupRestoreAuditEntry' }

    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]


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

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

    WorkfeedConfigApiUrlEntry:
      type: object
      properties:
        module: { type: string, enum: [workfeed] }
        variable: { type: string, enum: [api_url] }
        type: { type: string, enum: [string] }
        value: { type: string, example: "https://europe-west1-production-eu-327a3.cloudfunctions.net/api" }
      required: [module, variable, type, value]

    WorkfeedConfigApiKeyEntry:
      type: object
      properties:
        module: { type: string, enum: [workfeed] }
        variable: { type: string, enum: [api_key] }
        type: { type: string, enum: [string] }
        value: { type: string, example: "wf_live_xxxxxxxxxxxxxxxxx" }
      required: [module, variable, type, value]

    WorkfeedConfigCompanyIdEntry:
      type: object
      properties:
        module: { type: string, enum: [workfeed] }
        variable: { type: string, enum: [CompanyID] }
        type: { type: string, enum: [string] }
        value: { type: string, example: "123456" }
      required: [module, variable, type, value]

    WorkfeedConfigEntry:
      oneOf:
        - $ref: '#/components/schemas/WorkfeedConfigEnabledEntry'
        - $ref: '#/components/schemas/WorkfeedConfigApiUrlEntry'
        - $ref: '#/components/schemas/WorkfeedConfigApiKeyEntry'
        - $ref: '#/components/schemas/WorkfeedConfigCompanyIdEntry'
      discriminator:
        propertyName: variable
        mapping:
          enabled: '#/components/schemas/WorkfeedConfigEnabledEntry'
          api_url: '#/components/schemas/WorkfeedConfigApiUrlEntry'
          api_key: '#/components/schemas/WorkfeedConfigApiKeyEntry'
          CompanyID: '#/components/schemas/WorkfeedConfigCompanyIdEntry'

    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, machine_wash_minutes_included] }
        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]

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

    SlackConfigTestResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { $ref: '#/components/schemas/SlackConfigTestResult' }
          required: [data]

    SlackInternalDepartmentGoalProgressConfigResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfig' }
          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]


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

    WorkfeedConfigListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data: { type: array, items: { $ref: '#/components/schemas/WorkfeedConfigEntry' } }
          required: [data]
      example:
        success: true
        data:
          - module: workfeed
            variable: enabled
            type: bool
            value: true
          - module: workfeed
            variable: api_url
            type: string
            value: https://europe-west1-production-eu-327a3.cloudfunctions.net/api
          - module: workfeed
            variable: api_key
            type: string
            value: wf_live_xxxxxxxxxxxxxxxxx
          - module: workfeed
            variable: CompanyID
            type: string
            value: "123456"
        meta: []
        includes: []

    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]


    DepartmentWeatherStatus:
      type: string
      description: Productivity health for the slot. `unknown` is returned when the slot has not started yet, has no employee-hours, or one or more selected departments are missing department weather targets.
      enum: [unknown, healthy, degraded, unhealthy]

    DepartmentWeatherCondition:
      type: string
      enum: [clear, mostly_clear, partly_cloudy, mostly_cloudy, overcast, rain, showers, thunderstorm, snow, fog]

    DepartmentWeatherTarget:
      type: object
      properties:
        department_id:
          type: integer
          minimum: 1
        degraded_threshold:
          type: number
          nullable: true
          minimum: 0
          description: Minimum washes per hour ratio required for `degraded`. `null` when target is not configured.
        healthy_threshold:
          type: number
          nullable: true
          minimum: 0
          description: Minimum washes per hour ratio required for `healthy`. `null` when target is not configured.
        configured:
          type: boolean
          description: Whether both weather status thresholds are configured and valid for the department.
      required: [department_id, degraded_threshold, healthy_threshold, configured]

    DepartmentWeatherTargetUpsertRequest:
      type: object
      required: [department_id, degraded_threshold, healthy_threshold]
      properties:
        department_id:
          type: integer
          minimum: 1
        degraded_threshold:
          type: number
          minimum: 0
        healthy_threshold:
          type: number
          minimum: 0
          description: Must be greater than or equal to `degraded_threshold`.

    DepartmentWeatherTimelineEntry:
      type: object
      properties:
        date:
          type: string
          format: date
          description: Calendar date for the hourly slot (`YYYY-MM-DD`).
          example: '2026-03-24'
        time:
          type: string
          description: Hour label for the slot in 24-hour format (`HH:00`).
          example: '01:00'
        current:
          type: boolean
          description: True when this slot matches the current server hour.
          example: false
        weather:
          $ref: '#/components/schemas/DepartmentWeatherCondition'
        washes:
          type: integer
          minimum: 0
          example: 0
        hours:
          type: number
          format: float
          minimum: 0
          example: 2.5
          description: Sum of Workfeed employee-hours in the department for this exact hour slot
        status:
          $ref: '#/components/schemas/DepartmentWeatherStatus'
      required: [date, time, current, weather, washes, hours, status]

    WeatherApiObjectResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data:
              type: object
              additionalProperties: true
          required: [data]

    WorkfeedDepartment:
      type: object
      properties:
        id: { type: string, example: "PKHaOSgFA4uguOqmLfWv" }
        name: { type: string, example: "API Testing Account 😎" }
        isDeleted: { type: boolean, example: false }
        createTime: { type: string, format: date-time, example: "2023-10-25T09:43:06.650Z" }
        updateTime: { type: string, format: date-time, example: "2023-10-25T09:43:06.650Z" }
      additionalProperties: true

    WorkfeedEmployee:
      type: object
      properties:
        id: { type: string, example: "J9QAIiTG0nRC1OsC5YvHfLWdDHn1" }
        firstname: { type: string, example: "API 2" }
        lastname: { type: string, example: "Test 2" }
        email: { type: string, nullable: true, example: "test2@example.com" }
        phone: { type: string, nullable: true, example: "12345678" }
        roleIDs:
          type: array
          items: { type: string }
          example: ["hLbEKIPTlMh3ehotXl0w"]
        departmentIDs:
          type: array
          items: { type: string }
          example: ["PKHaOSgFA4uguOqmLfWv"]
        primaryDepartmentID: { type: string, nullable: true }
        street: { type: string, nullable: true, example: "" }
        city: { type: string, nullable: true, example: "" }
        zip: { type: string, nullable: true, example: "" }
        accessLevel: { type: string, nullable: true, example: "employee" }
        wage: { type: number, nullable: true }
        minHours: { type: number, nullable: true }
        maxHours: { type: number, nullable: true }
        isDeleted: { type: boolean, example: false }
        ssn: { type: string, nullable: true, example: "" }
        imageURL: { type: string, nullable: true, format: uri }
        createTime: { type: string, format: date-time, example: "2023-10-28T18:22:45.695Z" }
        updateTime: { type: string, format: date-time, example: "2023-10-28T18:34:37.191Z" }
      additionalProperties: true

    WorkfeedShiftComment:
      type: object
      properties:
        message: { type: string, nullable: true, example: "Comments! 😍" }
        creatorID: { type: string, nullable: true, example: "API" }
        createdOn: { type: string, format: date-time, nullable: true, example: "2023-10-27T09:43:36.542Z" }
      additionalProperties: true

    WorkfeedShiftCustomBreak:
      type: object
      properties:
        creatorID: { type: string, nullable: true, example: "automatic" }
        duration: { type: number, nullable: true, example: 1 }
        createdOn: { type: string, format: date-time, nullable: true, example: "2023-10-27T09:10:30.336Z" }
      additionalProperties: true

    WorkfeedShiftApproval:
      type: object
      properties:
        approver: { type: string, nullable: true, example: "automatic" }
        date: { type: string, format: date-time, nullable: true, example: "2023-12-27T09:10:30.336Z" }
        originalStart: { type: string, format: date-time, nullable: true, example: "2023-12-07T09:10:30.336Z" }
        originalEnd: { type: string, format: date-time, nullable: true, example: "2023-12-08T09:10:30.336Z" }
      additionalProperties: true

    WorkfeedShift:
      type: object
      properties:
        id: { type: string, example: "Trcu7MKFomu5y5zv1B8G" }
        start: { type: string, format: date-time, example: "2023-10-23T08:00:00.000Z" }
        end: { type: string, format: date-time, example: "2023-10-23T16:00:00.000Z" }
        employeeID: { type: string, nullable: true }
        roleID: { type: string, nullable: true, example: "hLbEKIPTlMh3ehotXl0w" }
        departmentID: { type: string, nullable: true, example: "PKHaOSgFA4uguOqmLfWv" }
        released: { type: boolean, example: false }
        isForSale: { type: boolean, nullable: true, example: false }
        comment:
          allOf:
            - $ref: '#/components/schemas/WorkfeedShiftComment'
          nullable: true
        customBreak:
          allOf:
            - $ref: '#/components/schemas/WorkfeedShiftCustomBreak'
          nullable: true
        overlappingLeaveID: { type: string, nullable: true }
        approval:
          allOf:
            - $ref: '#/components/schemas/WorkfeedShiftApproval'
          nullable: true
        grossPay: { type: number, nullable: true }
        tagIDs:
          type: array
          items: { type: string }
        createTime: { type: string, format: date-time, example: "2023-10-27T09:10:30.336Z" }
        updateTime: { type: string, format: date-time, example: "2023-10-27T09:10:30.422Z" }
      additionalProperties: true

    WorkfeedEmployeeListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/WorkfeedEmployee'
          required: [data]

    WorkfeedEmployeeSingleResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data:
              $ref: '#/components/schemas/WorkfeedEmployee'
          required: [data]

    WorkfeedShiftListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/WorkfeedShift'
          required: [data]

    WorkfeedShiftSingleResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data:
              $ref: '#/components/schemas/WorkfeedShift'
          required: [data]

    WorkfeedDepartmentListResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/WorkfeedDepartment'
          required: [data]

    DepartmentWeatherTimelineResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data:
              type: array
              description: Hourly contiguous slots from start of range (`00:00`) to end of range (`23:00`, inclusive). Defaults to yesterday+today (48 entries) when date_from/date_to are not provided.
              items:
                $ref: '#/components/schemas/DepartmentWeatherTimelineEntry'
          required: [data]

    DepartmentWeatherTargetsResponse:
      allOf:
        - $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/DepartmentWeatherTarget'
          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
        limited_backoffice_managed:
          type: boolean
          description: Whether the user is managed through limited backoffice and has locked raw role/customer edits
        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
        assigned_vehicle_id:
          type: integer
          nullable: true
        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

    SuperuserSubuserAccount:
      type: object
      properties:
        id: { type: integer }
        username: { type: string, nullable: true }
        name: { type: string, nullable: true }
        email: { type: string, format: email, nullable: true }
        email_verified: { type: boolean }
        email_verified_at: { type: string, format: date-time, nullable: true }
        phone_country_code: { type: integer, nullable: true }
        phone: { type: integer, nullable: true }
        phone_verified: { type: boolean }
        phone_verified_at: { type: string, format: date-time, nullable: true }
        setup_required: { type: boolean }
        updated_at: { type: string, format: date-time, nullable: true }
        verification_state:
          type: string
          enum: [verified, partial, unverified, missing_email, missing_phone, missing_contacts]
        verification:
          $ref: '#/components/schemas/SubuserContactVerificationStatus'

    SuperuserSubuserProfileUpdateRequest:
      type: object
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 255
        username:
          type: string
          nullable: true
          minLength: 3
          maxLength: 50
        email:
          type: string
          format: email
          nullable: true
          maxLength: 255
        phone_country_code:
          type: integer
          minimum: 1
          maximum: 999
        phone:
          type: integer
          minimum: 1000
          maximum: 999999999999999

    SuperuserSubuserPasswordRequest:
      type: object
      required:
        - password
      properties:
        password:
          type: string
          minLength: 8
          maxLength: 255
          description: Must include at least one uppercase letter, one lowercase letter, and one number.

    SuperuserSubuserLoginLinkRequest:
      type: object
      properties:
        customer_number:
          type: integer
          minimum: 1
        grant_id:
          type: integer
          minimum: 1

    SuperuserSubuserLoginLinkResponse:
      type: object
      properties:
        subuser_id:
          type: integer
        customer_number:
          type: integer
        login_path:
          type: string
          example: /login/qr?token=...&type=subuser&customer_number=12345678
        login_url:
          type: string
          format: uri
          example: https://truckwash.io/login/qr?token=...&type=subuser&customer_number=12345678

    SuperuserSubuserContactLinkSendResponse:
      type: object
      properties:
        subuser_id:
          type: integer
        customer_number:
          type: integer
        login_path:
          type: string
        login_url:
          type: string
          format: uri
        delivery:
          $ref: '#/components/schemas/SubuserContactVerificationDelivery'
        subuser:
          $ref: '#/components/schemas/SuperuserSubuserAccount'

    SubuserContactVerificationChannelStatus:
      type: object
      properties:
        channel:
          type: string
          enum: [email, phone]
        value:
          type: string
          nullable: true
        masked_value:
          type: string
          nullable: true
        available:
          type: boolean
        verified:
          type: boolean
        verified_at:
          type: string
          format: date-time
          nullable: true
        country_code:
          type: integer
          nullable: true
          description: Present for phone verification status.
        phone:
          type: integer
          nullable: true
          description: Present for phone verification status.

    SubuserContactVerificationStatus:
      type: object
      properties:
        state:
          type: string
          enum: [verified, partial, unverified, missing_email, missing_phone, missing_contacts]
        email:
          $ref: '#/components/schemas/SubuserContactVerificationChannelStatus'
        phone:
          $ref: '#/components/schemas/SubuserContactVerificationChannelStatus'

    SubuserContactVerificationDelivery:
      type: object
      properties:
        channel:
          type: string
          enum: [email, phone]
        status:
          type: string
          enum: [sent, unavailable, failed, throttled, missing_destination]
        message:
          type: string
        masked_destination:
          type: string
          nullable: true
        expires_in:
          type: integer
          nullable: true
        retry_after:
          type: integer
          nullable: true

    SubuserContactVerificationSendResponse:
      type: object
      properties:
        delivery:
          $ref: '#/components/schemas/SubuserContactVerificationDelivery'
        verification:
          $ref: '#/components/schemas/SubuserContactVerificationStatus'

    SubuserContactVerificationManagedSendResponse:
      type: object
      properties:
        delivery:
          $ref: '#/components/schemas/SubuserContactVerificationDelivery'
        subuser:
          $ref: '#/components/schemas/SubuserManagementRow'

    SubuserContactVerificationAdminSendResponse:
      type: object
      properties:
        delivery:
          $ref: '#/components/schemas/SubuserContactVerificationDelivery'
        subuser:
          $ref: '#/components/schemas/SuperuserSubuserAccount'

    SuperuserSubuserVerificationStateRequest:
      type: object
      required: [verified]
      properties:
        verified:
          type: boolean

    SuperuserSubuserVerificationStateResponse:
      type: object
      properties:
        result:
          type: object
          properties:
            channel:
              type: string
              enum: [email, phone]
            status:
              type: string
              enum: [verified, unverified]
            message:
              type: string
            verified_at:
              type: string
              format: date-time
              nullable: true
        verification:
          $ref: '#/components/schemas/SubuserContactVerificationStatus'
        subuser:
          $ref: '#/components/schemas/SuperuserSubuserAccount'

    SubuserContactVerificationVerifyRequest:
      type: object
      required: [code]
      properties:
        code:
          type: string
          pattern: '^[0-9]{6}$'

    SubuserContactVerificationVerifyResponse:
      type: object
      properties:
        result:
          type: object
          properties:
            channel:
              type: string
              enum: [email, phone]
            status:
              type: string
              enum: [verified]
            message:
              type: string
            verified_at:
              type: string
              format: date-time
              nullable: true
        verification:
          $ref: '#/components/schemas/SubuserContactVerificationStatus'
        subuser:
          $ref: '#/components/schemas/SubuserSelf'

    SubuserManagementRow:
      type: object
      description: Chauffeur management row. Superuser list endpoints return one row per chauffeur account; grant-specific fields mirror a primary grant for backwards compatibility, and all visible customer grants are listed in `grants`.
      properties:
        id: { type: integer }
        username: { type: string, nullable: true }
        name: { type: string, nullable: true }
        email: { type: string, format: email, nullable: true }
        email_verified: { type: boolean }
        email_verified_at: { type: string, format: date-time, nullable: true }
        phone_country_code: { type: integer, nullable: true }
        phone: { type: integer, nullable: true }
        phone_verified: { type: boolean }
        phone_verified_at: { type: string, format: date-time, 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 }
        setup_required: { type: boolean }
        invite_accepted: { type: boolean }
        can_resend_invite: { type: boolean }
        profile_editable_by_manager: { type: boolean }
        customer_number: { type: integer }
        customer_name: { type: string, nullable: true }
        grant_id: { type: integer, nullable: true }
        grant_enabled: { type: boolean }
        grant_note: { type: string, nullable: true }
        assigned_vehicle_id: { type: integer, nullable: true }
        assigned_vehicle_reg: { type: string, nullable: true }
        assigned_vehicle:
          type: object
          nullable: true
          properties:
            id: { type: integer }
            reg: { type: string, nullable: true }
        dognvask_enabled: { type: boolean }
        grant_permissions:
          type: array
          items: { type: string }
        permissions:
          type: array
          items: { type: string }
        permission_template_key:
          type: string
          enum: [deactivated, driver, booking_coordinator, fleet_admin, custom]
        permission_groups:
          type: array
          items:
            $ref: '#/components/schemas/SubuserPermissionGroup'
        grant_created_at: { type: string, format: date-time, nullable: true }
        grant_updated_at: { type: string, format: date-time, nullable: true }
        grants:
          type: array
          description: Visible customer access grants for this chauffeur, grouped under the single chauffeur row.
          items:
            $ref: '#/components/schemas/SubuserManagementGrant'
        grant_count: { type: integer }
        customer_numbers:
          type: array
          items: { type: integer }
        access_state:
          type: string
          enum: [active, pending_setup, disabled, inactive]
        verification_state:
          type: string
          enum: [verified, partial, unverified, missing_email, missing_phone, missing_contacts]
        verification:
          $ref: '#/components/schemas/SubuserContactVerificationStatus'

    SubuserManagementGrant:
      type: object
      properties:
        grant_id: { type: integer }
        customer_number: { type: integer }
        customer_name: { type: string, nullable: true }
        grant_enabled: { type: boolean }
        grant_note: { type: string, nullable: true }
        assigned_vehicle_id: { type: integer, nullable: true }
        assigned_vehicle_reg: { type: string, nullable: true }
        assigned_vehicle:
          type: object
          nullable: true
          properties:
            id: { type: integer }
            reg: { type: string, nullable: true }
        dognvask_enabled: { type: boolean }
        grant_permissions:
          type: array
          items: { type: string }
        permissions:
          type: array
          items: { type: string }
        permission_template_key:
          type: string
          enum: [deactivated, driver, booking_coordinator, fleet_admin, custom]
        permission_groups:
          type: array
          items:
            $ref: '#/components/schemas/SubuserPermissionGroup'
        grant_created_at: { type: string, format: date-time, nullable: true }
        grant_updated_at: { type: string, format: date-time, nullable: true }
        access_state:
          type: string
          enum: [active, pending_setup, disabled, inactive]

    SubuserManagementSummary:
      type: object
      properties:
        total: { type: integer }
        active: { type: integer }
        pending_setup: { type: integer }
        disabled: { type: integer }

    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
        permission_template_key:
          type: string
          enum: [deactivated, driver, booking_coordinator, fleet_admin]
        assigned_vehicle_id:
          type: integer
          nullable: true

    SubuserGrantUpdateRequest:
      type: object
      properties:
        enabled:
          type: boolean
        note:
          type: string
          nullable: true
          maxLength: 65535
        permissions:
          type: array
          items:
            type: string
        permission_template_key:
          type: string
          enum: [deactivated, driver, booking_coordinator, fleet_admin]
        assigned_vehicle_id:
          type: integer
          nullable: true

    SubuserPermissionGroup:
      type: object
      properties:
        key:
          type: string
        capabilities:
          type: array
          items:
            type: string

    SubuserPermissionTemplate:
      type: object
      properties:
        key:
          type: string
          enum: [deactivated, driver, booking_coordinator, fleet_admin]
        label:
          type: string
        description:
          type: string
        enabled:
          type: boolean
        permissions:
          type: array
          items:
            type: string
        permission_groups:
          type: array
          items:
            $ref: '#/components/schemas/SubuserPermissionGroup'

    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
        email_verified:
          type: boolean
        email_verified_at:
          type: string
          format: date-time
          nullable: true
        phone_country_code:
          type: integer
          nullable: true
        phone:
          type: integer
          nullable: true
        phone_verified:
          type: boolean
        phone_verified_at:
          type: string
          format: date-time
          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
        verification_state:
          type: string
          enum: [verified, partial, unverified, missing_email, missing_phone, missing_contacts]
        verification:
          $ref: '#/components/schemas/SubuserContactVerificationStatus'

    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

    EconomicTransferQueueStatus:
      type: string
      enum:
        - QUEUED
        - PROCESSING
        - COMPLETED
        - FAILED

    EconomicTransferQueueJob:
      type: object
      properties:
        id:
          type: integer
        transfer_type:
          type: string
          enum:
            - ORDER_DRAFT_EXPORT
            - ORDER_INVOICE_EXPORT
            - COLLECTED_INVOICE_EXPORT
        status:
          $ref: '#/components/schemas/EconomicTransferQueueStatus'
        progress_percent:
          type: integer
          minimum: 0
          maximum: 100
        progress_message:
          type: string
          nullable: true
        attempts:
          type: integer
          minimum: 0
        max_attempts:
          type: integer
          minimum: 1
        error_message:
          type: string
          nullable: true
        payload:
          type: object
          nullable: true
          additionalProperties: true
        result:
          type: object
          nullable: true
          additionalProperties: true
        details_summary:
          type: object
          nullable: true
          additionalProperties: true
        created_by:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        next_retry_at:
          type: string
          format: date-time
          nullable: true
      required:
        - id
        - transfer_type
        - status
        - progress_percent
        - attempts
        - max_attempts

    EconomicTransferQueueEnqueueResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            message:
              type: string
            job_id:
              type: integer
              minimum: 1
            job:
              $ref: '#/components/schemas/EconomicTransferQueueJob'
          required:
            - message
            - job_id
            - job
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    EconomicTransferSynchronousFallbackResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            message:
              type: string
            mode:
              type: string
              enum: [synchronous_fallback]
            result:
              type: object
              additionalProperties: true
          required:
            - message
            - mode
            - result
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

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

    EconomicTransferQueueRetryResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            message:
              type: string
            job:
              $ref: '#/components/schemas/EconomicTransferQueueJob'
          required:
            - message
            - job
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    EconomicTransferQueueRunResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            message:
              type: string
            processed:
              type: integer
              minimum: 0
            completed:
              type: integer
              minimum: 0
            failed:
              type: integer
              minimum: 0
            jobs:
              type: array
              items:
                type: integer
                minimum: 1
            limit:
              type: integer
              minimum: 1
              maximum: 10
            transfer_type:
              type: string
              enum:
                - COLLECTED_INVOICE_EXPORT
          required:
            - message
            - processed
            - completed
            - failed
            - jobs
            - limit
            - transfer_type
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    EconomicTransferQueueListResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/EconomicTransferQueueJob'
            count:
              type: integer
              minimum: 0
            total:
              type: integer
              minimum: 0
            limit:
              type: integer
              minimum: 1
            offset:
              type: integer
              minimum: 0
            has_more:
              type: boolean
          required:
            - items
            - count
            - total
            - limit
            - offset
            - has_more
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    EconomicTransferQueueMonitorResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            jobs:
              type: array
              items:
                $ref: '#/components/schemas/EconomicTransferQueueJob'
            counts:
              type: object
              properties:
                queued:
                  type: integer
                  minimum: 0
                in_progress:
                  type: integer
                  minimum: 0
                failed:
                  type: integer
                  minimum: 0
                completed:
                  type: integer
                  minimum: 0
                total:
                  type: integer
                  minimum: 0
              required:
                - queued
                - in_progress
                - failed
                - completed
                - total
            progress_percent:
              type: integer
              minimum: 0
              maximum: 100
            limit:
              type: integer
              minimum: 1
              maximum: 100
          required:
            - jobs
            - counts
            - progress_percent
            - limit
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    EconomicTransferQueueDismissResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            message:
              type: string
            job:
              $ref: '#/components/schemas/EconomicTransferQueueJob'
          required:
            - message
            - job
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    EconomicTransferQueueDismissTerminalResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            message:
              type: string
            dismissed_count:
              type: integer
              minimum: 0
          required:
            - message
            - dismissed_count
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    OrderAttachmentDownloadLinkResponse:
      type: object
      required: [success, data, meta, includes]
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          type: object
          required: [download_link]
          properties:
            download_link:
              type: string
              format: uri
              pattern: '^https://'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    InvoicingPeriodResponseEnvelope:
      type: object
      required: [success, data, meta, includes]
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          $ref: '#/components/schemas/InvoicingPeriodData'
        meta:
          type: object
          additionalProperties: true
        includes:
          type: object
          additionalProperties: true

    InvoicingPeriodData:
      type: object
      required: [dateFrom, dateTo, types]
      properties:
        dateFrom:
          type: string
          format: date
        dateTo:
          type: string
          format: date
        types:
          type: object
          additionalProperties:
            type: array
            items:
              $ref: '#/components/schemas/InvoicingPeriodCustomer'

    InvoicingPeriodCustomer:
      type: object
      required: [customer_number, customer_name, transactions, invoice_collections]
      additionalProperties: true
      properties:
        customer_number:
          type: integer
        customer_name:
          type: string
        transactions:
          type: array
          items:
            $ref: '#/components/schemas/InvoicingPeriodTransaction'
        invoice_collections:
          type: array
          items:
            $ref: '#/components/schemas/InvoicingPeriodInvoiceCollection'

    InvoicingPeriodTransaction:
      type: object
      required: [id, booked, invoice_state]
      additionalProperties: true
      properties:
        id:
          type: integer
        invoice_collection_id:
          type: integer
          nullable: true
        booked:
          type: boolean
        invoice_state:
          type: string
          enum: [open, closed, economic_draft, economic_booked]
        completed_at:
          type: string
          format: date-time
          nullable: true
        amount:
          type: number
          format: float

    InvoicingPeriodInvoiceCollection:
      type: object
      required:
        - id
        - invoice_collection_id
        - customer_number
        - state
        - order_ids
        - order_count
        - total_net_amount
      additionalProperties: true
      properties:
        id:
          type: integer
        invoice_collection_id:
          type: integer
        customer_number:
          type: integer
        name:
          type: string
        external_id:
          type: string
          nullable: true
        booked_invoice_id:
          type: integer
          nullable: true
        state:
          type: string
          enum: [open, closed, economic_draft, economic_booked]
        order_ids:
          type: array
          items:
            type: integer
        order_count:
          type: integer
          minimum: 0
        total_net_amount:
          type: number
          format: float

    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: Selected e-conomic total (draft when available, otherwise booked) minus internal_total
          example: 0
        internal_total:
          type: number
          format: float
          description: Internal total amount for the collected invoice
          example: 694
      required:
        - collected_invoice_id
        - internal_total

    CollectedInvoiceEconomicPdfResponse:
      type: object
      description: Presigned PDF URL for a draft or booked e-conomic invoice attached to a collected invoice
      properties:
        collected_invoice_id:
          type: integer
          example: 123
        type:
          type: string
          enum:
            - draft
            - booked
          example: booked
        economic_invoice_id:
          type: integer
          example: 28368
        url:
          type: string
          format: uri
      required:
        - collected_invoice_id
        - type
        - economic_invoice_id
        - url

    CollectedInvoiceEconomicV2DetailsResponse:
      type: object
      properties:
        collected_invoice_id:
          type: integer
        external_id:
          type: string
        order_ids:
          type: array
          items:
            type: integer
        economic:
          type: object
          required: [draft_id, booked_id, state, available_pdf_type]
          properties:
            draft_id:
              type: integer
              nullable: true
            booked_id:
              type: integer
              nullable: true
            state:
              type: string
              enum: [draft, booked, none]
              description: Current authoritative e-conomic target state
            available_pdf_type:
              type: string
              enum: [draft, booked]
              nullable: true
              description: PDF target that callers should request
        customer:
          $ref: '#/components/schemas/CollectedInvoiceEconomicV2CustomerSummary'
        internal:
          type: object
          required: [normalized]
          properties:
            normalized:
              $ref: '#/components/schemas/EconomicV2NormalizedInvoice'
        draft:
          type: object
          properties:
            exists:
              type: boolean
            raw:
              type: object
              nullable: true
              additionalProperties: true
            normalized:
              allOf:
                - $ref: '#/components/schemas/EconomicV2NormalizedInvoice'
              nullable: true
        booked:
          type: object
          properties:
            exists:
              type: boolean
            raw:
              type: object
              nullable: true
              additionalProperties: true
            normalized:
              allOf:
                - $ref: '#/components/schemas/EconomicV2NormalizedInvoice'
              nullable: true
        warnings:
          type: array
          items:
            type: string
      required:
        - collected_invoice_id
        - order_ids
        - economic
        - customer
        - internal
        - draft
        - booked
        - warnings

    CollectedInvoiceEconomicV2CustomerSummary:
      type: object
      properties:
        internal_customer_number:
          type: integer
          nullable: true
        draft_customer_number:
          type: integer
          nullable: true
        booked_customer_number:
          type: integer
          nullable: true
        exists:
          type: boolean
        name:
          type: string
          nullable: true
        barred:
          type: boolean
          nullable: true
      required:
        - exists

    CollectedInvoiceEconomicV2CompareResponse:
      type: object
      properties:
        collected_invoice_id:
          type: integer
        details:
          $ref: '#/components/schemas/CollectedInvoiceEconomicV2DetailsResponse'
        comparison:
          $ref: '#/components/schemas/EconomicV2Comparison'
        warnings:
          type: array
          items:
            type: string
      required:
        - collected_invoice_id
        - details
        - comparison
        - warnings

    CollectedInvoiceEconomicV2CompareBulkResponse:
      type: object
      properties:
        requested:
          type: integer
        compared:
          type: integer
        failed:
          type: integer
        results:
          type: array
          items:
            $ref: '#/components/schemas/CollectedInvoiceEconomicV2CompareResponse'
        errors:
          type: array
          items:
            type: object
            properties:
              collected_invoice_id:
                type: integer
              error:
                type: string
      required:
        - requested
        - compared
        - failed
        - results
        - errors

    CollectedInvoiceEconomicV2RevenueStatisticsResponse:
      type: object
      properties:
        filters:
          type: object
          properties:
            dateFrom:
              type: string
              format: date
            dateTo:
              type: string
              format: date
            customer_numbers:
              type: array
              items:
                type: integer
            department_numbers:
              type: array
              items:
                type: integer
            currency:
              type: string
              nullable: true
            barred:
              type: string
              enum: [all, barred, active]
            max_pages:
              type: integer
        summary:
          $ref: '#/components/schemas/EconomicV2RevenueSummary'
        customers:
          type: array
          items:
            $ref: '#/components/schemas/EconomicV2RevenueCustomerStat'
        departments:
          type: array
          items:
            $ref: '#/components/schemas/EconomicV2RevenueDepartmentStat'
        currencies:
          type: array
          items:
            $ref: '#/components/schemas/EconomicV2RevenueCurrencyStat'
        warnings:
          type: array
          items:
            type: string
      required:
        - filters
        - summary
        - customers
        - departments
        - currencies
        - warnings

    EconomicV2RevenueSummary:
      type: object
      properties:
        invoice_count:
          type: integer
        line_count:
          type: integer
        unique_customers:
          type: integer
        net_amount:
          type: number
        vat_amount:
          type: number
        gross_amount:
          type: number
        average_invoice_net_amount:
          type: number
      required:
        - invoice_count
        - line_count
        - unique_customers
        - net_amount
        - vat_amount
        - gross_amount
        - average_invoice_net_amount

    EconomicV2RevenueCustomerStat:
      type: object
      properties:
        customer_number:
          type: integer
        customer_name:
          type: string
          nullable: true
        barred:
          type: boolean
          nullable: true
        invoice_count:
          type: integer
        net_amount:
          type: number
        vat_amount:
          type: number
        gross_amount:
          type: number
      required:
        - customer_number
        - invoice_count
        - net_amount
        - vat_amount
        - gross_amount

    EconomicV2RevenueDepartmentStat:
      type: object
      properties:
        department_key:
          type: string
        department_number:
          type: integer
          nullable: true
        invoice_count:
          type: integer
        line_count:
          type: integer
        net_amount:
          type: number
        vat_amount:
          type: number
        gross_amount:
          type: number
      required:
        - department_key
        - invoice_count
        - line_count
        - net_amount
        - vat_amount
        - gross_amount

    EconomicV2RevenueCurrencyStat:
      type: object
      properties:
        currency:
          type: string
        invoice_count:
          type: integer
        net_amount:
          type: number
        vat_amount:
          type: number
        gross_amount:
          type: number
      required:
        - currency
        - invoice_count
        - net_amount
        - vat_amount
        - gross_amount

    EconomicV2NormalizedInvoice:
      type: object
      properties:
        source:
          type: string
          enum: [internal, draft, booked]
        totals:
          type: object
          properties:
            net_total:
              type: number
            line_net_total:
              type: number
            line_count:
              type: integer
            billable_line_count:
              type: integer
            difference_from_line_sum:
              type: number
              nullable: true
        departments:
          $ref: '#/components/schemas/EconomicV2DepartmentDistribution'
        lines:
          type: array
          items:
            $ref: '#/components/schemas/EconomicV2NormalizedLineItem'
        warnings:
          type: array
          items:
            type: string
      required:
        - source
        - totals
        - departments
        - lines
        - warnings

    EconomicV2NormalizedLineItem:
      type: object
      properties:
        index:
          type: integer
        source:
          type: string
        source_order_id:
          type: integer
          nullable: true
        source_line_id:
          type: integer
          nullable: true
        line_type:
          type: string
          enum: [product, discount, text]
        billable:
          type: boolean
        product_number:
          type: string
          nullable: true
        product_id:
          type: integer
          nullable: true
        description:
          type: string
        reference:
          type: string
        quantity:
          type: number
        unit_net_price:
          type: number
        line_net_amount:
          type: number
        department_distribution:
          $ref: '#/components/schemas/EconomicV2DepartmentDistribution'
        match_key:
          type: string
      required:
        - source
        - line_type
        - billable
        - description
        - reference
        - quantity
        - unit_net_price
        - line_net_amount
        - department_distribution
        - match_key

    EconomicV2DepartmentDistribution:
      type: object
      additionalProperties:
        type: number
      example:
        "75": 100

    EconomicV2Comparison:
      type: object
      properties:
        totals:
          type: object
          properties:
            internal_net_total:
              type: number
        targets:
          type: object
          properties:
            draft:
              $ref: '#/components/schemas/EconomicV2TargetComparison'
            booked:
              $ref: '#/components/schemas/EconomicV2TargetComparison'
        warnings:
          type: array
          items:
            type: string
      required:
        - totals
        - targets
        - warnings

    EconomicV2TargetComparison:
      type: object
      properties:
        target:
          type: string
          enum: [draft, booked]
        status:
          type: string
          enum: [exact_match, partial_mismatch, total_mismatch, missing_target]
        overall_match:
          type: boolean
        totals:
          $ref: '#/components/schemas/EconomicV2TotalsComparison'
        lines:
          type: object
          properties:
            summary:
              type: object
              properties:
                internal_billable_count:
                  type: integer
                target_billable_count:
                  type: integer
                mismatch_count:
                  type: integer
            diff:
              type: array
              items:
                $ref: '#/components/schemas/EconomicV2LineDiffEntry'
        departments:
          type: object
          properties:
            matches:
              type: boolean
            diff:
              type: array
              items:
                $ref: '#/components/schemas/EconomicV2DepartmentDiffEntry'
        mismatch_reasons:
          type: array
          items:
            type: string
        warnings:
          type: array
          items:
            type: string
      required:
        - target
        - status
        - overall_match
        - totals
        - lines
        - departments
        - mismatch_reasons
        - warnings

    EconomicV2TotalsComparison:
      type: object
      properties:
        internal_net_total:
          type: number
          nullable: true
        target_net_total:
          type: number
          nullable: true
        difference:
          type: number
          nullable: true
        abs_difference:
          type: number
          nullable: true
        matches:
          type: boolean
      required:
        - matches

    EconomicV2LineDiffEntry:
      type: object
      properties:
        match_key:
          type: string
        reasons:
          type: array
          items:
            type: string
        internal_line:
          allOf:
            - $ref: '#/components/schemas/EconomicV2NormalizedLineItem'
          nullable: true
        target_line:
          allOf:
            - $ref: '#/components/schemas/EconomicV2NormalizedLineItem'
          nullable: true
      required:
        - match_key
        - reasons

    EconomicV2DepartmentDiffEntry:
      type: object
      properties:
        department_key:
          type: string
        internal_amount:
          type: number
        target_amount:
          type: number
        difference:
          type: number
        matches:
          type: boolean
      required:
        - department_key
        - internal_amount
        - target_amount
        - difference
        - matches

    InvoicingDistributionV2Transaction:
      type: object
      properties:
        id:
          type: integer
        date:
          type: string
          format: date-time
        amount:
          type: number
        booked:
          type: boolean
        department_id:
          type: integer
        excluded:
          type: boolean
      required: [id, date, amount, booked, department_id, excluded]

    InvoicingDistributionV2Customer:
      type: object
      properties:
        id:
          type: integer
          nullable: true
        customer_number:
          type: integer
        customer_name:
          type: string
        transactions:
          type: array
          items:
            $ref: '#/components/schemas/InvoicingDistributionV2Transaction'
        requires_action:
          type: boolean
        meta:
          type: object
          additionalProperties: true
      required: [customer_number, customer_name, transactions, requires_action, meta]

    InvoicingDistributionV2CategoryResponse:
      type: object
      properties:
        customers:
          type: array
          items:
            $ref: '#/components/schemas/InvoicingDistributionV2Customer'
        collective_results:
          type: object
          additionalProperties: true
        warnings:
          type: array
          items:
            type: string
      required: [customers, collective_results, warnings]

    InvoicingDistributionV2FixedPricingResponse:
      $ref: '#/components/schemas/InvoicingDistributionV2CategoryResponse'

    InvoicingDistributionV2WashSubscriptionsResponse:
      $ref: '#/components/schemas/InvoicingDistributionV2CategoryResponse'

    InvoicingDistributionV2CustomerPricesResponse:
      $ref: '#/components/schemas/InvoicingDistributionV2CategoryResponse'

    InvoicingDistributionV2BookedDepartment75Group:
      type: object
      properties:
        month:
          type: string
          example: '2026-01'
        source_category:
          type: string
          enum: [fixed_pricing, wash_subscriptions, unclassified]
        invoice_ids:
          type: array
          items:
            type: integer
        booked_net_amount:
          type: number
        department_distribution:
          $ref: '#/components/schemas/EconomicV2DepartmentDistribution'
        undistributed_net_amount:
          type: number
      required:
        - month
        - source_category
        - invoice_ids
        - booked_net_amount
        - department_distribution
        - undistributed_net_amount

    InvoicingDistributionV2BookedDepartment75Meta:
      type: object
      properties:
        booked_net_amount:
          type: number
        distributed_net_amount:
          type: number
        undistributed_net_amount:
          type: number
        department_distribution:
          $ref: '#/components/schemas/EconomicV2DepartmentDistribution'
        booked_groups:
          type: array
          items:
            $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75Group'
      required:
        - booked_net_amount
        - distributed_net_amount
        - undistributed_net_amount
        - department_distribution
        - booked_groups

    InvoicingDistributionV2BookedDepartment75Customer:
      allOf:
        - $ref: '#/components/schemas/InvoicingDistributionV2Customer'
        - type: object
          properties:
            meta:
              type: object
              properties:
                booked_department_75:
                  $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75Meta'
              required:
                - booked_department_75

    InvoicingDistributionV2BookedDepartment75CollectiveResults:
      type: object
      properties:
        booked_net_amount:
          type: number
        distributed_net_amount:
          type: number
        undistributed_net_amount:
          type: number
        department_distribution:
          $ref: '#/components/schemas/EconomicV2DepartmentDistribution'
        department_distribution_parsed:
          type: object
          additionalProperties:
            type: number
      required:
        - booked_net_amount
        - distributed_net_amount
        - undistributed_net_amount
        - department_distribution
        - department_distribution_parsed

    InvoicingDistributionV2BookedDepartment75Response:
      type: object
      properties:
        customers:
          type: array
          items:
            $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75Customer'
        collective_results:
          $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75CollectiveResults'
        warnings:
          type: array
          items:
            type: string
      required: [customers, collective_results, warnings]

    InvoicingDistributionV2AllResponse:
      type: object
      properties:
        fixed_pricing:
          $ref: '#/components/schemas/InvoicingDistributionV2FixedPricingResponse'
        wash_subscriptions:
          $ref: '#/components/schemas/InvoicingDistributionV2WashSubscriptionsResponse'
        customer_prices:
          $ref: '#/components/schemas/InvoicingDistributionV2CustomerPricesResponse'
        booked_department_75:
          $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75Response'
      required: [fixed_pricing, wash_subscriptions, customer_prices, booked_department_75]

    PricingHistoryVersionEntry:
      type: object
      properties:
        id:
          type: integer
        type:
          type: string
          enum: [fixed_pricing, vehicle_subscription, discount_override]
        customer_number:
          type: integer
        effective_from:
          type: string
          format: date-time
        effective_to:
          type: string
          format: date-time
          nullable: true
        source:
          type: string
        confidence:
          type: number
          minimum: 0
          maximum: 1
        inferred:
          type: boolean
        metadata_json:
          oneOf:
            - type: string
            - type: object
              additionalProperties: true
            - type: array
              items: {}
          nullable: true
      required:
        - id
        - type
        - customer_number
        - effective_from
        - source
        - confidence
        - inferred

    CustomerPricingHistoryResponse:
      type: object
      properties:
        customer_number:
          type: integer
        fixed_pricing:
          type: array
          items:
            type: object
            additionalProperties: true
        vehicle_subscriptions:
          type: array
          items:
            type: object
            additionalProperties: true
        discount_overrides:
          type: array
          items:
            type: object
            additionalProperties: true
        timeline:
          type: array
          items:
            $ref: '#/components/schemas/PricingHistoryVersionEntry'
      required:
        - customer_number
        - fixed_pricing
        - vehicle_subscriptions
        - discount_overrides
        - timeline

    InvoicingWashSubscriptionsDistributionResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/InvoicingWashSubscriptionsDistributionCustomer'
        meta:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionMeta'
        includes:
          type: object
          additionalProperties: true
      required:
        - success
        - data
        - meta
        - includes

    InvoicingWashSubscriptionsDistributionCustomer:
      type: object
      properties:
        id:
          type: integer
          nullable: true
        customer_number:
          type: integer
        customer_name:
          type: string
        transactions:
          type: array
          items:
            $ref: '#/components/schemas/InvoicingFixedPricingDistributionTransaction'
        requires_action:
          type: boolean
        meta:
          type: object
          properties:
            subscription:
              type: object
              additionalProperties: true
          required:
            - subscription
      required:
        - customer_number
        - customer_name
        - transactions
        - requires_action
        - meta

    InvoicingFixedPricingDistributionResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/InvoicingFixedPricingDistributionCustomer'
        meta:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionMeta'
        includes:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionIncludes'
      required:
        - success
        - data
        - meta
        - includes

    InvoicingFixedPricingDistributionCustomer:
      type: object
      properties:
        id:
          type: integer
        customer_number:
          type: integer
        customer_name:
          type: string
        transactions:
          type: array
          items:
            $ref: '#/components/schemas/InvoicingFixedPricingDistributionTransaction'
        requires_action:
          type: boolean
        meta:
          type: object
          properties:
            fixed_pricing:
              $ref: '#/components/schemas/InvoicingFixedPricingDistributionFixedPricing'
          required:
            - fixed_pricing
      required:
        - id
        - customer_number
        - customer_name
        - transactions
        - requires_action
        - meta

    InvoicingFixedPricingDistributionTransaction:
      type: object
      properties:
        id:
          type: integer
        date:
          type: string
          description: Datetime in `YYYY-MM-DD HH:mm:ss` format.
          example: '2026-02-02 10:43:41'
        amount:
          type: number
        booked:
          type: boolean
        excluded:
          type: boolean
      required:
        - id
        - date
        - amount
        - booked
        - excluded

    InvoicingFixedPricingDistributionFixedPricing:
      type: object
      properties:
        customer_number:
          type: integer
        price:
          type: number
        description:
          type: string
        original_price:
          type: number
        department_totals:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray'
        department_totals_relative:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray'
      required:
        - customer_number
        - price
        - description
        - original_price
        - department_totals
        - department_totals_relative

    InvoicingFixedPricingDistributionMeta:
      type: object
      properties:
        date_from:
          type: string
          description: Datetime in `YYYY-MM-DD HH:mm:ss` format.
          example: '2026-02-01 00:00:00'
        date_to:
          type: string
          description: Datetime in `YYYY-MM-DD HH:mm:ss` format.
          example: '2026-02-28 23:59:59'
      required:
        - date_from
        - date_to

    InvoicingFixedPricingDistributionIncludes:
      type: object
      properties:
        debug_invoicing_period_customers_with_orders_in_date_range:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime'
        debug_invoicing_period_process_customer_numbers:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime'
        debug_invoicing_period_get_transactions_for_customers_in_date_range:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime'
        debug_invoicing_period_calculate_transaction_totals:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime'
        debug_invoicing_period_construct_customer_objects:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime'
        collective_fixed_pricing_results:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionCollectiveResults'
      additionalProperties: true

    InvoicingFixedPricingDistributionExecutionTime:
      type: object
      properties:
        execution_time:
          type: number
      required:
        - execution_time

    InvoicingFixedPricingDistributionCollectiveResults:
      type: object
      properties:
        total_fixed_price:
          type: number
        total_original_price:
          type: number
        total_department_totals:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray'
        total_department_totals_relative:
          $ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray'
        total_department_totals_parsed:
          type: object
          additionalProperties:
            type: number
        total_department_totals_relative_parsed:
          type: object
          additionalProperties:
            type: number
      required:
        - total_fixed_price
        - total_original_price
        - total_department_totals
        - total_department_totals_relative
        - total_department_totals_parsed
        - total_department_totals_relative_parsed

    InvoicingFixedPricingDistributionNumberMapOrEmptyArray:
      oneOf:
        - type: object
          additionalProperties:
            type: number
        - type: array
          maxItems: 0

    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

    SelfServeLaneMachineRelayStatus:
      type: object
      properties:
        lane_id:
          type: integer
        relay:
          type: string
          enum: [MACHINE, MACHINE_PROGRAM_PICKER, MACHINE_CLEANER]
        relay_id:
          type: string
        online:
          type: boolean
        on:
          type: boolean

    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
        machine_wash_minutes_included:
          type: integer
          description: Included machine wash minutes before minute-based billing starts

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

    SelfserveStudioNode:
      type: object
      required: [id, position, data]
      properties:
        id:
          type: string
        type:
          type: string
          nullable: true
        position:
          type: object
          required: [x, y]
          properties:
            x: { type: number }
            y: { type: number }
        data:
          type: object
          additionalProperties: true
          properties:
            kind:
              type: string
              enum: [question, condition, rule, task, lane, machine_type, vehicle_type, edge_gateway, relay_binding, relay, runtime_checkpoint]
            object_id:
              oneOf:
                - type: integer
                - type: string
              nullable: true
            label:
              type: string
            raw:
              type: object
              additionalProperties: true
    SelfserveStudioEdge:
      type: object
      required: [id, source, target]
      properties:
        id: { type: string }
        source: { type: string }
        target: { type: string }
        type: { type: string, nullable: true }
        label: { type: string, nullable: true }
        data:
          type: object
          additionalProperties: true
    SelfserveStudioLayout:
      type: object
      properties:
        nodes:
          type: object
          additionalProperties:
            type: object
            properties:
              x: { type: number }
              y: { type: number }
        viewport:
          type: object
          additionalProperties: true
        runtime_affecting:
          type: boolean
          enum: [false]
    SelfserveStudioValidation:
      type: object
      properties:
        valid:
          type: boolean
        errors:
          type: array
          items: { type: string }
        warnings:
          type: array
          items: { type: string }
        items:
          type: array
          items:
            type: object
            properties:
              severity:
                type: string
                enum: [error, warning]
              message:
                type: string
        stats:
          type: object
          additionalProperties: true
        validated_at:
          type: string
          format: date-time
    SelfserveConfigVersion:
      type: object
      properties:
        id: { type: integer }
        department_id: { type: integer }
        status:
          type: string
          enum: [DRAFT, PUBLISHED, ARCHIVED]
        version_number: { type: integer }
        config:
          $ref: '#/components/schemas/SelfserveStudioV2Config'
        validation_result:
          $ref: '#/components/schemas/SelfserveStudioValidation'
        source_version_id: { type: integer, nullable: true }
        created_by: { type: integer, nullable: true }
        published_at: { type: string, nullable: true }
        created_at: { type: string, nullable: true }
        updated_at: { type: string, nullable: true }
    SelfserveStudioV2Config:
      type: object
      required: [schema_version, questions, conditions, rules, tasks]
      properties:
        schema_version:
          type: integer
          enum: [2]
        department_id:
          type: integer
        questions:
          type: array
          items:
            type: object
            additionalProperties: true
        conditions:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioV2Condition'
        rules:
          type: array
          description: Empty in schema_version 2; retained only for backward-compatible payload shape.
          maxItems: 0
          items:
            type: object
        tasks:
          type: array
          items:
            type: object
            additionalProperties: true
        v2_meta:
          type: object
          additionalProperties: true
        migration_issues:
          type: array
          items:
            type: object
            additionalProperties: true
    SelfserveStudioV2Condition:
      type: object
      required: [id, expression]
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
          nullable: true
        expression:
          $ref: '#/components/schemas/SelfserveStudioV2Expression'
      additionalProperties: true
    SelfserveStudioV2Expression:
      oneOf:
        - $ref: '#/components/schemas/SelfserveStudioV2ExpressionGroup'
        - $ref: '#/components/schemas/SelfserveStudioV2ExpressionPredicate'
    SelfserveStudioV2ExpressionGroup:
      type: object
      required: [type, operator, children]
      properties:
        type:
          type: string
          enum: [group]
        operator:
          type: string
          enum: [ALL, ANY]
        children:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioV2Expression'
    SelfserveStudioV2ExpressionPredicate:
      type: object
      required: [type, subject_type, subject_id, operator]
      properties:
        type:
          type: string
          enum: [predicate]
        subject_type:
          type: string
          enum: [question, condition]
        subject_id:
          type: integer
        operator:
          type: string
          enum: [IS_TRUE, IS_FALSE, IS_SET, IS_TRUE_OR_NOT_SET, IS_FALSE_OR_NOT_SET]
    SelfserveStudioGraph:
      type: object
      required: [nodes, edges, lookups, validation, layout, versions, simulator_defaults, gateway_workspace, permissions]
      properties:
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioEdge'
        lookups:
          type: object
          additionalProperties: true
        validation:
          $ref: '#/components/schemas/SelfserveStudioValidation'
        layout:
          $ref: '#/components/schemas/SelfserveStudioLayout'
        versions:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveConfigVersion'
        active_config:
          allOf:
            - $ref: '#/components/schemas/SelfserveStudioV2Config'
          nullable: true
        draft:
          type: object
          additionalProperties: true
        simulator_defaults:
          type: object
          additionalProperties: true
        gateway_workspace:
          type: object
          additionalProperties: true
        permissions:
          type: object
          additionalProperties:
            type: boolean
        meta:
          type: object
          additionalProperties: true
    SelfserveStudioGraphOperation:
      type: object
      properties:
        action:
          type: string
          enum: [create, update, delete, connect, disconnect, reorder, upsert, upsert_path]
        entity:
          type: string
          enum: [question, condition, task, action, path]
          description: Standalone rule operations are not accepted for schema_version 2 drafts.
        id:
          type: integer
          nullable: true
        source:
          type: string
          nullable: true
        target:
          type: string
          nullable: true
        data:
          type: object
          additionalProperties: true
        items:
          type: array
          items:
            type: object
            additionalProperties: true
    SelfserveStudioGraphSaveRequest:
      type: object
      required: [department]
      properties:
        department: { type: integer }
        operations:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioGraphOperation'
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioNode'
        layout:
          $ref: '#/components/schemas/SelfserveStudioLayout'
    SelfserveStudioLayoutSaveRequest:
      type: object
      required: [department, layout]
      properties:
        department: { type: integer }
        layout:
          $ref: '#/components/schemas/SelfserveStudioLayout'
    SelfserveMachineType:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    SelfserveVisibleQuestion:
      type: object
      properties:
        id:
          type: integer
        question:
          type: string
        description:
          type: string
        condition_id:
          type: integer
          nullable: true
        order_priority:
          type: integer
        answer:
          type: boolean
          nullable: true

    SelfserveTaskDecision:
      type: object
      properties:
        id:
          type: integer
        task:
          type: string
        description:
          type: string
        condition_id:
          type: integer
          nullable: true
        order_priority:
          type: integer
        services:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveLaneService'
        buttons:
          type: array
          items:
            type: integer

    SelfserveWashSession:
      type: object
      properties:
        id:
          type: integer
        lane_id:
          type: integer
        department_id:
          type: integer
        machine_type_id:
          type: integer
          nullable: true
        customer_number:
          type: integer
          nullable: true
        vehicle_id:
          type: integer
          nullable: true
        vehicle_type_id:
          type: integer
          nullable: true
        reg:
          type: string
        status:
          type: string
          enum:
            - PENDING_QUESTIONS
            - READY_FOR_MACHINE_START
            - MACHINE_NOT_ALLOWED
            - MACHINE_RELAY_ENABLED
            - MACHINE_STARTED
            - COMPLETED
            - FORCE_STOPPED
        allowed:
          type: boolean
        machine_relay_enabled:
          type: boolean
        machine_relay_enabled_at:
          type: string
          format: date-time
          nullable: true
        machine_start_triggered:
          type: boolean
        machine_start_triggered_at:
          type: string
          format: date-time
          nullable: true
        wash_started_at:
          type: string
          format: date-time
          nullable: true
        order_id:
          type: integer
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        metadata:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    SelfserveWashQuestionAnswer:
      type: object
      properties:
        question_id:
          type: integer
        question:
          type: string
        answer:
          type: boolean
          nullable: true
        answered_at:
          type: string
          format: date-time
          nullable: true

    SelfserveWashTaskSnapshot:
      type: object
      properties:
        task_id:
          type: integer
          nullable: true
        task:
          type: string
        description:
          type: string
          nullable: true
        services:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveLaneService'
        buttons:
          type: array
          items:
            type: integer

    SelfserveWashEvent:
      type: object
      properties:
        id:
          type: integer
        type:
          type: string
          enum:
            - SESSION_SYNCED
            - MACHINE_RELAY_ENABLED
            - MACHINE_START_TRIGGERED
            - SESSION_COMPLETED
            - SESSION_FORCE_STOPPED
        payload:
          type: object
          additionalProperties: true
          nullable: true
        created_at:
          type: string
          format: date-time

    SelfserveStudioSimulationDebug:
      type: object
      required: [summary, parameters, stages, questions, conditions, rules, tasks, hardware, graph_annotations, recommendations]
      properties:
        summary:
          type: object
          additionalProperties: true
        parameters:
          type: object
          additionalProperties: true
        stages:
          type: array
          items:
            type: object
            additionalProperties: true
        questions:
          type: array
          items:
            type: object
            additionalProperties: true
        conditions:
          type: array
          items:
            type: object
            additionalProperties: true
        rules:
          type: array
          items:
            type: object
            additionalProperties: true
        tasks:
          type: array
          items:
            type: object
            additionalProperties: true
        actions:
          type: array
          items:
            type: object
            additionalProperties: true
        dynamic_image_buttons:
          type: array
          items:
            type: object
            additionalProperties: true
        decisions:
          type: array
          items:
            type: object
            required: [kind, id, label, state, reason, node_ids, causes]
            properties:
              kind:
                type: string
              id:
                oneOf:
                  - type: integer
                  - type: string
                nullable: true
              label:
                type: string
              state:
                type: string
              reason:
                type: string
              node_ids:
                type: array
                items:
                  type: string
              causes:
                type: array
                items:
                  type: object
                  additionalProperties: true
            additionalProperties: true
        hardware:
          type: object
          additionalProperties: true
        graph_annotations:
          type: object
          properties:
            nodes:
              type: object
              additionalProperties:
                type: object
                additionalProperties: true
            edges:
              type: object
              additionalProperties:
                type: object
                additionalProperties: true
        recommendations:
          type: array
          items:
            type: object
            additionalProperties: true

    SelfserveStudioSimulationResponse:
      allOf:
        - $ref: '#/components/schemas/SelfserveVehicleAllowedResponse'
        - type: object
          properties:
            simulator_version: { type: integer }
            dry_run: { type: boolean, enum: [true] }
            mode: { type: string, enum: [full_dry_run] }
            config_source: { type: string, enum: [draft, published] }
            debug:
              $ref: '#/components/schemas/SelfserveStudioSimulationDebug'

    SelfserveStudioPathOutcomesRequest:
      type: object
      required: [department]
      properties:
        department:
          type: integer
        lane_id:
          type: integer
          nullable: true
        vehicle_type_id:
          type: integer
          nullable: true
        config_source:
          type: string
          enum: [draft, published]
          default: draft
        hardware_mode:
          type: string
          enum: [studio, real, none]
          default: studio
        include_hardware:
          type: boolean
          default: true
        max_states:
          type: integer
          minimum: 1
          maximum: 2048
          default: 2048
          nullable: true
          description: Optional debug cap for explored states. Omitted and larger values are capped at 2048.
        path_sample_limit:
          type: integer
          minimum: 1
          maximum: 2048
          default: 2048
          nullable: true
          description: Optional cap for returned path rows. Omitted returns every projected terminal path within the state cap; larger values are capped at 2048.

    SelfserveStudioPathOutcomesResponse:
      type: object
      required: [scope, summary, outcomes, paths, warnings, truncated, progress]
      properties:
        scope:
          type: object
          additionalProperties: true
        summary:
          type: object
          required: [state_count, terminal_path_count, outcome_count, question_count, max_states, path_sample_count]
          properties:
            state_count: { type: integer }
            terminal_path_count: { type: integer }
            outcome_count: { type: integer }
            question_count: { type: integer }
            question_ids:
              type: array
              items: { type: integer }
            max_states: { type: integer }
            path_sample_count: { type: integer }
            confirmations:
              $ref: '#/components/schemas/SelfserveStudioPathConfirmationSummary'
        outcomes:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathOutcome'
        paths:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathResult'
        warnings:
          type: array
          items: { type: string }
        truncated:
          type: boolean
        progress:
          $ref: '#/components/schemas/SelfserveStudioPathProgress'
        confirmations:
          type: object
          properties:
            summary:
              $ref: '#/components/schemas/SelfserveStudioPathConfirmationSummary'
            removed:
              type: array
              items:
                $ref: '#/components/schemas/SelfserveStudioPathConfirmation'

    SelfserveStudioPathProgress:
      type: object
      required: [complete, percent, state_count, pending_state_count, terminal_path_count]
      properties:
        complete: { type: boolean }
        percent:
          type: integer
          minimum: 0
          maximum: 100
        state_count: { type: integer }
        pending_state_count: { type: integer }
        terminal_path_count: { type: integer }
        scenario_index:
          type: integer
          nullable: true
        scenario_count:
          type: integer
          nullable: true

    SelfserveStudioPathOutcome:
      type: object
      required: [id, summary, path_count, allowed, services, tasks, signals, sample_chains, node_ids]
      properties:
        id: { type: string }
        summary: { type: string }
        path_count: { type: integer }
        allowed: { type: boolean }
        services:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveLaneService'
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathTask'
        signals:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathSignal'
        sample_chains:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathSampleChain'
        scopes:
          type: array
          items:
            type: object
            additionalProperties: true
        node_ids:
          type: array
          items: { type: string }

    SelfserveStudioPathResult:
      type: object
      required: [id, result, summary, allowed, services, tasks, signals, task_count, signal_count, answers, scope, node_ids]
      properties:
        id: { type: string }
        result: { type: string }
        summary: { type: string }
        allowed: { type: boolean }
        services:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveLaneService'
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathTask'
        signals:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathSignal'
        task_count: { type: integer }
        signal_count: { type: integer }
        answers:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathAnswer'
        scope:
          type: object
          additionalProperties: true
        node_ids:
          type: array
          items: { type: string }
        path_signature: { type: string }
        result_signature: { type: string }
        confirmation_status:
          type: string
          enum: [unconfirmed, confirmed, stale]
        confirmed_at:
          type: string
          nullable: true
        confirmed_by:
          type: integer
          nullable: true
        stale_reason:
          type: string
          nullable: true

    SelfserveStudioPathConfirmationSummary:
      type: object
      properties:
        confirmed: { type: integer }
        unconfirmed: { type: integer }
        stale: { type: integer }
        removed: { type: integer }
        total: { type: integer }

    SelfserveStudioPathConfirmationRequest:
      type: object
      required: [department, path_signature]
      properties:
        department: { type: integer }
        action:
          type: string
          enum: [confirm, reset, delete, clear]
          default: confirm
        path_signature: { type: string }
        result_signature:
          type: string
          description: Required when action is confirm.
        scope:
          type: object
          additionalProperties: true
        answers:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathAnswer'
        result:
          type: object
          additionalProperties: true

    SelfserveStudioPathConfirmation:
      type: object
      properties:
        id:
          type: integer
          nullable: true
        department_id: { type: integer }
        lane_id:
          type: integer
          nullable: true
        vehicle_type_id:
          type: integer
          nullable: true
        config_version_id:
          type: integer
          nullable: true
        config_source: { type: string }
        path_signature: { type: string }
        result_signature: { type: string }
        confirmation_status:
          type: string
          enum: [unconfirmed, confirmed, stale]
        answers:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathAnswer'
        result:
          type: object
          additionalProperties: true
        scope:
          type: object
          additionalProperties: true
        confirmed_at:
          type: string
          nullable: true
        confirmed_by:
          type: integer
          nullable: true
        stale_reason:
          type: string
          nullable: true

    SelfserveStudioPathTask:
      type: object
      properties:
        id: { type: integer }
        node_id: { type: string }
        label: { type: string }
        services:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveLaneService'
        buttons:
          type: array
          items: {}
        order_priority: { type: integer }

    SelfserveStudioPathSignal:
      type: object
      properties:
        sequence: { type: integer }
        runtime_stage: { type: string }
        signal_type: { type: string }
        relay_role: { type: string }
        relay_id:
          type: string
          nullable: true
        target_gateway_label:
          type: string
          nullable: true
        target_binding:
          type: string
          nullable: true
        source: { type: string }
        virtual: { type: boolean }
        predicted_status: { type: string }
        payload:
          type: object
          additionalProperties: true
        skip_block_reason:
          type: string
          nullable: true

    SelfserveStudioPathSampleChain:
      type: object
      properties:
        scope:
          type: object
          additionalProperties: true
        answers:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveStudioPathAnswer'

    SelfserveStudioPathAnswer:
      type: object
      properties:
        question_id: { type: integer }
        question: { type: string }
        node_id: { type: string }
        answer: { type: boolean }
        answer_label: { type: string }

    SelfserveVehicleAllowedResponse:
      type: object
      properties:
        lane:
          $ref: '#/components/schemas/DepartmentLane'
        machine_type:
          allOf:
            - $ref: '#/components/schemas/SelfserveMachineType'
          nullable: true
        vehicle:
          type: object
          additionalProperties: true
          nullable: true
        reg:
          type: string
        customer_number:
          type: integer
          nullable: true
        vehicle_type_id:
          type: integer
          nullable: true
        questions:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveVisibleQuestion'
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveTaskDecision'
        allowed_services:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveLaneService'
        machine_available:
          type: boolean
        all_visible_questions_answered:
          type: boolean
        allowed:
          type: boolean
        blocked_reason:
          type: string
          nullable: true
        session:
          allOf:
            - $ref: '#/components/schemas/SelfserveWashSession'
          nullable: true
        config_source:
          type: string
          nullable: true
        evaluation_trace:
          type: object
          nullable: true
          additionalProperties: true

    SelfserveWashSummary:
      type: object
      properties:
        session:
          $ref: '#/components/schemas/SelfserveWashSession'
        lane:
          allOf:
            - $ref: '#/components/schemas/DepartmentLane'
          nullable: true
        machine_type:
          allOf:
            - $ref: '#/components/schemas/SelfserveMachineType'
          nullable: true
        questions:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveWashQuestionAnswer'
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveWashTaskSnapshot'
        allowed_services:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveLaneService'
        machine_available:
          type: boolean
        all_visible_questions_answered:
          type: boolean
        allowed:
          type: boolean
        events:
          type: array
          items:
            $ref: '#/components/schemas/SelfserveWashEvent'

    SelfserveForceStopResponse:
      type: object
      properties:
        lane_id:
          type: integer
        forced:
          type: boolean
        bill:
          type: boolean
        order_id:
          type: integer
          nullable: true
        session:
          allOf:
            - $ref: '#/components/schemas/SelfserveWashSummary'
          nullable: true
        runtime_before_reset:
          type: object
          additionalProperties: true

    DepartmentSelfserveVehicleConditionMutationResponse:
      type: object
      properties:
        condition:
          $ref: '#/components/schemas/DepartmentSelfserveVehicleCondition'
        selfserve:
          $ref: '#/components/schemas/SelfserveWashSummary'

    MachineButtonPressWebhookResponse:
      type: object
      properties:
        message:
          type: string
        scanner:
          type: string
        lane_id:
          type: integer
        selfserve:
          $ref: '#/components/schemas/SelfserveWashSummary'

    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
        machine_type_id:
          type: integer
          description: Reusable machine type ID
          nullable: true
        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
        machine_type_id:
          type: integer
          description: Reusable machine type ID
          nullable: true
        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
        selfserve_enabled:
          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
        relay_machine_program_picker_id:
          type: string
        relay_machine_cleaner_id:
          type: string
        dynamic_image_id:
          type: integer
          nullable: true
          minimum: 1
        machine_type_id:
          type: integer
          nullable: true
        selfserve_enabled:
          type: boolean
          default: true
        status:
          type: string
        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
        relay_machine_program_picker_id:
          type: string
        relay_machine_cleaner_id:
          type: string
        dynamic_image_id:
          type: integer
          nullable: true
          minimum: 1
        machine_type_id:
          type: integer
          nullable: true
        selfserve_enabled:
          type: boolean
          default: true

    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
        relay_machine_program_picker_id:
          type: string
        relay_machine_cleaner_id:
          type: string
        dynamic_image_id:
          type: integer
          nullable: true
          minimum: 1
        machine_type_id:
          type: integer
          nullable: true
        selfserve_enabled:
          type: boolean

    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

    VehicleManagementSummary:
      type: object
      required: [total, wash_subscription, self_service]
      properties:
        total:
          type: integer
          minimum: 0
        wash_subscription:
          type: integer
          minimum: 0
        self_service:
          type: integer
          minimum: 0

    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
        target_duration:
          type: string
          nullable: true
          description: |
            Optional advanced target duration mode.
            Accepted values: ENTIRE_DURATION, WEEKS, MONTHS, YEARS.
            When omitted, legacy target behavior is preserved for backward compatibility.
            The canonical field name is snake_case `target_duration`.
            For backward-compatibility the API also accepts camelCase `targetDuration` on input.
          enum: [ENTIRE_DURATION, WEEKS, MONTHS, YEARS]
          example: WEEKS
        target_duration_every:
          type: integer
          nullable: true
          minimum: 1
          description: |
            Optional cadence value used with `target_duration` WEEKS, MONTHS, or YEARS.
            Example: with `target_duration=WEEKS` and `target_duration_every=2`,
            the target applies every second week.
            Ignored when `target_duration=ENTIRE_DURATION`.
            The canonical field name is snake_case `target_duration_every`.
            For backward-compatibility the API also accepts camelCase `targetDurationEvery` on input.
          example: 1
        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
          format: uuid
          example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
        workspaceId:
          type: string
          format: uuid
          example: "3d5fae4f-9c2d-41aa-9840-28b18e6a94bc"
        channelId:
          type: string
          format: uuid
          example: "a2545e48-fe8c-5741-9bdc-42a081076bc9"
        callFlowId:
          type: string
          format: uuid
          nullable: true
        originator:
          type: object
          additionalProperties: true
        receiver:
          type: object
          additionalProperties: true
        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
          nullable: true
        hangupSource:
          type: string
          nullable: true
        sipInsights:
          type: object
          additionalProperties: true
        qualityInsights:
          type: object
          additionalProperties: true
        price:
          type: object
          additionalProperties: true
        createdAt: { type: string, format: date-time, nullable: true }
        updatedAt: { type: string, format: date-time, nullable: true }
        ringingAt: { type: string, format: date-time, nullable: true }
        answeredAt: { type: string, format: date-time, nullable: true }
        endedAt: { type: string, format: date-time, nullable: true }

    BirdVoiceCallCommandCondition:
      type: object
      properties:
        variable: { type: string }
        operator: { type: string }
        value: { type: string }

    BirdVoiceCallCommandResult:
      type: object
      properties:
        id:
          type: string
          format: uuid
        callId:
          type: string
          format: uuid
        callFlowId:
          type: string
          format: uuid
          nullable: true
        status:
          type: string
        command:
          type: string
        conditions:
          type: array
          items:
            $ref: '#/components/schemas/BirdVoiceCallCommandCondition'

    BirdVoiceCallBridgeResult:
      allOf:
        - $ref: '#/components/schemas/BirdVoiceCallCommandResult'
        - type: object
          properties:
            bridgeCallId:
              type: string
              format: uuid
              nullable: true

    BirdVoiceCallRecording:
      type: object
      properties:
        id:
          type: string
          format: uuid
        callId:
          type: string
          format: uuid
        status:
          type: string
          example: ongoing
        duration:
          type: integer
          nullable: true
        stereo:
          type: boolean
          nullable: true
        mediaUrl:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
          nullable: true
        updatedAt:
          type: string
          format: date-time
          nullable: true

    BirdVoiceCallInsights:
      type: object
      description: Voice call insights payload as returned by Bird.
      additionalProperties: true

    BirdFlashCall:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
          nullable: true
        channelId:
          type: string
          format: uuid
          nullable: true
        from:
          type: string
          nullable: true
        to:
          type: string
          nullable: true
        receivedCli:
          type: string
          nullable: true
        result:
          type: string
          nullable: true
        status:
          type: string
          nullable: true
        duration:
          type: integer
          nullable: true
        createdAt:
          type: string
          format: date-time
          nullable: true
        updatedAt:
          type: string
          format: date-time
          nullable: true

    BirdVoiceCallListResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          type: object
          properties:
            nextPageToken:
              type: string
              nullable: true
              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: []

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

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

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

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

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

    BirdVoiceCallsLogResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          type: object
          properties:
            nextPageToken:
              type: string
              nullable: true
            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: []

    BirdTestOutboundCallRequest:
      type: object
      additionalProperties: false
      properties:
        from:
          type: string
          description: Caller E.164 number to use for the test call
          example: "+4599988877"
        to:
          type: string
          description: Target E.164 number. Defaults to configured test number if omitted.
          example: "+4542331128"
        timeout:
          type: integer
          minimum: 1
          description: Backward-compatible alias mapped to ringTimeout
        ringTimeout:
          type: integer
          minimum: 3
          maximum: 120
        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
          enum: [rejected, busy]
          description: Optional hangup cause passed through to Bird

    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/BirdVoiceCallCommandResult' }
        meta:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []
        includes:
          oneOf:
            - type: array
              items: {}
            - type: object
              additionalProperties: true
          example: []

    BirdInboundCallWebhookRequest:
      type: object
      additionalProperties: true
      properties:
        callId:
          type: string
          example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
        workspaceId:
          type: string
          format: uuid
        channelId:
          type: string
          format: uuid
        payload:
          type: object
          additionalProperties: true
          properties:
            endKey:
              type: string
              example: "#"
            retries:
              type: integer
              example: 3
            timeout:
              type: integer
              example: 30
            say:
              type: object
              additionalProperties: true
              properties:
                locale:
                  type: string
                  example: "en-US"
                voice:
                  type: string
                  example: "female"
        request:
          type: object
          additionalProperties: true
          properties:
            callId:
              type: string
              example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
            workspaceId:
              type: string
              format: uuid
            channelId:
              type: string
              format: uuid
        waitConditions:
          type: object
          additionalProperties: true
          properties:
            timeout:
              type: string
              example: "PT10M"
            events:
              type: array
              items:
                type: object
                additionalProperties: true
                properties:
                  action:
                    type: string
                    example: "continue"
                  name:
                    type: string
                    example: "call_command_gather_finished"
        event:
          type: object
          additionalProperties: true
        result:
          type: object
          additionalProperties: true
        resumeData:
          type: object
          additionalProperties: true
        dtmf:
          type: string
          description: DTMF value when present, for example `1`, `1#`, or `10#`
          example: "10#"
        digit:
          type: string
          description: Alternate DTMF field, also accepts values such as `10#`
          example: "10#"
        digits:
          type: string
          description: Alternate DTMF field
          example: "10#"
        keys:
          type: string
          description: Alternate DTMF field returned by gather results
          example: "10#"
        key:
          type: string
          example: "1"
        input:
          oneOf:
            - type: string
            - type: object
              additionalProperties: true
        conditions:
          type: array
          items:
            type: object
            additionalProperties: true

    BirdInboundCallWebhookResponse:
      type: object
      oneOf:
        - $ref: '#/components/schemas/BirdInboundCallWebhookFlowGatherResponse'
        - $ref: '#/components/schemas/BirdInboundCallWebhookActionResultResponse'
        - $ref: '#/components/schemas/BirdInboundCallWebhookGatherAcceptedResponse'
        - $ref: '#/components/schemas/BirdInboundCallWebhookTransportErrorResponse'

    BirdInboundCallWebhookFlowGatherResponse:
      type: object
      properties:
        requestId:
          type: string
          example: "request-123"
        callId:
          type: string
          example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
        status:
          type: string
          enum: [gather]
        completed:
          type: boolean
          enum: [false]
        stage:
          type: string
          enum: [department_select, gate_type_select]
        prompt:
          type: string
        gather:
          type: object
          properties:
            input:
              type: string
              enum: [dtmf]
            maxNumKeys:
              type: integer
            endKey:
              type: string
              example: "#"
            timeout:
              type: integer
            retries:
              type: integer
            say:
              type: object
              properties:
                locale:
                  type: string
                  example: "en-US"
                voice:
                  type: string
                  example: "female"
                text:
                  type: string
        selection:
          type: object
          properties:
            departmentId:
              type: integer
              nullable: true
            departmentName:
              type: string
              nullable: true
            gateType:
              type: string
              nullable: true
              enum: [entrance, exit]
            gateId:
              type: integer
              nullable: true
        invalidSelectionCount:
          type: integer
        resumed:
          type: boolean
        statusCode:
          type: integer
          enum: [200]
        statusText:
          type: string
          enum: [OK]

    BirdInboundCallWebhookActionResultResponse:
      type: object
      properties:
        requestId:
          type: string
          example: "request-123"
        result:
          type: object
          properties:
            callId:
              type: string
              example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
            status:
              type: string
              enum: [completed, failed, ignored]
            action:
              type: string
              enum: [gate_opened, gate_open_failed, no_action, ignored]
            message:
              type: string
            departmentId:
              type: integer
              nullable: true
            gateType:
              type: string
              nullable: true
              enum: [entrance, exit]
            gateId:
              type: integer
              nullable: true
            gateOpened:
              type: boolean
        resumeData:
          type: object
          additionalProperties: true
          properties:
            action:
              type: string
              example: "continue"
            completed:
              type: boolean
              example: true
            result:
              type: string
              example: "gate_opened"
            gateOpened:
              type: boolean
              example: true
        completedAt:
          type: string
          format: date-time
        statusCode:
          type: integer
          enum: [200]
        statusText:
          type: string
          enum: [OK]

    BirdInboundCallWebhookGatherAcceptedResponse:
      type: object
      properties:
        event:
          type: object
          additionalProperties: true
        requestId:
          type: string
          example: "request-123"
        result:
          type: object
          properties:
            callId:
              type: string
              example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09"
            command:
              type: string
              enum: [gather]
            id:
              type: string
            status:
              type: string
              example: "accepted"
        resumeData:
          type: object
          properties:
            action:
              type: string
              example: "continue"
        resumedAt:
          type: string
          format: date-time
          nullable: true
        suspendedAt:
          type: string
          format: date-time
        statusCode:
          type: integer
          enum: [202]
        statusText:
          type: string
          enum: [Accepted]

    BirdInboundCallWebhookTransportErrorResponse:
      type: object
      properties:
        requestId:
          type: string
        statusCode:
          type: integer
          enum: [400, 500]
        statusText:
          type: string
          enum: [Bad Request, Internal Server Error]
        error:
          type: object
          properties:
            message:
              type: string

    BirdVoiceCallCreateRequest:
      type: object
      additionalProperties: false
      required: [to]
      properties:
        from: { type: string }
        to: { type: string }
        ringTimeout: { type: integer, minimum: 3, maximum: 120 }
        maxDuration: { type: integer, minimum: 1 }
        sendKeys: { type: string }
        record: { type: boolean }
        recordStart: { type: string, enum: [record-from-answer, record-from-ringing] }
        flowStart: { type: string, enum: [from-answer, from-ringing] }
        stereo: { type: boolean }
        callFlow:
          type: array
          items:
            type: object
            additionalProperties: true
        scheduledFor: { type: string, format: date-time }
        notification:
          type: object
          additionalProperties: false
          properties:
            url: { type: string }
        amdSettings:
          type: object
          additionalProperties: true
        tags:
          type: array
          items: { type: string }

    BirdVoiceCallUpdateRequest:
      type: object
      additionalProperties: false
      properties:
        status:
          type: string
          enum: [completed]
        callFlow:
          type: array
          items:
            type: object
            additionalProperties: true

    BirdVoiceCallAnswerRequest:
      type: object
      additionalProperties: false
      properties: {}

    BirdVoiceCallRingingRequest:
      type: object
      additionalProperties: false
      properties: {}

    BirdVoiceCallHangupRequest:
      type: object
      additionalProperties: false
      properties:
        cause:
          type: string
          enum: [rejected, busy]

    BirdVoiceCallPlaybackRequest:
      type: object
      additionalProperties: false
      required: [media]
      properties:
        media:
          type: array
          minItems: 1
          items: { type: string }
        loop: { type: integer, minimum: 0 }
        timeout: { type: integer, minimum: 0 }
        pauseMilliseconds: { type: integer, minimum: 0, maximum: 30000 }

    BirdVoiceCallSayRequest:
      type: object
      additionalProperties: false
      required: [text]
      properties:
        text: { type: string }
        locale: { type: string }
        voice: { type: string }
        loop: { type: integer, minimum: 0 }
        timeout: { type: integer, minimum: 0 }
        hangup: { type: boolean }

    BirdVoiceCallGatherRequest:
      type: object
      additionalProperties: false
      properties:
        maxNumKeys: { type: integer, minimum: 1 }
        endKey: { type: string, enum: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#'] }
        timeout: { type: integer, minimum: 0 }
        retries: { type: integer, minimum: 0 }
        input: { type: string, enum: [dtmf, speech, 'dtmf speech'] }
        speechLocale: { type: string }
        playback:
          $ref: '#/components/schemas/BirdVoiceCallPlaybackRequest'
        say:
          $ref: '#/components/schemas/BirdVoiceCallSayRequest'

    BirdVoiceCallBridgeRequest:
      type: object
      additionalProperties: false
      required: [to]
      properties:
        from: { type: string }
        to: { type: string }
        ringTimeout: { type: integer, minimum: 3, maximum: 120 }
        maxDuration: { type: integer, minimum: 1 }
        ringTone: { type: string }
        hangupAfterBridge: { type: boolean }
        record: { type: boolean }
        recordStart: { type: string, enum: [record-from-answer, record-from-ringing] }
        recordStereo: { type: boolean }
        callFlow:
          type: array
          items:
            type: object
            additionalProperties: true
        notification:
          type: object
          additionalProperties: false
          properties:
            url: { type: string }
        amdSettings:
          type: object
          additionalProperties: true

    BirdVoiceCallRecordRequest:
      type: object
      additionalProperties: false
      properties:
        endKey: { type: string, enum: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#'] }
        maxLength: { type: integer, minimum: 1 }
        timeout: { type: integer, minimum: 0 }
        beep: { type: boolean }
        transcribe: { type: boolean }
        transcribeLocale: { type: string }

    BirdVoiceCallRecordingCreateRequest:
      type: object
      additionalProperties: false
      properties:
        maxLength: { type: integer, minimum: 1 }
        stereo: { type: boolean }

    BirdVoiceCallRecordingUpdateRequest:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status:
          type: string
          enum: [paused, ongoing, completed]

    BirdFlashCallCreateRequest:
      type: object
      additionalProperties: false
      required: [to]
      properties:
        from: { type: string }
        to: { type: string }
        ringTimeout: { type: integer, minimum: 3, maximum: 120 }

    BirdFlashCallEndRequest:
      type: object
      additionalProperties: false
      required: [result]
      properties:
        receivedCli: { type: string }
        result:
          type: string
          enum: [unknown, verified, canceled, timeout, wrong_cli]

    BirdFlashCallHangupRequest:
      oneOf:
        - type: object
          additionalProperties: false
          required: [result]
          properties:
            receivedCli: { type: string }
            result:
              type: string
              enum: [unknown, verified, canceled, timeout, wrong_cli]
        - type: object
          additionalProperties: false
          required: [from, to]
          properties:
            from: { type: string }
            to: { type: string }
            receivedCli: { type: string }
            result:
              type: string
              enum: [unknown, verified, canceled, timeout, wrong_cli]

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

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

    BirdFlashCallHangupResult:
      type: object
      properties:
        id:
          type: string
          format: uuid
          nullable: true
        result:
          type: string
          nullable: true
        receivedCli:
          type: string
          nullable: true
        from:
          type: string
          nullable: true
        to:
          type: string
          nullable: true

    BirdFlashCallHangupResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          $ref: '#/components/schemas/BirdFlashCallHangupResult'
        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: []

    DepartmentDailyReportOutsideHoursBreakdown:
      type: object
      properties:
        orders: { type: integer }
        xlvask: { type: integer }
        selfserve: { type: integer }

    DepartmentDailyReportOutsideHoursSummary:
      type: object
      properties:
        department_ids:
          type: array
          items: { type: integer }
        date: { type: string }
        date_to: { type: string }
        total: { type: integer }
        by_source:
          $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursBreakdown'
        has_missing_opening_hours: { type: boolean }
        missing_department_ids:
          type: array
          items: { type: integer }

    DepartmentDailyReportMetric:
      type: object
      properties:
        state: { type: string }
        value:
          type: number
          nullable: true
        out_of:
          type: number
          nullable: true
        message:
          type: string
          nullable: true
        by_source:
          $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursBreakdown'
        has_missing_opening_hours: { type: boolean }
        missing_department_ids:
          type: array
          items: { type: integer }

    DepartmentDailyReportComplaintCreateRequest:
      type: object
      required: [department_id, wash_date, category, description]
      properties:
        department_id: { type: integer }
        customer_number:
          type: integer
          nullable: true
        wash_date:
          type: string
          format: date
        category:
          $ref: '#/components/schemas/DepartmentDailyReportComplaintCategory'
        description:
          type: string
          minLength: 1
          maxLength: 4000

    DepartmentDailyReportComplaintUpdateRequest:
      type: object
      required: [id]
      properties:
        id: { type: integer }
        department_id: { type: integer }
        customer_number:
          type: integer
          nullable: true
        wash_date:
          type: string
          format: date
        category:
          $ref: '#/components/schemas/DepartmentDailyReportComplaintCategory'
        description:
          type: string
          minLength: 1
          maxLength: 4000

    DepartmentDailyReportComplaintCategory:
      type: string
      enum:
        - wash_quality
        - wash_price
        - damage_paint
        - damage_mirrors
        - damage_cables_electronics
        - damage_plastic_parts
        - damage_other
        - service
        - other

    DepartmentDailyReportComplaintCustomerSearchResult:
      type: object
      properties:
        customer_number:
          type: integer
        customer_name:
          type: string
          nullable: true

    DepartmentDailyReportComplaint:
      type: object
      properties:
        id: { type: integer }
        department_id: { type: integer }
        department_name:
          type: string
          nullable: true
        customer_number:
          type: integer
          nullable: true
        customer_name:
          type: string
          nullable: true
        wash_date:
          type: string
          format: date
          nullable: true
        category:
          allOf:
            - $ref: '#/components/schemas/DepartmentDailyReportComplaintCategory'
          nullable: true
        description: { type: string }
        created_by: { type: integer }
        created_by_name:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    DepartmentDailyReportComplaintResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          $ref: '#/components/schemas/DepartmentDailyReportComplaint'

    DepartmentDailyReportComplaintCollectionResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          oneOf:
            - $ref: '#/components/schemas/DepartmentDailyReportComplaint'
            - type: array
              items:
                $ref: '#/components/schemas/DepartmentDailyReportComplaint'

    DepartmentDailyReportComplaintCustomerSearchResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          type: array
          items:
            $ref: '#/components/schemas/DepartmentDailyReportComplaintCustomerSearchResult'

    DepartmentDailyReportComplaintDeleteResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          type: object
          properties:
            message: { type: string }

    DepartmentDailyReportProductTile:
      type: object
      properties:
        product_id: { type: integer }
        slug: { type: string }
        title: { type: string }
        state: { type: string }
        value: { type: integer }
        out_of: { type: integer }
        target_percentage: { type: number, format: float, nullable: true }
        target_department_id: { type: integer, nullable: true }

    DepartmentDailyReportProductTargetRequest:
      type: object
      required:
        - department_id
        - product_id
        - target_percentage
      properties:
        department_id: { type: integer }
        product_id: { type: integer }
        target_percentage:
          type: number
          format: float
          nullable: true

    DepartmentDailyReportProductTarget:
      type: object
      properties:
        department_id: { type: integer }
        product_id: { type: integer }
        target_percentage: { type: number, format: float, nullable: true }

    DepartmentDailyReportProductTargetResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          $ref: '#/components/schemas/DepartmentDailyReportProductTarget'

    DepartmentDailyReportOverviewPayload:
      type: object
      properties:
        department_ids:
          type: array
          items: { type: integer }
        date: { type: string }
        date_to: { type: string }
        metrics:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/DepartmentDailyReportMetric'
        products:
          type: array
          items:
            $ref: '#/components/schemas/DepartmentDailyReportProductTile'

    DepartmentDailyReportOverviewResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          $ref: '#/components/schemas/DepartmentDailyReportOverviewPayload'

    SuperuserDepartmentOverviewPayload:
      type: object
      properties:
        department:
          $ref: '#/components/schemas/Department'
        overview:
          $ref: '#/components/schemas/DepartmentDailyReportOverviewPayload'

    SuperuserDepartmentOverviewResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          $ref: '#/components/schemas/SuperuserDepartmentOverviewPayload'

    DepartmentDailyReportTransactionCountPayload:
      type: object
      properties:
        quantity: { type: integer }
        products: { type: integer }
        earnings: { type: integer }
        washes: { type: integer }
        water_usage: { type: integer }
        date: { type: string }
        date_to: { type: string }
        department_id: { type: integer }
        outside_hours:
          $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursSummary'

    DepartmentDailyReportTransactionCountResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          $ref: '#/components/schemas/DepartmentDailyReportTransactionCountPayload'

    DepartmentDailyReportOutsideHoursTrendPoint:
      type: object
      properties:
        date: { type: string }
        total: { type: integer }
        by_source:
          $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursBreakdown'
        has_missing_opening_hours: { type: boolean }
        missing_department_ids:
          type: array
          items: { type: integer }

    DepartmentDailyReportOutsideHoursTrendPayload:
      type: object
      properties:
        department_ids:
          type: array
          items: { type: integer }
        date: { type: string }
        date_to: { type: string }
        points:
          type: array
          items:
            $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendPoint'
        has_missing_opening_hours: { type: boolean }
        missing_department_ids:
          type: array
          items: { type: integer }

    DepartmentDailyReportOutsideHoursTrendResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendPayload'
