# CreateShiftPattern

## Permission Scope

shiftPattern

## Overview

createShiftPattern defines a reusable シフトパターン (早番 / 遅番 / 日勤 / 夜勤 / 宿直 / 中抜け / 公休) with a stable `code`, display `name`, `kind`, optional `scopeId`, and zero or more ordered embedded `segments`, so planners can compose shifts from a named template instead of re-entering times on every shift.

## Business Rules

- `code` must be unique and stable within its scope; selection and downstream labeling bind to `code`, never to the display `name` (ADR-015; #14 反面教師)
- `scopeId` is optional (an organization Site); `code` uniqueness is checked against the pair `(scopeId, code)` — the same `code` may exist under a different `scopeId` (including the shared `scopeId = null` scope, which is one scope for this purpose)
- `kind` must be one of the normalized enum values `DAY` / `EARLY` / `LATE` / `NIGHT` / `ON_CALL` / `OFF`
- A pattern must be created with **at least one** segment, **except** when `kind = OFF`, which must be created with **zero** segments (公休/休み has no work interval to define)
- A single-part pattern has one segment; a 中抜け pattern has two or more segments separated by a positive gap
- Segments are numbered by `sequence`, 1-based and contiguous, consistent with time order
- Each segment's `startTime`/`endTime` are minutes from midnight in `[0, 1440)`; `spansMidnight` is true exactly when `endTime <= startTime`
- Each segment's `breakMinutes` is non-negative and strictly less than that segment's gross span
- The pattern carries no date and no assignee — it is a template only

## Process Flow

```mermaid
flowchart TD
    A[Receive code, name, kind,<br/>optional scopeId,<br/>ordered list of segments] --> B{"(scopeId, code) unique?"}
    B -->|No| BX[Return error: SHIFT_PATTERN_CODE_ALREADY_EXISTS]
    B -->|Yes| K{kind = OFF?}
    K -->|Yes| KX{Zero segments provided?}
    KX -->|No| KXE[Return error: SEGMENT_KIND_MISMATCH]
    KX -->|Yes| H
    K -->|No| C{At least one segment provided?}
    C -->|No| CX[Return error: SEGMENT_REQUIRED]
    C -->|Yes| D[Validate each segment:<br/>startTime/endTime range,<br/>breakMinutes < gross span,<br/>spansMidnight consistent]
    D --> E{Two or more segments?}
    E -->|Yes| F{Positive gap between<br/>consecutive segments?}
    F -->|No| FX[Return error: SEGMENT_GAP_INVALID]
    F -->|Yes| G
    E -->|No| G[Assign sequence 1..n]
    G --> H[Insert ShiftPattern with<br/>its embedded segments]
    H --> I[Return created pattern]
```

## External Dependencies

- organization::Site (bundled erp-kit module, package import per ADR-001) - optional `scopeId` scoping the pattern's `code` uniqueness to a single site

## Error Scenarios

- **MISSING_REQUIRED_FIELD**: a required field is missing
- **SHIFT_PATTERN_CODE_ALREADY_EXISTS**: The given (scopeId, code) pair is already used by another pattern
- **SEGMENT_REQUIRED**: no segments were provided, or the change would leave zero segments
- **SEGMENT_KIND_MISMATCH**: one or more segments were provided when kind = OFF (an OFF pattern must have zero segments)
- **SEGMENT_TIME_INVALID**: a segment's startTime/endTime is outside [0, 1440) or inconsistent with spansMidnight
- **SEGMENT_BREAK_INVALID**: breakMinutes is negative or not strictly less than the segment's gross span
- **SEGMENT_GAP_INVALID**: two consecutive segments do not leave a positive gap
- **SEGMENT_SEQUENCE_INVALID**: Segment sequence numbers are not 1-based, contiguous, or consistent with time order

## Test Cases

- creates a single-segment pattern (e.g. 日勤) with kind DAY
- creates a 中抜け pattern with two segments separated by a positive gap
- creates an ON_CALL pattern with a spansMidnight segment
- creates an OFF pattern with zero segments
- throws SEGMENT_KIND_MISMATCH when an OFF pattern is given one or more segments
- creates two patterns with the same code under different scopeId values
- throws SHIFT_PATTERN_CODE_ALREADY_EXISTS when (scopeId, code) is already used
- throws SHIFT_PATTERN_CODE_ALREADY_EXISTS when code is already used
- throws SEGMENT_REQUIRED when no segments are provided
- throws SEGMENT_GAP_INVALID when two segments have a zero or negative gap
- throws SEGMENT_BREAK_INVALID when a segment's breakMinutes is not less than its gross span
- selection and labeling of the created pattern bind to code, not name
