# CreateShiftPlacements

## Permission Scope

shiftSchedule

## Overview

createShiftPlacements bulk-creates many `ShiftPlacement` placements in a single all-or-nothing transaction, each stamped with `provenance = GENERATED`. It is the counterpart to `createShifts`: generation logic (e.g. a periodic-rotation shift schedule generator) produces a batch of Shifts and then places Assignments onto them in bulk, rather than issuing one `createShiftPlacement` call per placement.

## Business Rules

- The placement is created `ACTIVE`, with `supersededById` and `releasedAt` null; retiring it is `releaseShiftPlacement` (CANCELLED) or `swapShiftPlacement` (SUPERSEDED), never a delete
- Every referenced Assignment must be effective on the date of each slot it is placed on; each Assignment is read once for the batch rather than once per entry, and one ineffective pairing rejects the whole batch

- Accepts an array of `{ shiftId, assignmentId }` entries
- The input array must be non-empty and must not exceed the batch size limit (5,000 entries)
- Validation runs for **every** entry before any row is inserted; if **any** entry fails validation, the whole batch is rejected and **nothing** is inserted (all-or-nothing)
- Each entry is validated with the same rules as `createShiftPlacement`: the target Shift must exist and must not be withdrawn (`cancelledAt` null)
- No entry may duplicate an existing ACTIVE placement for its `(shiftId, assignmentId)` pair, and the batch may not contain the same pair twice — both reject the whole batch
- Every inserted ShiftPlacement has `provenance = GENERATED` (unlike `createShiftPlacement`, which defaults to `MANUAL`) and `assignedAt` set to the same transaction timestamp
- Referenced Shifts are batch-loaded once (not once per entry) to keep the transaction efficient at scale

## Process Flow

```mermaid
flowchart TD
    A[Receive array of shiftId,assignmentId pairs] --> B{Array non-empty and<br/>within batch size limit?}
    B -->|No| BX[Return error: BULK_INPUT_EMPTY / BULK_INPUT_TOO_LARGE]
    B -->|Yes| C[Batch-load referenced Shifts]
    C --> D{Every referenced Shift exists<br/>and is PLANNED or PUBLISHED?}
    D -->|No| DX[Return error: SHIFT_NOT_FOUND / SHIFT_CANCELLED<br/>from the first invalid entry; insert nothing]
    D -->|Yes| E[Insert all ShiftPlacements<br/>with provenance = GENERATED]
    E --> F[Return created placements]
```

## External Dependencies

- [shiftSchedule::Shift](../model/Shift.md) model - batch-loaded; every referenced Shift must be PLANNED or PUBLISHED
- Shares validation logic with [CreateShiftPlacement](./CreateShiftPlacement.md) (same module-internal functions, not duplicated)

## Error Scenarios

- **MISSING_REQUIRED_FIELD**: a required field is missing
- **ASSIGNMENT_NOT_EFFECTIVE**: the referenced Assignment does not exist or is not effective on the relevant date
- **BULK_INPUT_EMPTY**: the input array is empty
- **BULK_INPUT_TOO_LARGE**: the input array exceeds the batch size limit
- **SHIFT_NOT_FOUND**: no Shift exists for the given id
- **SHIFT_CANCELLED**: the target Shift is CANCELLED
- **SHIFT_PLACEMENT_ALREADY_EXISTS**: an ACTIVE ShiftPlacement already places this Assignment on this Shift

## Test Cases

- throws SHIFT_PLACEMENT_ALREADY_EXISTS when two entries in the batch share a shift and assignment
- throws SHIFT_PLACEMENT_ALREADY_EXISTS when an entry duplicates an existing ACTIVE placement

- creates multiple ShiftPlacements in one call, all with provenance GENERATED
- rejects the entire batch when one Assignment does not cover its slot's date
- rejects the entire batch and inserts nothing when one entry among many targets a CANCELLED shift
- throws BULK_INPUT_EMPTY when the assignments array is empty
- throws BULK_INPUT_TOO_LARGE when the assignments array exceeds the batch size limit
- throws SHIFT_NOT_FOUND identifying which entry failed when a later entry references a missing shift
- throws MISSING_REQUIRED_FIELD when the assignments array itself is missing
