openapi: 3.0.3
info:
  title: Repzo API - AI Object Detection Category
  version: 1.0.0
  description: |
    Detection categories group what a shelf-scan session is ABOUT. A category
    carries a `name`, an array of `model_settings` — each item naming a
    trained model / model version plus the full analyze-session `config`
    (confidence, IoU, walk, dims-reclassifier, size-gate knobs) — and a
    `labels` array (the `ai-object-detection-label` ids the category tracks,
    inputs for the upcoming jobs & metrics).

    The mobile AR app lists categories and lets the rep pick one before
    calibration (optional). The picked id rides every uploaded frame's
    `meta_inline.category` and is stamped onto the session. When the device
    posts the upload-complete marker, a session WITH a category is
    auto-analyzed once per `model_settings` item — serially, skipped when the
    session verdict is `rejected`. A session without a category is never
    auto-analyzed.

    Multi-tenant (`company_namespace`, injected from the caller's token) with
    soft-delete (`disabled: true`). Names are unique per namespace among
    non-deleted categories. Deleting a category still referenced by sessions
    fails unless `?force=true`, which detaches it from those sessions first.
    Callable by admins (dashboard CRUD) and reps (mobile list). `PATCH` is not
    allowed (400).

    **Population.** `populatedKeys[]` on find embeds `labels` under
    `labels_populated`; the nested `model_settings.model` and
    `model_settings.model_version` joins are applied IN PLACE (the id inside
    each `model_settings[]` item is replaced by the document).
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /ai-object-detection-category:
    get:
      summary: List detection categories
      operationId: findAiObjectDetectionCategories
      parameters:
        - in: query
          name: _id
          description: Filter by category `_id`. Pass once or as `?_id[]=...` for multiple.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: name
          description: Exact-match on `name` (single value or list).
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: search
          description: Case-insensitive substring / regex match on `name`.
          schema: { type: string }
        - in: query
          name: labels
          description: Filter to categories tracking the given label id(s).
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: disabled
          description: "`true` returns only soft-deleted categories, `false` only live ones. Omitted returns both."
          schema: { type: boolean }
        - in: query
          name: from_updatedAt
          description: Return categories with `updatedAt` on/after this Unix timestamp (ms); snapped to start of day unless `exact_time=true`.
          schema: { type: number }
        - in: query
          name: to_updatedAt
          description: Return categories with `updatedAt` on/before this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: from_createdAt
          description: Return categories with `createdAt` on/after this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: to_createdAt
          description: Return categories with `createdAt` on/before this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: populatedKeys
          description: |
            Joins to include. `labels` is emitted under `labels_populated`;
            `model_settings.model` / `model_settings.model_version` replace the
            ids inside each `model_settings[]` item in place.
          schema:
            type: array
            items:
              type: string
              enum: [labels, model_settings.model, model_settings.model_version]
        - in: query
          name: per_page
          description: Page size (capped by the server's pagination max).
          schema: { type: integer, minimum: 1 }
          example: 20
        - in: query
          name: page
          description: 1-based page number.
          schema: { type: integer, minimum: 1 }
          example: 1
        - in: query
          name: sort
          description: Field to sort by. Defaults to `_id`.
          schema: { type: string, default: _id }
        - in: query
          name: sortPageOrder
          description: Sort direction. Defaults to descending.
          schema: { type: string, enum: [asc, dsc], default: dsc }
      responses:
        "200":
          description: Paginated categories.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoryFindResult"
    post:
      summary: Create a detection category
      description: 'Each `model_settings` item must name a `model` (400 otherwise). A `model_version` of `latest`, `""` or `null` is normalised to unset (= track the model''s current version).'
      operationId: createAiObjectDetectionCategory
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CategoryCreateBody"
      responses:
        "201":
          description: The created category.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategorySchema"
        "400":
          description: "`model_settings` is not an array, an item lacks `model`, or the name already exists among live categories."
  /ai-object-detection-category/{id}:
    get:
      summary: Get one detection category
      operationId: getAiObjectDetectionCategory
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The category document (no population on get).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategorySchema"
        "400":
          description: No category with that id in the caller's namespace.
    put:
      summary: Update a detection category
      description: "Standard put (mongoose `updateOne` with validators). `model_settings`, when present, is normalised and validated as on create."
      operationId: updateAiObjectDetectionCategory
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CategoryUpdateBody"
      responses:
        "200":
          description: The category document after the update is applied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategorySchema"
        "404":
          description: No category with that id in the caller's namespace.
    delete:
      summary: Soft-delete a detection category
      description: |
        Sets `disabled: true`. Fails with 400 when live sessions still reference
        the category (error data `{ assigned_sessions: <n>, requires_force: true }`)
        — retry with `?force=true` to detach it from those sessions first (the
        dashboard asks for explicit confirmation).
      operationId: removeAiObjectDetectionCategory
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: query
          name: force
          description: |
            When `true`, a category still referenced by sessions is detached
            from them (their `category` field is unset) and then deleted.
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: The soft-deleted category document.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategorySchema"
        "400":
          description: Sessions still reference the category (and `force` was not set), or it was not found.
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: api-key
      description: |
        Server-issued API key. Also accepted via the `x-api-key` header or the
        `?apiKey=` query parameter as fallbacks.
    JwtAuth:
      type: apiKey
      in: header
      name: Authorization
      description: |
        Raw JWT in the `Authorization` header — **no `Bearer ` prefix**.
        Obtained from `POST /authenticate` (admin / rep / client login).
  schemas:
    ModelSetting:
      type: object
      description: |
        One auto-analysis recipe. A category with N items produces N analyses
        per received session (run serially).
      required:
        - model
      properties:
        _id:
          type: string
          description: Sub-document id (server-assigned).
        model:
          type: string
          description: |
            `ai-object-detection-model` `_id`. Required — it names which model to
            run, and (when `model_version` is omitted) the target whose
            `current_model_version` resolves the "latest" version.
        model_version:
          type: string
          nullable: true
          description: |
            `ai-object-detection-model-version` `_id` the auto-analysis infers
            with. Omit (or send the sentinel `latest`, `""` or `null`, all
            normalised to unset) to track the model's `current_model_version` —
            the latest trained version at analysis time.
        config:
          type: object
          additionalProperties: true
          description: |
            Analyze-session settings applied to the run — the same object the
            dashboard's Analyze 3D Scene dialog posts (`conf`, `iou`, walk
            params, `reclassify_*`, `size_gate*`, `build_point_cloud`, ...).
            Unset keys fall back to the scene-math defaults.
    CategoryCreateBody:
      type: object
      description: "Body for creating a category. `company_namespace` is optional for SDK callers and otherwise injected from the caller's session."
      required:
        - name
      properties:
        name:
          type: string
          description: Unique per namespace among non-deleted categories.
        model_settings:
          type: array
          items:
            $ref: "#/components/schemas/ModelSetting"
        labels:
          type: array
          items: { type: string }
          description: "`ai-object-detection-label` ids the category tracks (jobs/metrics inputs)."
        company_namespace:
          type: array
          items: { type: string }
          description: Optional tenant namespace override for SDK callers.
    CategoryUpdateBody:
      type: object
      description: "Body for updating a category. `company_namespace` is derived from the caller's session — do not send it."
      properties:
        name: { type: string }
        model_settings:
          type: array
          items:
            $ref: "#/components/schemas/ModelSetting"
        labels:
          type: array
          items: { type: string }
        disabled:
          type: boolean
          description: "Soft-delete flag — set `true` to disable via update."
    CategorySchema:
      type: object
      properties:
        _id: { type: string }
        name: { type: string }
        model_settings:
          type: array
          items:
            $ref: "#/components/schemas/ModelSetting"
        labels:
          type: array
          items: { type: string }
        disabled: { type: boolean }
        company_namespace:
          type: array
          items: { type: string }
          description: Tenant key (server-injected).
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    CategoryFindResult:
      type: object
      description: Standard paginated result envelope.
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/CategorySchema"
        total_result: { type: number }
        current_count: { type: number }
        total_pages: { type: number }
        current_page: { type: number }
        per_page: { type: number }
        first_page_url: { type: string }
        last_page_url: { type: string }
        next_page_url: { type: string, nullable: true }
        prev_page_url: { type: string, nullable: true }
        path: { type: string }
