openapi: 3.0.3
info:
  title: Repzo API - Day Shift
  version: 1.0.0
  description: |
    **Day shifts** are reusable weekly work schedules assigned to sales reps.
    Each shift holds a `schedule[]` of per-weekday entries
    (`{ day, work_time[] }`); every `work_time` range is a `HH:mm` wall-clock
    interval in the company time zone, and a range whose `to <= from` crosses
    midnight into the next calendar date (e.g. `18:00 → 02:00`).

    **Why it matters.** Repzo keys all daily data on a **business-day string**
    (`"2026-07-14"`), derived from a per-company `end_of_day` cut. A shift acts
    as a **per-weekday override of that cut**: yesterday's overnight spill can
    raise the boundary (so early-morning activity still belongs to the prior
    working day) and today's early shift start can pull it down. When a rep has
    **no shift assigned**, business-day resolution is byte-identical to the
    legacy company `end_of_day` behaviour. See the shift-aware resolver in
    `src/util.ts` (`resolveBusinessDay`, `findDay`, `findTimeFrame`).

    **Assignment.** A rep is assigned a shift through the rep's own
    `assigned_shift` field (see the Rep service). The resolved schedule is
    snapshotted onto the rep's `day` document at day-open — together with the
    business-day context it was stamped with (`EOD`, `timeZone`) — and every
    business-day stamping site (visits, activities, day close/recalc,
    transactional documents) resolves against that full snapshot while the
    day is open: neither a mid-day shift reassignment / schedule edit nor an
    owner changing the company `end_of_day` / `time_zone` moves an in-flight
    day's boundaries or stamps new documents onto a different business day
    than the open day. The rep's current shift and the namespace's current
    settings only apply when no day is open (i.e. from the next day-open
    onward).

    **Who calls it.** Admins manage shifts from the back-office UI. Reps are
    notified of changes via the `update-day-shift` command (real-time push).

    **Multi-tenancy & lifecycle.** Records are scoped by `company_namespace[]`
    (server-injected from session). Soft-delete via `disabled: true`; deletion
    is rejected while any active rep's `assigned_shift` still points at the
    shift. `designation` is an optional free-text label. `name` is unique per
    namespace among active rows (compound unique index
    `(company_namespace, name)`, partial on `disabled: false`).

    **Validation.** On create/update the `schedule` is validated so business-day
    resolution stays well-defined: each weekday appears at most once; every
    range has a valid, non-equal `from`/`to`; per day, ranges are
    non-overlapping and only the **last** range may cross midnight; a day's
    overnight tail must not overlap the next weekday's earliest start; and,
    against the namespace's `end_of_day`, no weekday's business day may be
    eliminated — a shift whose end reaches or passes the FOLLOWING weekday's
    end-of-day cut while that weekday has no ranges (e.g. `end_of_day` 12:00
    with Monday 22:00→13:00 and Tuesday off) is rejected, since the label in
    between could never own a single instant.

    **Key relationships.** Referenced by `representatives` via the rep's
    `assigned_shift` field.

    **Events.** Create / update / remove emit `update-day-shift`, which notifies
    reps in the namespace. Bulk `patch` is not supported.
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /day-shift:
    get:
      summary: Find day shifts
      operationId: findDayShifts
      parameters:
        - in: query
          name: _id
          description: |
            "Filter by shift `_id`. Pass once for a single match, or as
            `?_id[]=...&_id[]=...` for multiple."
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: name
          description: |
            Exact-match on shift `name` **or** `designation` (the value is
            expanded to match either field).
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: designation
          description: Exact-match on shift `designation`.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: disabled
          description: Include disabled (soft-deleted) shifts. Defaults to `false`.
          schema: { type: boolean, default: false }
        - in: query
          name: from_updatedAt
          description: |
            Cursor — return only shifts with `updatedAt` greater than this Unix
            timestamp (ms). Used by sync clients.
          schema: { type: number }
        - in: query
          name: inject_assigned_reps
          description: |
            When truthy, each returned shift is enriched with an
            `assigned_reps[]` array (`_id`, `name`) of the active reps whose
            `assigned_shift` points at it.
          schema: { type: boolean, default: false }
        - in: query
          name: from__id
          description: Cursor — return records with `_id` greater than this value.
          schema: { type: string }
        - in: query
          name: to__id
          description: Cursor — return records with `_id` less than this value.
          schema: { type: string }
        - 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
        - in: query
          name: sortBy
          description: |
            Sort directives. Encoded with `qs` bracket notation, e.g.
            `?sortBy[0][field]=_id&sortBy[0][type]=desc`.
          schema:
            type: array
            items:
              type: object
              properties:
                field:
                  type: string
                  enum: [_id]
                type:
                  type: string
                  enum: [asc, desc]
      responses:
        "200":
          description: Paginated list of shifts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShiftFindResult"
    post:
      summary: Create a day shift
      description: |
        Creates a shift. The `schedule` is validated (see the service
        description). Emits `update-day-shift`.
      operationId: createDayShift
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ShiftCreateBody"
      responses:
        "201":
          description: The newly-created shift document.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShiftSchema"
  /day-shift/{id}:
    get:
      summary: Get a day shift by id
      operationId: getDayShift
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The shift document for the given `_id`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShiftSchema"
    put:
      summary: Update a day shift
      description: |
        Standard put. When `schedule` is present it is validated. Setting
        `disabled: true` soft-deletes the shift and — like DELETE — is
        rejected with a `BadRequest` while any active rep's `assigned_shift`
        still points at it. Emits `update-day-shift`.
      operationId: updateDayShift
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ShiftUpdateBody"
      responses:
        "200":
          description: The shift document after the update is applied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShiftSchema"
    delete:
      summary: Soft-delete a day shift
      description: |
        Soft-deletes the shift (`disabled: true`). Rejected with a
        `BadRequest` while any active rep's `assigned_shift` still points at
        it. Emits `update-day-shift`.
      operationId: removeDayShift
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: "The shift document after soft-deletion (`disabled: true`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShiftSchema"
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:
    ShiftRange:
      type: object
      description: |
        A single work interval in `HH:mm` wall-clock time (company time zone).
        A range whose `to <= from` crosses midnight into the next calendar date.
      required:
        - from
        - to
      properties:
        from:
          type: string
          pattern: "^([01]\\d|2[0-3]):[0-5]\\d$"
          example: "18:00"
        to:
          type: string
          pattern: "^([01]\\d|2[0-3]):[0-5]\\d$"
          example: "02:00"
    ShiftEntry:
      type: object
      description: One weekday's work_time ranges.
      required:
        - day
        - work_time
      properties:
        day:
          type: string
          enum: [sunday, monday, tuesday, wednesday, thursday, friday, saturday]
        work_time:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/ShiftRange"
    ShiftSchema:
      type: object
      description: Shift document.
      properties:
        _id:
          type: string
          description: Unique identifier for the shift.
        name:
          type: string
          description: Display name.
        designation:
          type: string
          description: Optional free-text label for the shift.
        disabled:
          type: boolean
          description: Soft-delete flag.
        schedule:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/ShiftEntry"
          description: Weekly work schedule (per-weekday work_time ranges).
        total_working_hours:
          type: number
          description: |
            Server-computed total working hours across the schedule
            (cross-midnight ranges counted correctly). Derived on write.
        total_working_days:
          type: number
          description: |
            Server-computed count of weekdays with at least one work_time
            range. Derived on write.
        company_namespace:
          type: array
          items: { type: string }
          description: Tenant key. Server-injected — never accept from clients.
        createdAt:
          type: string
          format: date-time
          description: Creation timestamp.
        updatedAt:
          type: string
          format: date-time
          description: Last update timestamp.
    ShiftCreateBody:
      type: object
      description: |
        Body for creating a shift. The tenant key (`company_namespace`) is
        optional for SDK callers and is otherwise injected from the caller's
        session. `schedule` is validated on write.
      required:
        - name
        - schedule
      properties:
        name:
          type: string
          description: Display name.
        designation:
          type: string
          description: Optional free-text label for the shift.
        schedule:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/ShiftEntry"
        company_namespace:
          type: array
          items: { type: string }
          description: Optional tenant namespace override for SDK callers.
    ShiftUpdateBody:
      type: object
      description: |
        Body for updating a shift. The tenant key (`company_namespace`) is
        derived from the caller's session — do not send it. When `schedule` is
        present it is validated. Set `disabled: true` to soft-delete.
      properties:
        name:
          type: string
        designation:
          type: string
        schedule:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/ShiftEntry"
        disabled:
          type: boolean
          description: |
            Soft-delete flag. Set to `true` to disable the shift — rejected
            while any active rep is still assigned to it.
    ShiftFindResult:
      type: object
      description: Standard paginated result envelope.
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ShiftSchema"
        total_result:
          type: number
          description: Total number of shifts matching the filter.
        current_count:
          type: number
          description: Count of shifts on the current page.
        total_pages:
          type: number
          description: Total number of pages.
        current_page:
          type: number
          description: Current page number.
        per_page:
          type: number
          description: Number of shifts per page.
        first_page_url:
          type: string
          description: URL for the first page.
        last_page_url:
          type: string
          description: URL for the last page.
        next_page_url:
          type: string
          nullable: true
          description: URL for the next page.
        prev_page_url:
          type: string
          nullable: true
          description: URL for the previous page.
        path:
          type: string
          description: Base URL path.
