openapi: 3.0.3
info:
  title: Repzo API - AI Object Detection Label Group
  version: 1.0.0
  description: |
    A **label group** is a family of sibling object-detection labels that the
    detector tends to confuse — the size or flavour variants of one product
    line (e.g. the 400g / 800g jars of the same mayonnaise). Groups exist for
    the **dims reclassifier**: after a shelf-scan session is analysed, a
    detection whose measured physical size fits a sibling better than its
    predicted label may be re-labelled — but only ever BETWEEN labels that
    share a group, never across groups.

    A group is just a named bucket; labels join it through their own
    `label_group` field on `ai-object-detection-label` (one group per label).
    Use `GET /ai-object-detection-label?label_group=<id>` to list a group's
    members.

    **Who calls it.** Back-office admins from the label screen.

    **Multi-tenancy & lifecycle.** Scoped by `company_namespace[]` (injected
    from the caller's session). Soft-delete via `disabled: true`; `name` is
    unique per namespace among non-deleted groups. Deleting a group that still
    has live member labels is rejected with 400 (the error payload carries
    `assigned_labels` and `requires_force: true`) unless `?force=true`, which
    first unsets `label_group` on those labels and then soft-deletes the
    group — the dashboard asks for explicit confirmation before forcing.
    `PATCH` is not allowed (400).

    **Key relationships.** Referenced by `ai-object-detection-label`
    (`label_group`).
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /ai-object-detection-label-group:
    get:
      summary: Find label groups
      operationId: findAiObjectDetectionLabelGroups
      parameters:
        - in: query
          name: _id
          description: Filter by group `_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 }
          example: mayonnaise
        - in: query
          name: disabled
          description: "`true` returns only soft-deleted groups, `false` only live ones. Omitted returns both."
          schema: { type: boolean }
        - in: query
          name: from_updatedAt
          description: Return groups 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 groups with `updatedAt` on/before this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: from_createdAt
          description: Return groups with `createdAt` on/after this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: to_createdAt
          description: Return groups with `createdAt` on/before this Unix timestamp (ms).
          schema: { type: number }
        - 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 list of label groups.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OdLabelGroupFindResult"
    post:
      summary: Create a label group
      operationId: createAiObjectDetectionLabelGroup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OdLabelGroupCreateBody"
      responses:
        "201":
          description: The created group.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OdLabelGroupSchema"
        "400":
          description: Missing name, or the name already exists among live groups in the namespace.
  /ai-object-detection-label-group/{id}:
    get:
      summary: Get a label group by id
      operationId: getAiObjectDetectionLabelGroup
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The group document.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OdLabelGroupSchema"
        "400":
          description: No group with that id in the caller's namespace.
    put:
      summary: Update a label group
      description: "Standard put (mongoose `updateOne` with validators). Set `disabled: true` to soft-delete without the member-label guard."
      operationId: updateAiObjectDetectionLabelGroup
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OdLabelGroupUpdateBody"
      responses:
        "200":
          description: The group after the update.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OdLabelGroupSchema"
        "404":
          description: No group with that id in the caller's namespace.
    delete:
      summary: Soft-delete a label group (guarded)
      description: |
        Sets `disabled: true`. Rejected with 400 while live labels still point
        at the group (`{ assigned_labels: <n>, requires_force: true }` in the
        error data) unless `?force=true`, which unsets `label_group` on those
        labels first.
      operationId: removeAiObjectDetectionLabelGroup
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: query
          name: force
          description: When `true`, detach the group from its member labels and then soft-delete it.
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: The group after soft-deletion.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OdLabelGroupSchema"
        "400":
          description: The group still has member labels (and `force` was not set), or 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:
    OdLabelGroupSchema:
      type: object
      properties:
        _id: { type: string }
        name:
          type: string
          description: Unique per namespace among non-deleted groups.
        disabled:
          type: boolean
          description: Soft-delete flag.
        company_namespace:
          type: array
          items: { type: string }
          description: Tenant key (server-injected).
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    OdLabelGroupCreateBody:
      type: object
      description: "Body for creating a group. `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 groups.
        company_namespace:
          type: array
          items: { type: string }
          description: Optional tenant namespace override for SDK callers.
    OdLabelGroupUpdateBody:
      type: object
      description: "Body for updating a group. `company_namespace` is derived from the caller's session — do not send it. Set `disabled: true` to soft-delete."
      properties:
        name: { type: string }
        disabled:
          type: boolean
          description: Soft-delete flag.
    OdLabelGroupFindResult:
      type: object
      description: Standard paginated result envelope.
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/OdLabelGroupSchema"
        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 }
