openapi: 3.0.3
info:
  title: Repzo API - Geo Zone
  version: 1.0.0
  description: |
    A **Geo Zone** is a named geographic area defined by a single GeoJSON
    polygon. Zones are assigned to reps (via the rep's `assigned_geo_zones`
    array — currently capped at one zone per rep) so the mobile app can
    enforce location policies — e.g. whether a rep may start the day or
    start a visit while outside their zone.

    **Who uses it.** Admins manage zones from the back office and assign
    them through the `rep` service. The mobile app reads zones (with the
    incremental `from_updatedAt` cursor) to evaluate policies offline.

    **Multi-tenancy & lifecycle.** Records are scoped by
    `company_namespace[]` (injected from session — never sent in the body)
    and use **soft-delete** via `disabled: true`. `name` is unique per
    namespace among non-deleted zones. A zone that is still assigned to one
    or more reps **cannot be disabled or deleted** — the request is rejected
    with `400` naming the assigned reps; reassign them first.

    **Validation.** A zone cannot be saved without a `name` and a valid
    polygon: the polygon needs at least one closed linear ring of 4+
    `[longitude, latitude]` positions (first and last must match, longitude
    in ±180, latitude in ±90). Violations return a `400` naming the failing
    ring.

    **Key relationships.** Referenced by `rep.assigned_geo_zones`. The
    `editor` audit object (who created / last updated the zone) is stamped
    server-side from the session token.
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /geo-zone:
    get:
      summary: Find geo zones
      operationId: findGeoZones
      parameters:
        - in: query
          name: _id
          description: Filter by zone `_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 zone name. Pass once or as `?name[]=...` for multiple.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: search
          description: Case-insensitive substring search on `name`.
          schema: { type: string }
          example: north
        - in: query
          name: disabled
          description: Include disabled (soft-deleted) zones. Defaults to `false`.
          schema: { type: boolean, default: false }
        - in: query
          name: inject_assigned_reps
          description: |
            When truthy, each returned zone is enriched with an
            `assigned_reps[]` array (`_id`, `name`) of the active reps whose
            `assigned_geo_zones` contains it.
          schema: { type: boolean, default: false }
        - in: query
          name: from_updatedAt
          description: Cursor — zones updated on or after this Unix timestamp (ms). Used by the mobile app for incremental sync.
          schema: { type: number }
        - in: query
          name: to_updatedAt
          description: Cursor — zones updated on or before this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: from_createdAt
          description: Cursor — zones created on or after this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: to_createdAt
          description: Cursor — zones created on or before this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: per_page
          description: Page size. Defaults to the server's configured pagination limit.
          schema: { type: integer, minimum: 1, maximum: 500 }
          example: 50
        - in: query
          name: page
          description: 1-indexed page number.
          schema: { type: integer, minimum: 1 }
          example: 1
      responses:
        "200":
          description: A paginated list of geo zones matching the filter.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GeoZoneFindResult"
    post:
      summary: Create a geo zone
      operationId: createGeoZone
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GeoZoneCreateBody"
      responses:
        "201":
          description: The newly-created geo zone. `editor` is stamped from the session token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GeoZoneSchema"
        "400":
          description: |
            Validation failure — missing `name`, missing polygon, or an
            invalid polygon (open ring, fewer than 4 positions, out-of-range
            coordinates). The message names the failing ring. Also returned
            when the zone name already exists in the namespace.
  /geo-zone/{id}:
    get:
      summary: Get a geo zone by ID
      operationId: getGeoZone
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: query
          name: inject_assigned_reps
          description: When truthy, the zone is enriched with an `assigned_reps[]` array (`_id`, `name`).
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: The geo zone document for the given `_id`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GeoZoneSchema"
    put:
      summary: Update a geo zone
      operationId: updateGeoZone
      description: |
        Partial update — only the fields present in the body are changed.
        `polygon`, when sent, replaces the zone's polygon and is validated
        like create. Setting `disabled: true` deactivates the zone and is
        **rejected with `400` while any rep is still assigned to it**;
        reassign those reps first.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GeoZoneUpdateBody"
      responses:
        "200":
          description: The geo zone document after the update is applied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GeoZoneSchema"
        "400":
          description: Zone not found, polygon validation failure, or the zone is being disabled while reps are still assigned to it.
    delete:
      summary: Remove a geo zone (soft-delete)
      operationId: removeGeoZone
      description: |
        Soft-deletes the zone (`disabled: true`). Rejected with `400` while
        any rep is still assigned to it — the error names the assigned reps;
        reassign them to another zone first.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: "The geo zone document after soft-deletion (`disabled: true`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GeoZoneSchema"
        "400":
          description: Zone not found, or the zone is still assigned to one or more reps.
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:
    GeoZoneFindResult:
      type: object
      description: Paginated list of geo zones.
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/GeoZoneSchema"
        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 }
    GeoZoneSchema:
      type: object
      description: Geo zone document as stored.
      properties:
        _id: { type: string }
        name:
          {
            type: string,
            description: Unique per namespace among non-deleted zones.,
          }
        description: { type: string }
        polygon:
          $ref: "#/components/schemas/GeoZonePolygon"
        disabled:
          { type: boolean, description: Soft-delete / deactivation flag. }
        editor:
          $ref: "#/components/schemas/GeoZoneEditor"
        assigned_reps:
          type: array
          description: "Present only when the request sent `inject_assigned_reps=true` — the active reps assigned to this zone."
          items:
            type: object
            properties:
              _id: { type: string }
              name: { type: string }
        company_namespace:
          type: array
          items: { type: string }
          description: Tenant key set on save from session.
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        __v: { type: number }
    GeoZonePolygon:
      type: object
      description: |
        A GeoJSON Polygon. `coordinates` is an array of linear rings; the
        first ring is the outer boundary, any further rings are holes.
      required: [coordinates]
      properties:
        type:
          type: string
          enum: [Polygon]
          default: Polygon
        coordinates:
          type: array
          minItems: 1
          description: |
            Array of linear rings. Each ring is an array of at least 4
            positions and must be closed (first position equals the last).
            Positions are ordered as **longitude, latitude** (longitude
            first) — longitude in [-180, 180], latitude in [-90, 90].
          items:
            type: array
            minItems: 4
            description: "A closed linear ring of `[longitude, latitude]` positions."
            items:
              type: array
              minItems: 2
              maxItems: 2
              items: { type: number }
          example:
            - - [35.910, 31.954]
              - [35.930, 31.954]
              - [35.930, 31.970]
              - [35.910, 31.954]
    GeoZoneEditor:
      type: object
      description: Audit stamp of who created / last updated the zone. Set server-side from the session token — never sent by the client.
      properties:
        _id: { type: string }
        type: { type: string, enum: [admin, rep, tenant, client] }
        name: { type: string }
        rep: { type: string }
        admin: { type: string }
        client: { type: string }
        tenant: { type: string }
    GeoZoneCreateBody:
      type: object
      description: |
        Body for creating a geo zone. The `editor` audit stamp is stamped
        server-side from the session token (not accepted on create). The tenant
        key (`company_namespace`) is normally injected from session and can be
        omitted — it is optional here for integration / cross-namespace callers.
        The soft-delete flag (`disabled`) defaults to `false`.
      required:
        - name
        - polygon
      properties:
        name:
          type: string
          description: Zone name — required and unique per namespace among non-deleted zones.
        description: { type: string }
        polygon:
          $ref: "#/components/schemas/GeoZonePolygon"
        company_namespace:
          type: array
          items: { type: string }
          description: Tenant key — normally injected from session; optional, for cross-namespace callers.
    GeoZoneUpdateBody:
      type: object
      description: |
        Body for updating a geo zone — only the fields present are changed.
        The tenant key (`company_namespace`) is derived from session. The
        `editor` audit stamp is normally refreshed server-side but may be
        provided (optional).
      properties:
        name: { type: string }
        description: { type: string }
        polygon:
          $ref: "#/components/schemas/GeoZonePolygon"
        disabled:
          type: boolean
          description: "Soft-delete / deactivation flag. Setting `true` is rejected while any rep is still assigned to this zone."
        editor:
          $ref: "#/components/schemas/GeoZoneEditor"
