# ListOpenShifts

## Overview

listOpenShifts returns live slots in a `CONFIRMED` shift schedule that no `ACTIVE` placement staffs, paginated. It is the query behind the "unfilled demand slots" view that drives open-shift advertising and shift schedule gap-filling.

Openness is an **anti-join and nothing else**: a Shift names no worker, so the only record of who staffs a slot is `ShiftPlacement`. This is deliberate — while openness was also expressed as a null column on the Shift, the column and the anti-join could disagree, and a slot someone had already been placed on kept being advertised.

## Business Rules

- Accepts an optional date range and/or `siteId`, and pagination parameters
- A shift is open when **all** of the following hold: its ShiftSchedule is `CONFIRMED`, its own `cancelledAt` is null, and no `ShiftPlacement` with `status = ACTIVE` references it
- A slot in a `DRAFT` shift schedule is not advertisable yet — it is a plan, not a commitment
- A placement removes the shift from this list the moment it is created, and releasing or superseding that placement puts the shift back on it (a `CANCELLED` or `SUPERSEDED` row does not staff anything)
- A withdrawn slot (`cancelledAt` set) never appears, whatever its placements
- When a date range is given, filters to shifts whose `date` falls within it
- When `siteId` is given, filters to shifts at that Site
- Results are paginated, ordered by `date` ascending **by default**; `orderBy` / `orderDirection` override the sort field (`date`, `plannedStartAt`, `createdAt`, `id`), and `id` is always appended as a tiebreaker so paging is stable across rows sharing the sort value

## Process Flow

```mermaid
flowchart TD
    A[Receive date range, siteId, pagination params] --> B[Join Shift to its ShiftSchedule]
    B --> C[Keep shiftSchedules with status CONFIRMED and shifts with cancelledAt null]
    C --> D[Exclude shifts having any ACTIVE ShiftPlacement]
    D --> E{Date range provided?}
    E -- Yes --> F[Filter date within range]
    E -- No --> G[Skip date filter]
    F --> H{siteId provided?}
    G --> H
    H -- Yes --> I[Filter by siteId]
    H -- No --> J[Skip site filter]
    I --> K[Order by the requested field, default date, then id, and paginate]
    J --> K
    K --> L[Return open shifts]
```

## External Dependencies

- [shiftSchedule::ShiftSchedule](../model/ShiftSchedule.md) model - supplies the CONFIRMED filter; committed-ness is the shift schedule's, not the shift's
- [shiftSchedule::ShiftPlacement](../model/ShiftPlacement.md) model - the anti-join that defines openness

## Error Scenarios

- **INVALID_DATE_RANGE**: endDate precedes startDate
- **INVALID_PAGINATION**: pagination cursor or limit is malformed

## Test Cases

- keys openness on the absence of an ACTIVE placement, not on any field of the Shift
- only lists slots whose shiftSchedule is CONFIRMED
- excludes a withdrawn slot
- filters shifts to the given date range and siteId when provided
- paginates results according to the given limit and cursor
- throws INVALID_DATE_RANGE when endDate precedes startDate
- throws INVALID_PAGINATION when limit is not a positive integer
