---
openapi: 3.0.1
info:
  title: Kontrolle+ Provider API
  version: v1
  description: |
    Provider-facing API used by external control solutions (OpenEye, ELAZ, Conel)
    to submit control-container data.

    API profile:
    - Scope: provider ingestion only
    - Base path: /api/provider/v1
    - Content type: application/json
    - Error format: application/json (standard provider envelope)

    Rendering recommendation for integrators:
    - Swagger UI (`/swagger`) for try-it-out requests.
    - ReDoc/Scalar for clearer nested schema reading.

    INT base URL:
    - https://kontrolleplus-dev-data.orangeglacier-1536cc06.switzerlandnorth.azurecontainerapps.io/

    Authorization process (INT test client):
    1) Export test credentials

    ```bash
    export PROVIDER_TENANT_ID="c160f35c-f7f5-433e-a67d-8689de4f0193"
    export PROVIDER_CLIENT_ID="9f441a84-9140-4690-9275-21e2cd4ac827"
    export PROVIDER_CLIENT_SECRET="lJQ8Q~VAy~caHnoZiMpqu5BRQhmY5HelDcemvbB-"
    export PROVIDER_TOKEN_SCOPE="api://kontrolleplus-provider-api-dev/.default"
    export PROVIDER_TOKEN_ENDPOINT="https://c160f35c-f7f5-433e-a67d-8689de4f0193.ciamlogin.com/c160f35c-f7f5-433e-a67d-8689de4f0193/oauth2/v2.0/token"
    ```

    2) Request token via OAuth2 client credentials

    ```bash
    curl -sS -X POST "$PROVIDER_TOKEN_ENDPOINT" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      --data-urlencode "client_id=$PROVIDER_CLIENT_ID" \
      --data-urlencode "client_secret=$PROVIDER_CLIENT_SECRET" \
      --data-urlencode "grant_type=client_credentials" \
      --data-urlencode "scope=$PROVIDER_TOKEN_SCOPE"
    ```

    3) Extract access token (optional)

    ```bash
    ACCESS_TOKEN=$(curl -sS -X POST "$PROVIDER_TOKEN_ENDPOINT" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      --data-urlencode "client_id=$PROVIDER_CLIENT_ID" \
      --data-urlencode "client_secret=$PROVIDER_CLIENT_SECRET" \
      --data-urlencode "grant_type=client_credentials" \
      --data-urlencode "scope=$PROVIDER_TOKEN_SCOPE" | jq -r '.access_token')
    ```

    Use the token in API calls:
    - Authorization: Bearer <access_token>

    Authorize in Swagger UI:
    1) Open /swagger
    2) Click Authorize (top right)
    3) In ProviderJwt, paste: Bearer <access_token>
    4) Click Authorize, then Close
    5) Use Try it out on POST /api/provider/v1/controls
