# ReleaseShiftPlacement

## Permission Scope

shiftSchedule

## Overview

releaseShiftPlacement unstaffs a slot by moving the placement to `CANCELLED`. It replaces the former `deleteShiftPlacement`: a placement is never physically removed, because "X was planned on this slot until it was released" is exactly the fact a confirmed shift schedule has to be able to answer afterwards.

## Business Rules

- The placement must exist and be `ACTIVE`; a `SUPERSEDED` or `CANCELLED` placement rejects this command
- The row is not deleted — `status` becomes `CANCELLED` and `releasedAt` is stamped
- `supersededById` stays null: a release has no successor, which is what distinguishes it from a swap
- Releasing leaves the slot open again — it now has no `ACTIVE` placement, which is exactly what `listOpenShifts` keys on
- Use `swapShiftPlacement` when a replacement is known; releasing and re-placing as two calls leaves the slot momentarily unstaffed and loses the link between the two placements

## Process Flow

```mermaid
flowchart TD
    A[Receive placement id] --> B{Placement exists?}
    B -->|No| BX[Return error: SHIFT_PLACEMENT_NOT_FOUND]
    B -->|Yes| C{status = ACTIVE?}
    C -->|No| CX[Return error: SHIFT_PLACEMENT_NOT_ACTIVE]
    C -->|Yes| D[Set status = CANCELLED and stamp releasedAt]
    D --> E[Return released placement]
```

## External Dependencies

- [shiftSchedule::ShiftPlacement](../model/ShiftPlacement.md) model - the stateful placement this command releases

## Error Scenarios

- **SHIFT_PLACEMENT_NOT_FOUND**: no ShiftPlacement exists for the given id
- **SHIFT_PLACEMENT_NOT_ACTIVE**: the placement is not ACTIVE

## Test Cases

- sets status CANCELLED and stamps releasedAt without deleting the row
- leaves supersededById null, which is what distinguishes a release from a swap
- throws SHIFT_PLACEMENT_NOT_ACTIVE when the placement was already released
- throws SHIFT_PLACEMENT_NOT_ACTIVE when the placement was superseded by a swap
- throws SHIFT_PLACEMENT_NOT_FOUND when the placement does not exist
