# CreateShiftPlacement

## Permission Scope

shiftSchedule

## Overview

createShiftPlacement places a workforce `Assignment` onto a `Shift` — the authoritative "who actually staffs this slot" record. It covers direct placement of the shift's planned home assignment, filling an open shift (未割当), 応援 (support work by someone from another post), and multi-site placement where the worker is placed at a Site other than their home post.

## Business Rules

- The placement is created `ACTIVE`, with `supersededById` and `releasedAt` null; retiring it is `releaseShiftPlacement` (CANCELLED) or `swapShiftPlacement` (SUPERSEDED), never a delete
- The Assignment must be **effective on the Shift's date** — read through the injected workforce `getAssignment`, because shiftSchedule cannot join workforce's tables. The FK alone only proves the row exists, not that the person was posted there that day

- The ShiftPlacement references exactly one `Shift` and exactly one workforce `Assignment`
- The placed `Assignment` must exist — enforced today only by the DB-level foreign key, not by an application-level effective-date check (cross-module query injection for `Assignment` is not yet wired into `module.ts`)
- The target Shift must be live; a withdrawn Shift (`cancelledAt` set) accepts no placements
- A slot is considered filled once at least one ACTIVE ShiftPlacement references it
- A shift may carry more than one ShiftPlacement (team shifts), but **at most one ACTIVE placement per Assignment**: placing the same person on the same slot twice would double-count them in personal schedules and in variance reporting
- A superseded or cancelled placement does not block a new one, so releasing or swapping someone out and later placing them back on the same slot is allowed
- A placement whose Assignment's home Position/Site differs from the Shift's `siteId` represents 応援 / multi-site work; this attribution is **derived** from that comparison and lives on the placement, not the Shift
- `assignedAt` is recorded at creation time as part of the audit trail
- `provenance` defaults to `MANUAL` when not supplied (a person is placing this Assignment); bulk generation logic instead calls `createShiftPlacements` with `provenance = GENERATED`

## Process Flow

```mermaid
flowchart TD
    A[Receive shiftId, assignmentId,<br/>optional provenance] --> B{Shift exists?}
    B -->|No| BX[Return error: SHIFT_NOT_FOUND]
    B -->|Yes| C{Shift cancelledAt is null?}
    C -->|No| CX[Return error: SHIFT_CANCELLED]
    C -->|Yes| E[Determine placement type:<br/>home / open-shift fill / 応援 / multi-site<br/>by comparing Assignment site to Shift siteId]
    E --> F[Insert ShiftPlacement with<br/>assignedAt = now, provenance defaulted to MANUAL]
    F --> G[Return created placement]
```

## External Dependencies

- [shiftSchedule::Shift](../model/Shift.md) model - the slot being staffed; must not be withdrawn
- workforce::Assignment (erp-kit bundled module) - the assignment being placed, referenced by id (existence enforced only by the DB-level FK, not an application-level effective-date check); its home Position/Site is compared against the shift's siteId to detect 応援/multi-site

## Error Scenarios

- **SHIFT_NOT_FOUND**: no Shift exists for the given id
- **SHIFT_CANCELLED**: the target Shift is CANCELLED
- **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
- **SHIFT_PLACEMENT_ALREADY_EXISTS**: an ACTIVE ShiftPlacement already places this Assignment on this Shift

Not currently raised by this command: an `assignmentId` referencing a non-existent Assignment fails at the DB-level FK constraint instead of a dedicated application error (no `ASSIGNMENT_NOT_EFFECTIVE` check — see command implementation note).

## Test Cases

- throws SHIFT_PLACEMENT_ALREADY_EXISTS when the Assignment already has an ACTIVE placement on the Shift
- places an Assignment whose earlier placement on the same Shift was cancelled

- throws MISSING_REQUIRED_FIELD when shiftId is missing
- throws MISSING_REQUIRED_FIELD when assignmentId is missing
- throws SHIFT_NOT_FOUND when the shift does not exist
- throws ASSIGNMENT_NOT_EFFECTIVE when the Assignment does not cover the shift's date
- throws SHIFT_CANCELLED when the target shift is CANCELLED
- placing an Assignment onto a PUBLISHED shift creates a ShiftPlacement
- creating a ShiftPlacement without specifying provenance defaults it to MANUAL
- filling an open shift (null Shift.assignmentId) marks it staffed
- a support placement references an Assignment whose home Site differs from the shift's Site
- a shift can carry multiple ShiftPlacements (team shift)