paths:
  /api/provider/v1/controls:
    post:
      tags:
        - ProviderControls
      summary: Submit a batch of provider controls for async job processing.
      description: |
        Accepts a non-empty array of provider controls and creates an asynchronous processing job.

        Processing model:
        - Request is syntactically validated upfront.
        - A job is created and returned immediately (202 Accepted).
        - Controls are processed asynchronously and tracked per item.

        Idempotency and deduplication:
        - A control is uniquely identified by a derived inspection key built from inspector and started_at.
        - Duplicate controls within or across submissions are marked with item status DUPLICATE.

        Use GET /api/provider/v1/jobs/{job_id} for aggregate status and
        GET /api/provider/v1/jobs/{job_id}/items for per-control outcomes.
      requestBody:
        required: true
        description: Non-empty list of provider controls to ingest.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProviderControlBatchRequest"
            example:
              schema_version: "1.0"
              device_id: AndId_c428366d643e9ce2
              controls:
                - started_at: "2026-03-09T08:00:00Z"
                  partial_close: "2026-03-09T08:10:00Z"
                  ended_at: "2026-03-09T08:20:00Z"
                  control_grade_pct: 75
                  executing_tu_code: "801"
                  inspector: "UE67277"
                  journey:
                    carrier_tu_code: "801"
                    sjyid: ch:1:sjyid:100058:12952-002
                    line:
                      slnid: ch:1:slnid:103456
                      start: Bernau (Schw), Poche
                      end: Neuenburg (Baden), Bahnhof
                    from_stop_sloid: ch:1:sloid:8578391
                    to_stop_sloid: ch:1:sloid:8578391
                    from_stop_name: Bernau (Schw), Poche
                    to_stop_name: Neuenburg (Baden), Bahnhof
                    planned_departure: "2026-03-09T08:06:00Z"
                  paper_tickets:
                    - category: EINZELBILLETT
                      count: 2
                      timestamp: "2026-03-09T08:09:00Z"
      responses:
        "202":
          description: Batch accepted and queued as a processing job.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderBatchSubmissionResponse"
              example:
                job_id: job-20260309-082001-8f9ab1
                status: QUEUED
                message: Batch accepted for asynchronous processing
                accepted_count: 1
                created_at: "2026-03-09T08:20:01Z"
                status_url: /api/provider/v1/jobs/job-20260309-082001-8f9ab1
        "400":
          description: Malformed request payload.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
              example:
                code: BAD_REQUEST
                message: Malformed request payload
                status: 400
                traceId: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00
                fragments: []
        "401":
          description: Missing, invalid, or expired token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
              example:
                code: UNAUTHORIZED
                message: Missing, invalid, or expired token
                status: 401
                traceId: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00
                fragments: []
        "403":
          description: Token valid but required scope/role is missing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
              example:
                code: FORBIDDEN
                message: Token valid but required scope or role is missing
                status: 403
                traceId: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00
                fragments: []
        "422":
          description: Semantic validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
              example:
                code: VALIDATION_FAILED
                message: One or more validation errors occurred
                status: 422
                traceId: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00
                fragments:
                  - field: control_grade_pct
                    code: INVALID_VALUE
                    message: Wert muss 25, 50, 75 oder 100 sein
        "429":
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
              example:
                code: RATE_LIMITED
                message: Too many requests
                status: 429
                traceId: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00
                fragments: []
        "500":
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
      security:
        - ProviderJwt: []

  /api/provider/v1/jobs/{job_id}:
    get:
      tags:
        - ProviderControls
      summary: Get aggregate status of an asynchronous provider ingest job.
      description: |
        Returns global job status and processing progress counters.
      parameters:
        - name: job_id
          in: path
          required: true
          schema:
            type: string
          description: Job identifier returned by POST /api/provider/v1/controls.
      responses:
        "200":
          description: Job status found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderJobStatusResponse"
              example:
                job_id: job-20260309-082001-8f9ab1
                status: RUNNING
                total_items: 100
                processed_items: 45
                succeeded_items: 38
                duplicate_items: 2
                failed_items: 5
                created_at: "2026-03-09T08:20:01Z"
                started_at: "2026-03-09T08:20:03Z"
                completed_at: null
        "401":
          description: Missing, invalid, or expired token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
        "403":
          description: Token valid but required scope/role is missing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
        "404":
          description: Job id not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
              example:
                code: NOT_FOUND
                message: Job not found
                status: 404
                traceId: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00
                fragments: []
      security:
        - ProviderJwt: []

  /api/provider/v1/jobs/{job_id}/items:
    get:
      tags:
        - ProviderControls
      summary: Get per-item results for a provider ingest job.
      description: |
        Returns per-control processing outcomes for the specified job.
        Optional status filter helps clients fetch only failed or duplicate items.
      parameters:
        - name: job_id
          in: path
          required: true
          schema:
            type: string
          description: Job identifier returned by POST /api/provider/v1/controls.
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [PROCESSED, DUPLICATE, VALIDATION_FAILED, TRANSIENT_ERROR, PERMANENT_ERROR]
          description: Optional filter by item status.
      responses:
        "200":
          description: Job items found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderJobItemsResponse"
              example:
                job_id: job-20260309-082001-8f9ab1
                items:
                  - item_id: item-001
                    control_id: insp-ue67277-20260309T080000Z
                    status: PROCESSED
                    retryable: false
                    message: Control processed successfully
                  - item_id: item-002
                    control_id: insp-ue67277-20260309T081000Z
                    status: VALIDATION_FAILED
                    retryable: false
                    message: Validation failed for control_grade_pct
                    error_code: INVALID_VALUE
        "401":
          description: Missing, invalid, or expired token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
        "403":
          description: Token valid but required scope/role is missing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
        "404":
          description: Job id not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderErrorResponse"
      security:
        - ProviderJwt: []
