# UpdateShift

## Permission Scope

shift

## Overview

updateShift adjusts a shift's classification, site, or its embedded `segments` array while its ShiftSchedule is still `DRAFT`. Once the shift schedule is `CONFIRMED` the period is committed to workers and its slots cannot be freely edited — changing a slot then means withdrawing it (`cancelShift`) and adding a new one. Staffing is not editable here at all: that is `ShiftPlacement`.

## Business Rules

- Only a shift whose ShiftSchedule is `DRAFT` may be updated; a `CONFIRMED` shift schedule's shift rejects this command, and a withdrawn shift rejects it too
- The parent ShiftSchedule row is locked while the check and the update run, so a concurrent `confirmShiftSchedule` cannot commit the period in between and let the edit through
- After any segment change, the shift must still have **at least one** segment and satisfy: `SPLIT` requires two or more segments with positive gaps; `NORMAL` / `THROUGH` / `ON_CALL` require exactly one; `SUPPORT` may have one or more
- `shiftType` may be changed only if consistent with the resulting segment count
- `plannedStartAt`/`plannedEndAt` are recomputed as the envelope of the (possibly updated) segments — they are never set independently
- `date` changes must remain consistent with the first segment's start
- `siteId` may be changed to or from null for multi-site placement
- Staffing cannot be changed here at all: who works a slot is its `ShiftPlacement`, which stays changeable even after the shift schedule is confirmed
- Segment-level edits must preserve: positive gaps between consecutive segments, `breakMinutes` strictly less than gross span, and contiguous 1-based `sequence`

## Process Flow

```mermaid
flowchart TD
    A[Receive shiftId,<br/>optional shiftType/siteId/segments] --> B{Shift exists?}
    B -->|No| BX[Return error: SHIFT_NOT_FOUND]
    B -->|Yes| C{shiftSchedule status = DRAFT?}
    C -->|No| CX[Return error: SHIFT_NOT_EDITABLE]
    C -->|Yes| D{segments provided?}
    D -->|Yes| E[Validate segment set:<br/>gaps, breakMinutes, sequence]
    E -->|Invalid| EX[Return error: SEGMENT_GAP_INVALID / SEGMENT_BREAK_INVALID]
    E -->|Valid| F
    D -->|No| F{shiftType consistent with<br/>segment count?}
    F -->|No| FX[Return error: SHIFT_TYPE_SEGMENT_MISMATCH]
    F -->|Yes| G{siteId provided?}
    G -->|Yes| H{Assignment effective on date?}
    H -->|No| HX[Return error: ASSIGNMENT_NOT_EFFECTIVE]
    H -->|Yes| I
    G -->|No| I[Apply updates and<br/>recompute plannedStartAt/plannedEndAt envelope]
    I --> J[Return updated shift]
```

## External Dependencies

- [shiftSchedule::ShiftSchedule](../model/ShiftSchedule.md) model - the parent period whose DRAFT status makes the slot editable
- organization::Site (bundled erp-kit module, package import per ADR-001) - optional siteId update for multi-site placement

## Error Scenarios

- **SHIFT_NOT_FOUND**: no Shift exists for the given id
- **SHIFT_NOT_EDITABLE**: the parent Shift's ShiftSchedule is not in DRAFT status
- **SHIFT_ALREADY_CANCELLED**: the slot has already been withdrawn (cancelledAt is set)
- **SEGMENT_REQUIRED**: no segments were provided, or the change would leave zero segments
- **SEGMENT_GAP_INVALID**: two consecutive segments do not leave a positive gap
- **SEGMENT_BREAK_INVALID**: breakMinutes is negative or not strictly less than the segment's gross span
- **SHIFT_TYPE_SEGMENT_MISMATCH**: segment count is inconsistent with shiftType

## Test Cases

- updates a PLANNED shift's segments and recomputes the plannedStartAt/plannedEndAt envelope
- updates a PLANNED shift's siteId for multi-site placement
- does not touch staffing: the update writes no assignment field at all
- locks the parent shiftSchedule while it checks, so a concurrent confirm cannot slip in
- throws SHIFT_NOT_EDITABLE when the shift's shiftSchedule is already CONFIRMED
- throws SEGMENT_GAP_INVALID when an update leaves a zero-gap pair
- throws SHIFT_TYPE_SEGMENT_MISMATCH when shiftType and segment count disagree after the update
- throws SHIFT_ALREADY_CANCELLED when the slot has been withdrawn
- throws SHIFT_NOT_FOUND when the shift does not exist
- ignores a reserved column smuggled in through the custom-field channel
