# ListShiftPlacementsByAssignment

## Overview

listShiftPlacementsByAssignment returns where a given workforce Assignment is placed over a date range, paginated. It supports a worker's personal schedule view and coverage audits (e.g. confirming every shift a person covered, including 応援 and multi-site placements).

## Business Rules

- Accepts an `assignmentId`, an optional date range (matched against the parent Shift's `date`), and pagination parameters
- Returns all ShiftPlacements whose `assignmentId` matches, including direct placements, open-shift fills, and 応援/multi-site placements where the Assignment's home post differs from the shift's Site
- When a date range is given, filters to placements whose parent Shift's `date` falls within it
- Results are paginated, ordered by the parent shift's `date` ascending **by default**; `orderBy` / `orderDirection` override the sort field (`date`, `assignedAt`, `id`), and `id` is always appended as a tiebreaker so paging is stable across placements sharing the sort value

## Process Flow

```mermaid
flowchart TD
    A[Receive assignmentId, date range, pagination params] --> B[Filter ShiftPlacement by assignmentId]
    B --> C{Date range provided?}
    C -- Yes --> D[Join parent Shift and filter date within range]
    C -- No --> E[Skip date filter]
    D --> F[Sort by the requested field, default Shift.date, then id]
    E --> F
    F --> G[Apply pagination]
    G --> H[Return paginated list of placements]
```

## External Dependencies

- [workforce::Assignment](../model/ShiftPlacement.md) — the Assignment whose placements are listed (cross-module reference resolved via `assignmentId`)
- [shiftSchedule::Shift](../model/Shift.md) — parent shift resolved to apply the date range filter

## Error Scenarios

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

- An effective placement is `status = ACTIVE` **and** on a slot that is not withdrawn (`Shift.cancelledAt` null); withdrawing a shift does not touch its placements, so the slot check is what keeps a cancelled shift out of someone's current schedule
- `includeInactive` opts back into the full history (superseded, released, and placements on withdrawn slots)

## Test Cases

- returns all ShiftPlacements for a given assignmentId
- includes a 応援/multi-site placement whose home Site differs from the shift's Site
- filters placements to the given date range when provided
- returns an empty page for an assignment with no placements
- 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
- excludes a placement whose slot has been withdrawn
- includeInactive brings back the full history, withdrawn slots included