components:
  schemas:
    ProviderErrorResponse:
      type: object
      required:
        - code
        - message
        - status
        - traceId
        - fragments
      properties:
        code:
          type: string
        message:
          type: string
        status:
          type: integer
          format: int32
        traceId:
          type: string
        fragments:
          type: array
          items:
            $ref: "#/components/schemas/ProviderErrorFragment"

    ProviderErrorFragment:
      type: object
      required:
        - field
        - code
        - message
      properties:
        field:
          type: string
        code:
          type: string
        message:
          type: string

    ProviderControlContainerRequest:
      type: object
      additionalProperties: false
      description: Full provider submission payload for one control container.
      required:
        - started_at
        - partial_close
        - control_grade_pct
        - executing_tu_code
        - inspector
        - journey
        - paper_tickets
      properties:
        started_at:
          type: string
          format: date-time
          description: Timestamp when inspection begins.
        partial_close:
          type: string
          format: date-time
          description: Interim close timestamp.
        ended_at:
          type: string
          format: date-time
          nullable: true
          description: End-of-inspection timestamp.
        control_grade_pct:
          type: integer
          format: int32
          description: Inspection level/coverage percentage.
          enum: [25, 50, 75, 100]
        executing_tu_code:
          type: string
          minLength: 1
          description: Organization number of the company executing the control (for example 801, 2108).
          pattern: "^[0-9]+$"
        inspector:
          type: string
          minLength: 1
          description: Inspector identifier.
        journey:
          $ref: "#/components/schemas/ProviderJourneyInfo"
        paper_tickets:
          type: array
          minItems: 1
          description: Non-empty list of paper ticket aggregates captured during inspection.
          items:
            $ref: "#/components/schemas/ProviderPaperTicketCount"

    ProviderControlBatchRequest:
      type: object
      additionalProperties: false
      description: Asynchronous provider batch ingest request.
      required:
        - schema_version
        - device_id
        - controls
      properties:
        schema_version:
          type: string
          description: Contract version. Must be exactly 1.0.
          enum: ["1.0"]
        device_id:
          type: string
          maxLength: 100
          description: Unique identifier of the provider device.
        controls:
          type: array
          minItems: 1
          maxItems: 500
          description: Non-empty list of provider controls to process asynchronously.
          items:
            $ref: "#/components/schemas/ProviderControlContainerRequest"

    ProviderJourneyInfo:
      type: object
      additionalProperties: false
      description: Journey attributes used for operational context and downstream matching.
      required:
        - carrier_tu_code
        - from_stop_name
        - to_stop_name
        - line
        - from_stop_sloid
        - to_stop_sloid
        - planned_departure
      properties:
        carrier_tu_code:
          type: string
          minLength: 1
          description: Carrier/company responsible for transport execution.
          pattern: "^[0-9]+$"
        from_stop_name:
          type: string
          minLength: 1
          description: Human-readable boarding/start stop name.
        to_stop_name:
          type: string
          minLength: 1
          description: Human-readable destination/end stop name.
        sjyid:
          type: string
          nullable: true
          description: Swiss Journey ID; unique identifier for a concrete journey/run. Accepted formats are listed below.
          oneOf:
            - type: string
              title: Canonical
              description: Canonical format.
              pattern: "^ch:1:sjyid:[0-9]{6}:[0-9]{5}-[0-9]{3}$"
            - type: string
              title: Legacy
              description: Legacy numeric base ID XXXXXX.
              pattern: "^[0-9]{6}$"
        line:
          $ref: "#/components/schemas/ProviderJourneyLine"
        from_stop_sloid:
          type: string
          description: Boarding stop SLOID. Accepted formats are listed below.
          oneOf:
            - type: string
              title: Canonical
              description: Canonical format.
              pattern: "^ch:1:sloid:[0-9]{6}$"
            - type: string
              title: Legacy
              description: Legacy numeric base ID XXXXXX.
              pattern: "^[0-9]{6}$"
        to_stop_sloid:
          type: string
          description: Destination/alighting stop SLOID. Accepted formats are listed below.
          oneOf:
            - type: string
              title: Canonical
              description: Canonical format.
              pattern: "^ch:1:sloid:[0-9]{6}$"
            - type: string
              title: Legacy
              description: Legacy numeric base ID XXXXXX.
              pattern: "^[0-9]{6}$"
        planned_departure:
          type: string
          format: date-time
          description: Departure time recorded by the inspector during preparation before ticket checks begin.

    ProviderJourneyLine:
      type: object
      additionalProperties: false
      required:
        - slnid
        - start
        - end
      properties:
        slnid:
          type: string
          description: Swiss Line ID. Accepts only canonical format.
          pattern: "^ch:1:slnid:[0-9]{6}$"
        start:
          type: string
          minLength: 1
          description: Direction where the line starts.
        end:
          type: string
          minLength: 1
          description: Direction where the line ends.

    ProviderPaperTicketCount:
      type: object
      additionalProperties: false
      description: Paper ticket aggregate entry at a captured timestamp.
      required:
        - category
        - count
        - timestamp
      properties:
        category:
          type: string
          description: Paper ticket category.
          enum: [EINZELBILLETT, VERBUNDABO, INCOMING, SONSTIGE]
        count:
          type: integer
          format: int32
          description: Number of tickets in this category.
          minimum: 0
          maximum: 999
        timestamp:
          type: string
          format: date-time
          description: Captured timestamp.

    ProviderBatchSubmissionResponse:
      type: object
      additionalProperties: false
      description: Batch submission result returned by POST /controls.
      required:
        - job_id
        - status
        - message
        - accepted_count
        - created_at
        - status_url
      properties:
        job_id:
          type: string
          description: Created asynchronous job identifier.
        status:
          type: string
          description: Initial asynchronous job state.
          enum: [QUEUED]
        message:
          type: string
          description: Human-readable submission result message.
        accepted_count:
          type: integer
          format: int32
          minimum: 0
          description: Number of controls accepted for asynchronous processing.
        created_at:
          type: string
          format: date-time
          description: Job creation timestamp.
        status_url:
          type: string
          description: Relative URL to query aggregate job status.

    ProviderJobStatusResponse:
      type: object
      additionalProperties: false
      description: Aggregate asynchronous processing status for one ingest job.
      required:
        - job_id
        - status
        - total_items
        - processed_items
        - succeeded_items
        - duplicate_items
        - failed_items
        - created_at
      properties:
        job_id:
          type: string
        status:
          type: string
          enum: [QUEUED, RUNNING, COMPLETED, COMPLETED_WITH_ERRORS, FAILED, EXPIRED]
        total_items:
          type: integer
          format: int32
          minimum: 0
        processed_items:
          type: integer
          format: int32
          minimum: 0
        succeeded_items:
          type: integer
          format: int32
          minimum: 0
        duplicate_items:
          type: integer
          format: int32
          minimum: 0
        failed_items:
          type: integer
          format: int32
          minimum: 0
        created_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true

    ProviderJobItemsResponse:
      type: object
      additionalProperties: false
      required:
        - job_id
        - items
      properties:
        job_id:
          type: string
        items:
          type: array
          items:
            $ref: "#/components/schemas/ProviderJobItemResult"

    ProviderJobItemResult:
      type: object
      additionalProperties: false
      required:
        - item_id
        - control_id
        - status
        - retryable
        - message
      properties:
        item_id:
          type: string
          description: Internal item identifier in the processing job.
        control_id:
          type: string
          description: Derived control identifier.
        status:
          type: string
          enum: [PROCESSED, DUPLICATE, VALIDATION_FAILED, TRANSIENT_ERROR, PERMANENT_ERROR]
        retryable:
          type: boolean
          description: Indicates whether retrying this control may succeed.
        message:
          type: string
          description: Human-readable item processing result.
        error_code:
          type: string
          nullable: true
          description: Optional machine-readable error code.

    ProviderValidationError:
      type: object
      additionalProperties: false
      description: Field-level validation issue item.
      required:
        - field
        - code
        - message
      properties:
        field:
          type: string
          description: JSON field path that failed validation.
        code:
          type: string
          description: Machine-readable validation code.
          enum: [REQUIRED, INVALID_VALUE, OUT_OF_RANGE]
        message:
          type: string
          description: Human-readable validation message.

    ProviderValidationProblemDetails:
      type: object
      additionalProperties: {}
      description: Problem-details payload with structured validation errors.
      properties:
        type:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        status:
          type: integer
          format: int32
          nullable: true
        detail:
          type: string
          nullable: true
        instance:
          type: string
          nullable: true
        errors:
          type: array
          description: Per-field validation errors.
          items:
            $ref: "#/components/schemas/ProviderValidationError"

  securitySchemes:
    ProviderJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        OAuth2 client credentials bearer token for Provider API.

        Token endpoint (INT):
        - https://c160f35c-f7f5-433e-a67d-8689de4f0193.ciamlogin.com/c160f35c-f7f5-433e-a67d-8689de4f0193/oauth2/v2.0/token

        Scope (INT):
        - api://kontrolleplus-provider-api-dev/.default

        Required claim:
        - roles includes ProviderApi.Access
