openapi: 3.0.3
info:
  title: Repzo API - Object Detection Analytics Report
  version: 1.0.0
  description: |
    **Detection analytics** — the V2 report over metric results
    (`ai.objectDetectionMetricResults`). One endpoint, TWO views selected by
    `body.view`:

    - `metrics` (default) — one row per METRIC RESULT: the effective answer
      (human override wins) and the 0..1 score are the core data, joined with
      the scanned session's client, channel and rep, and the mission the
      session was started from (when any).
    - `segments` — one row per UNWOUND SegmentOutput of share-of-shelf
      results; every metric-level field rides along and the SEGMENT's share
      is the core. The competitive read: filter segment "American Garden" +
      channels A and B, group by `channel` → its share per channel; group by
      `channel` + `month` over the last two months → how it moved.

    **V2 body** (`POST`): `anyOf[0].criteria[]` filters (`time` between ms or
    presets; `metric`/`type`/`output`/`flag`/`client`/`channel`/`rep`/`team`/
    `mission`/`segment` with in/nin/eq/ne — `mission` matches the mission the
    session was STARTED FROM, so it narrows to mission-driven sessions;
    LABEL filters `label`/`label_group`/`product_brand`/`product_category`
    match rows whose metric INVOLVES the labels — named directly or resolved
    through the label's product-side attributes, each key AND-ing
    independently; `business_day`/`month`/`week` bucket values set the time
    range), `group[]` (`{ _id: <key> }`), `projection`, `columns` override,
    `options { limit, page, sort, totals_summary }`. Only the FIRST `anyOf`
    group is read. Every read is TIME-BOUNDED (default: last 30 days).
    Filter, column and sort definitions live in the shared report metadata
    under the types `object-detection-metrics` / `object-detection-segments`
    — seeded by `src/scripts/migrate-object-detection-analytics-data.js`
    (rerun-safe).

    **Query params ride along.** The same filter keys are accepted as query
    params on both GET and POST (that is how a grouped row's `drilldown` is
    spread back), plus `from_time` / `to_time`, `sortBy`, `export` and
    `emailSubject`. `per_page` / `page` on the query string are NOT honoured
    — paging comes from `options.limit` / `options.page` (the GET adapter
    therefore always uses the default page size).

    **Grouping & drilldown.** Group keys: metric, type, output, client,
    channel, rep, mission, business_day, week, month (+ segment on the
    segments view; type/output are metrics-view only). Grouped rows return
    `row_count`, `avg_score`, `avg_answer` (+ `avg_ratio` — the average share
    — and `main_rows` on segments; `flagged` on metrics) and a `drilldown`
    object holding ONLY filter-compatible ID/bucket values; spreading it as
    query params on the next call yields the detail rows.

    Export: `?export=excel` schedules the email/Excel flow and returns an
    acknowledgement instead of rows (`send_to_stream` is used internally by
    the scheduler). Read-only — get/update/patch/remove are rejected with
    400. Scoped by `company_namespace` (from the token). Admin-facing.
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /object-detection-analytics-report:
    get:
      summary: Legacy read (thin adapter onto the V2 POST)
      operationId: findObjectDetectionAnalyticsReport
      parameters:
        - in: query
          name: view
          schema:
            type: string
            enum: [metrics, segments]
            default: metrics
        - in: query
          name: group
          description: "Group keys — array or comma-separated string (e.g. `channel,month`)."
          schema:
            oneOf:
              - type: string
              - type: array
                items:
                  type: string
                  enum:
                    [
                      metric,
                      type,
                      output,
                      client,
                      channel,
                      rep,
                      mission,
                      segment,
                      business_day,
                      month,
                      week,
                    ]
        - $ref: "#/components/parameters/from_time"
        - $ref: "#/components/parameters/to_time"
        - $ref: "#/components/parameters/business_day"
        - $ref: "#/components/parameters/month"
        - $ref: "#/components/parameters/week"
        - $ref: "#/components/parameters/metric"
        - $ref: "#/components/parameters/type"
        - $ref: "#/components/parameters/output"
        - $ref: "#/components/parameters/flag"
        - $ref: "#/components/parameters/client"
        - $ref: "#/components/parameters/channel"
        - $ref: "#/components/parameters/rep"
        - $ref: "#/components/parameters/team"
        - $ref: "#/components/parameters/mission"
        - $ref: "#/components/parameters/segment"
        - $ref: "#/components/parameters/label"
        - $ref: "#/components/parameters/label_group"
        - $ref: "#/components/parameters/product_brand"
        - $ref: "#/components/parameters/product_category"
        - $ref: "#/components/parameters/sortBy"
        - $ref: "#/components/parameters/export"
        - $ref: "#/components/parameters/emailSubject"
      responses:
        "200":
          description: "Same shape as the POST response (default page size — `per_page` is not honoured). With `export`, the export acknowledgement instead."
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AnalyticsResponse"
                  - $ref: "#/components/schemas/ExportAck"
    post:
      summary: The V2 report read
      operationId: createObjectDetectionAnalyticsReport
      parameters:
        - $ref: "#/components/parameters/export"
        - $ref: "#/components/parameters/emailSubject"
        - $ref: "#/components/parameters/sortBy"
        - in: query
          name: view
          description: "Fallback when the body has no `view`."
          schema:
            type: string
            enum: [metrics, segments]
        - $ref: "#/components/parameters/from_time"
        - $ref: "#/components/parameters/to_time"
        - $ref: "#/components/parameters/business_day"
        - $ref: "#/components/parameters/month"
        - $ref: "#/components/parameters/week"
        - $ref: "#/components/parameters/metric"
        - $ref: "#/components/parameters/type"
        - $ref: "#/components/parameters/output"
        - $ref: "#/components/parameters/flag"
        - $ref: "#/components/parameters/client"
        - $ref: "#/components/parameters/channel"
        - $ref: "#/components/parameters/rep"
        - $ref: "#/components/parameters/team"
        - $ref: "#/components/parameters/mission"
        - $ref: "#/components/parameters/segment"
        - $ref: "#/components/parameters/label"
        - $ref: "#/components/parameters/label_group"
        - $ref: "#/components/parameters/product_brand"
        - $ref: "#/components/parameters/product_category"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AnalyticsBody"
      responses:
        "201":
          description: "Paginated rows + keys/columns/totals/sort_fields. With `export`, the export acknowledgement instead."
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AnalyticsResponse"
                  - $ref: "#/components/schemas/ExportAck"
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: api-key
    JwtAuth:
      type: apiKey
      in: header
      name: Authorization
  parameters:
    from_time:
      in: query
      name: from_time
      description: "ms epoch lower bound on the result's createdAt. Default window: the last 30 days."
      schema: { type: number }
    to_time:
      in: query
      name: to_time
      description: ms epoch upper bound (default now).
      schema: { type: number }
    business_day:
      in: query
      name: business_day
      description: "`YYYY-MM-DD` bucket (company time zone) — sets the whole time range; overrides from_time / to_time."
      schema: { type: string }
    month:
      in: query
      name: month
      description: "`YYYY-MM` bucket — sets the whole time range."
      schema: { type: string }
    week:
      in: query
      name: week
      description: "ISO week bucket `GGGG-Www` (e.g. `2026-W29`) — sets the whole time range."
      schema: { type: string }
    metric:
      in: query
      name: metric
      description: Metric definition id(s).
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    type:
      in: query
      name: type
      description: Metric type(s).
      schema:
        oneOf:
          - type: string
            enum:
              [
                adjacent_block,
                facings_count,
                on_shelf_availability,
                share_of_shelf,
              ]
          - type: array
            items: { type: string }
    output:
      in: query
      name: output
      description: Output family.
      schema:
        oneOf:
          - type: string
            enum: [compatibility, numerical, share_of_shelf]
          - type: array
            items: { type: string }
    flag:
      in: query
      name: flag
      description: Flagged results only (true) / unflagged only (false).
      schema: { type: boolean }
    client:
      in: query
      name: client
      description: Client id(s) of the scanned session.
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    channel:
      in: query
      name: channel
      description: Client-channel id(s).
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    rep:
      in: query
      name: rep
      description: Rep id(s) — the session creator.
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    team:
      in: query
      name: team
      description: Team id(s) on the client.
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    mission:
      in: query
      name: mission
      description: "Mission id(s) the session was STARTED FROM — narrows to mission-driven sessions."
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    segment:
      in: query
      name: segment
      description: Segment id(s) — segments view only (ignored on metrics).
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    label:
      in: query
      name: label
      description: "Label id(s) — rows whose metric INVOLVES any of them (args or resolved SOS segments)."
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    label_group:
      in: query
      name: label_group
      description: Label-group id(s) — resolved to the group's labels, then matched like `label`.
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    product_brand:
      in: query
      name: product_brand
      description: Product-brand id(s) — resolved to the labels linked to the brand.
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    product_category:
      in: query
      name: product_category
      description: Product-category id(s) — resolved to the labels linked to the category.
      schema:
        oneOf:
          - type: string
          - type: array
            items: { type: string }
    sortBy:
      in: query
      name: sortBy
      description: "Query-string sort (`sortBy[0][field]=score&sortBy[0][type]=desc`); takes precedence over `options.sort`. Accepted fields: _id, time, createdAt, score, answer, metric_name, client_name, channel_name, segment_name, segment_ratio, segment_answer, row_count, avg_score, avg_ratio, avg_answer."
      schema:
        type: array
        items:
          type: object
          properties:
            field: { type: string }
            type:
              type: string
              enum: [asc, desc]
    export:
      in: query
      name: export
      description: "`excel` schedules the emailed export instead of returning rows (the response is then the acknowledgement)."
      schema:
        type: string
        enum: [excel]
    emailSubject:
      in: query
      name: emailSubject
      description: Subject / name of the scheduled export email (default "Detection Analytics").
      schema: { type: string }
  schemas:
    AnalyticsBody:
      type: object
      properties:
        view:
          type: string
          enum: [metrics, segments]
          default: metrics
        anyOf:
          type: array
          description: "Filter groups — only the FIRST group is read; its criteria are AND-ed."
          items:
            type: object
            properties:
              criteria:
                type: array
                items:
                  type: object
                  required: [key]
                  properties:
                    key:
                      type: string
                      enum:
                        [
                          time,
                          createdAt,
                          metric,
                          type,
                          output,
                          flag,
                          client,
                          channel,
                          rep,
                          team,
                          mission,
                          segment,
                          label,
                          label_group,
                          product_brand,
                          product_category,
                          business_day,
                          month,
                          week,
                        ]
                    operator:
                      type: string
                      description: "in/nin/eq/ne for id & enum keys (default in); between/gte/lte or a preset for time / createdAt; ignored for label and bucket keys."
                      enum:
                        [
                          in,
                          nin,
                          eq,
                          ne,
                          between,
                          gte,
                          lte,
                          today,
                          yesterday,
                          last_seven_days,
                          last_thirty_days,
                          last_month,
                          last_three_months,
                          last_six_months,
                          last_twelve_months,
                        ]
                    value:
                      description: "Ids / strings / booleans (array or single). time between: [from_ms, to_ms]; gte/lte: ms; presets: omitted; buckets: one bucket string."
                      oneOf:
                        - type: array
                          items: {}
                        - type: string
                        - type: number
                        - type: boolean
        group:
          type: array
          description: 'Group keys, e.g. [{ _id: "channel" }, { _id: "month" }]. type/output are metrics-view only, segment is segments-view only.'
          items:
            type: object
            properties:
              _id:
                type: string
                enum:
                  [
                    metric,
                    type,
                    output,
                    client,
                    channel,
                    rep,
                    mission,
                    segment,
                    business_day,
                    month,
                    week,
                  ]
        projection:
          type: array
          description: Column keys to show (ungrouped rows only).
          items: { type: string }
        columns:
          type: array
          description: Optional column override (defaults come from the report metadata).
          items:
            $ref: "#/components/schemas/ReportColumn"
        options:
          type: object
          properties:
            limit:
              type: number
              default: 50
              description: "Rows per page — replaces the query-string `per_page`, which is not honoured."
            page: { type: number, default: 1 }
            sort:
              type: array
              description: "Fields must exist in the report's sort metadata (`sort_fields`), else the default applies (row_count desc grouped, time desc flat)."
              items:
                type: object
                properties:
                  field: { type: string }
                  type:
                    type: string
                    enum: [asc, desc]
            totals_summary:
              type: string
              enum: [all, page, none]
              default: none
    ReportColumn:
      type: object
      description: A shared report-metadata column definition.
      properties:
        _id: { type: string }
        key: { type: string }
        name: { type: string }
        position: { type: number }
        selectable: { type: boolean }
        show:
          type: string
          enum: [default, hide, show]
        default_show: { type: boolean }
        totals_key: { type: string }
        column_group: { type: string }
        report_types:
          type: array
          items: { type: string }
        disabled: { type: boolean }
    AnalyticsRow:
      type: object
      description: "Flat rows carry the result fields (segments view adds the segment_* fields); grouped rows carry drilldown + row_count/avg_* + the grouped identity names / buckets."
      properties:
        _id:
          type: string
          description: Metric result id (flat rows only).
        time: { type: number, description: "Result creation time (ms)." }
        business_day:
          { type: string, description: "YYYY-MM-DD (company time zone)." }
        timestamp: { type: string, description: "YYYY-MM-DD HH:mm:ss." }
        month: { type: string, description: "YYYY-MM." }
        week: { type: string, description: "ISO week GGGG-Www." }
        metric_id: { type: string }
        metric_name: { type: string }
        metric_type:
          type: string
          enum:
            [
              adjacent_block,
              facings_count,
              on_shelf_availability,
              share_of_shelf,
            ]
        output:
          type: string
          enum: [compatibility, numerical, share_of_shelf]
        flag: { type: boolean }
        score: { type: number, description: "0..1, rounded to 4 decimals." }
        client_id: { type: string }
        client_name: { type: string }
        channel_id: { type: string }
        channel_name: { type: string }
        rep_id: { type: string }
        rep_name: { type: string }
        mission_id:
          type: string
          description: The mission the session was STARTED FROM — absent on generic scans.
        mission_name: { type: string }
        teams_ids:
          type: array
          items: { type: string }
        session_id: { type: string }
        analysis_id: { type: string }
        answer:
          description: "metrics view — effective answer (confirmed override wins): boolean for compatibility, number otherwise."
          oneOf:
            - type: boolean
            - type: number
          nullable: true
        ratio: { type: number, description: "metrics view — effective ratio." }
        segment_id: { type: string, description: segments view. }
        segment_name: { type: string, description: segments view. }
        is_main:
          type: string
          enum: [main, context]
          description: "segments view — main = the row the target is defined for."
        segment_answer:
          {
            type: number,
            description: "segments view — measured quantity (2 decimals).",
          }
        segment_ratio:
          {
            type: number,
            description: "segments view — the segment's share 0..1.",
          }
        target_ratio:
          { type: number, description: "segments view — main rows only." }
        target_answer:
          { type: number, description: "segments view — main rows only." }
        segment_score:
          { type: number, description: "segments view — main rows only." }
        measure:
          type: string
          enum: [width_cm, area_cm2, facings]
        drilldown:
          type: object
          description: "grouped rows — filter-compatible ids / buckets keyed by the group keys (metric, type, output, client, channel, rep, mission, segment, business_day, month, week)."
          additionalProperties: { type: string }
        row_count: { type: number, description: grouped rows. }
        avg_score: { type: number, description: grouped rows. }
        avg_answer: { type: number, description: grouped rows. }
        avg_ratio:
          {
            type: number,
            description: "grouped rows, segments view — average share.",
          }
        main_rows: { type: number, description: "grouped rows, segments view." }
        flagged: { type: number, description: "grouped rows, metrics view." }
        type:
          type: string
          description: "grouped by type — the metric-type bucket."
      additionalProperties: true
    AnalyticsResponse:
      type: object
      properties:
        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 }
        data:
          type: array
          items:
            $ref: "#/components/schemas/AnalyticsRow"
        keys:
          type: array
          description: Table columns for the current state (grouped vs flat).
          items:
            type: object
            properties:
              key: { type: string }
              value: { type: string, description: Translated label. }
              type:
                type: string
                enum: [string, number]
              visible: { type: boolean }
        columns:
          type: array
          description: "The report's column metadata sorted by position (or the `columns` override echoed back)."
          items:
            $ref: "#/components/schemas/ReportColumn"
        totals:
          type: object
          description: "When options.totals_summary is all (absolute_total) / page (page_total): Rows, Average Score (+ Average Share on segments)."
          properties:
            absolute_total:
              type: object
              additionalProperties: { type: number }
            page_total:
              type: object
              additionalProperties: { type: number }
            labels:
              type: array
              items:
                type: object
                properties:
                  key: { type: string }
                  value: { type: string }
        sort_fields:
          type: array
          description: The report's sort metadata rows — the valid options.sort fields.
          items:
            type: object
            properties:
              _id: { type: string }
              key: { type: string }
              name: { type: string }
              selectable: { type: boolean }
              report_types:
                type: array
                items: { type: string }
    ExportAck:
      type: object
      description: "Returned when `export` is set — the report was queued to be emailed."
      properties:
        _id: { type: string }
        success: { type: boolean }
        msg: { type: string }
        isExport: { type: boolean }
