# CancelShift

## Permission Scope

shift

## Overview

cancelShift withdraws a single slot by stamping `cancelledAt`. It is the one thing the shift-schedule-level status cannot express — "the period is confirmed but this one night shift is no longer needed" — and it works whether the parent ShiftSchedule is `DRAFT` or `CONFIRMED`. Withdrawal is terminal: a withdrawn shift is immutable and accepts no placements.

## Business Rules

- A live shift (`cancelledAt` null) may be withdrawn; a shift already withdrawn rejects this command (terminal)
- Withdrawal works regardless of the parent ShiftSchedule's status — a `CONFIRMED` period can lose one slot without being un-confirmed
- Cancellation does not delete the shift or its segments; the row is kept for audit / historical query, the same discipline ShiftPlacement follows
- Once withdrawn, the shift accepts no new `ShiftPlacement` placements
- Existing placements on a withdrawn shift are not automatically released by this command; unstaffing is a separate concern (`releaseShiftPlacement`)

## Process Flow

```mermaid
flowchart TD
    A[Receive shiftId] --> B{Shift exists?}
    B -->|No| BX[Return error: SHIFT_NOT_FOUND]
    B -->|Yes| C{cancelledAt is null?}
    C -->|No| CX[Return error: SHIFT_ALREADY_CANCELLED]
    C -->|Yes| D[Stamp cancelledAt]
    D --> E[Return withdrawn shift]
```

## External Dependencies

- [shiftSchedule::Shift](../model/Shift.md) model - the slot this command withdraws
- [shiftSchedule::ShiftPlacement](../model/ShiftPlacement.md) model - placements referencing this shift are not accepted once it becomes CANCELLED

## Error Scenarios

- **SHIFT_NOT_FOUND**: no Shift exists for the given id
- **SHIFT_ALREADY_CANCELLED**: the slot has already been withdrawn (cancelledAt is set)

## Test Cases

- cancelShift stamps cancelledAt on a live slot instead of deleting it
- withdraws a single slot out of an already CONFIRMED shiftSchedule
- throws SHIFT_ALREADY_CANCELLED when the slot was already withdrawn
- throws SHIFT_NOT_FOUND when the shift does not exist
- a cancelled shift accepts no new ShiftPlacement placements afterward
