# FormReportedBlocks

## Permission Scope

`reportedBlock`

## Overview

FormReportedBlocks forms or refreshes punch-derived ReportedTimeBlocks for an Assignment around a reference workday by pairing raw TimeClockEvents through the day-breaker into WORK, BREAK, and STEP_OUT intervals. A single call can touch the reference day and the day before it (overnight spillover); each formed block is assigned to its own workday independently, so a call anchored on one day never mis-stamps or double-counts a different day's already-formed shift.

## Business Rules

- Blocks produced here are `sourceKind = PUNCH_DERIVED` and trace back to the matched raw TimeClockEvents that formed them
- A block's workday is the day-breaker day of its *opening* punch (in the given `timezone`, defaulting to UTC), not the input `workDate` - only the reference day and the day before it are ever written to by one call
- `blockType` is one of WORK, BREAK, STEP_OUT, stored with normalized enum naming
- `startAt`/`endAt` bound a non-negative interval on `workDate`; durations are whole minutes (no float hours), and an overnight block may end after midnight while still belonging to its `workDate`
- Among current (non-superseded) blocks sharing an Assignment and workday, WORK blocks do not overlap; BREAK and STEP_OUT blocks fall within a covering WORK span
- A punch-derived block is re-derivable from its source TimeClockEvents; re-running formation for the same workday regenerates rather than duplicates blocks. This is safe to do as a hard delete-then-insert because a corrected block is re-tagged `sourceKind = MANUAL` on correction (see CorrectReportedBlock) - a current `PUNCH_DERIVED` row here can never be the target of a historical supersede reference
- Voided punches (per the latest TimeClockEventVoid record) are excluded from pairing
- An opener with no closer anywhere in the fetch window (e.g. still clocked in, or on an open break) is left unformed rather than rejecting the whole call - it's the normal "not done yet" state, not a data error, and it doesn't block other, independently-closed pairs elsewhere in the same window
- A degenerate pair whose closer lands at or before its opener would form an empty/inverted interval; that pair forms no block (the shared reported-interval well-formedness rule, minus the whole-minute constraint since punch-derived blocks carry raw device timestamps)
- A workday whose covering Timecard is not OPEN (SUBMITTED, APPROVED, or LOCKED) is skipped entirely (never silently rewritten); it's reported back via `notOpenWorkDates` for the caller to surface
- `OVERLAPPING_BLOCK` is only raised when every touched workday that had something to write hit an overlap; a not-OPEN or overlapping day among several touched days does not fail the days that succeeded (those are reported via `notOpenWorkDates`/`overlapErrorWorkDates` instead)
- After formation, every workday actually re-formed this run has its CalculatedTimeBlocks re-derived in the same transaction (the "Reported change → Calculated re-derivation" invariant, centralized in the command layer rather than the resolver — the same seam that makes a punch flow through to calculation); this is keyed off the re-formed days, not only the days that produced blocks, so a day that re-formed to **zero** blocks (e.g. its CLOCK_OUT was voided) has its prior CalculatedTimeBlocks cleared and its covering Timecard total re-aggregated to zero, rather than retaining the pre-void value. ASSIGNMENT_NOT_FOUND (the day-breaker-derived workDate precedes the Assignment's effectiveStart — the classic case being an overnight first shift) is a benign, logged skip for that one workday, but WORK_RULE_NOT_FOUND rolls back the whole formation

## Process Flow

```mermaid
flowchart TD
    A[Trigger: new/voided punches for an Assignment, anchored on a reference workday] --> B[Gather TimeClockEvents one day before through two days after the reference day, excluding voided pairs]
    B --> C[Pair CLOCK_IN/CLOCK_OUT and BREAK_START/END and STEP_OUT/IN into intervals; an opener with no closer yet is left unformed, not rejected]
    C --> D[Assign each formed block its own workday from its opener's day-breaker day]
    D --> E{For each of the reference day and the day before it...}
    E --> F{Covering Timecard OPEN?}
    F -- No --> G[Skip this workday; report via notOpenWorkDates]
    F -- Yes --> H{Overlaps an existing declared/corrected current WORK block?}
    H -- Yes --> I[Skip this workday; report via overlapErrorWorkDates]
    H -- No --> J[Regenerate this workday's current PUNCH_DERIVED blocks]
    J --> K[Re-derive CalculatedTimeBlocks for each distinct written workday, in the same transaction]
    K --> L{Calculation succeeds for each touched day?}
    L -- WORK_RULE_NOT_FOUND --> M[Roll back the whole formation]
    L -- No current blocks or ASSIGNMENT_NOT_FOUND --> O[Log and skip calculation for that day only]
    L -- Yes --> N[Committed]
```

## External Dependencies

- [time-tracking::TimeClockEvent](../model/TimeClockEvent.md) - the raw punch events paired into blocks via the day-breaker
- [time-tracking::Timecard](../model/Timecard.md) - governs whether a touched workday is still open for automatic regeneration (OPEN only)

## Error Scenarios

- **OVERLAPPING_BLOCK**: the interval overlaps an existing current WORK block for the same Assignment and workday
- **ASSIGNMENT_NOT_FOUND**: no workforce Assignment is effective for the relevant worker/date

## Test Cases

- forming punch-derived blocks pairs events onto the correct day-breaker workday
- current (non-superseded) WORK blocks for an Assignment and workday do not overlap
- block durations are whole minutes (no float hours)
- `blockType` and `sourceKind` are stored with normalized enum naming
- an overnight block that crosses midnight is kept on its day-breaker workday even though endAt is after 00:00
- a trailing unclosed opener (still clocked in) does not block other independently-closed pairs in the same window
- a workDate whose covering Timecard is not OPEN (SUBMITTED, APPROVED, or LOCKED) is skipped (not silently rewritten), reported via notOpenWorkDates
- a BREAK nested inside a still-open WORK span forms independently once it closes, not swallowed by the WORK pairing
- a degenerate punch pair whose closer is at or before its opener forms no block
