# DeclareReportedBlock

## Permission Scope

`reportedBlock`

## Overview

DeclareReportedBlock lets a worker or administrator manually declare a WORK, BREAK, or STEP_OUT interval for a workday when no punch device exists, creating a ReportedTimeBlock with `sourceKind = MANUAL`. After writing the block it re-derives that workday's CalculatedTimeBlocks in the same transaction (the "Reported change → Calculated re-derivation" invariant, centralized in the command layer rather than the resolver): a workDate that legitimately precedes the Assignment defers calculation, while an unresolvable WorkRule rolls the declaration back.

## Business Rules

- A manually declared block is the declared source of truth for its interval (it has no source TimeClockEvents to derive from)
- `startAt`/`endAt` must not be in the future; a declaration reflects something that already happened
- `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)
- The block must reference exactly one workforce Assignment effective on `workDate`
- An original declaration has no `correctionReason` (that field is reserved for corrections, see CorrectReportedBlock)
- 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 BREAK/STEP_OUT declaration must land within a covering current WORK block; declaring one before any covering WORK exists is rejected and writes nothing (declaring a WORK block only adds coverage and needs no containment check)
- Declaration is only allowed while the covering Timecard for `workDate` is OPEN; if SUBMITTED, APPROVED, or LOCKED, `reopenTimecard` must be used first to return the card to OPEN (reopen → correct → re-lock)
- After the block is written, CalculateTimeBlocks is invoked for the same Assignment/workDate in the same transaction; a WORK_RULE_NOT_FOUND failure rolls back the whole declaration rather than leaving stale/zero calculated totals. ASSIGNMENT_NOT_FOUND is treated as a benign, logged skip instead — a day-breaker-derived workDate can legitimately precede the Assignment's effectiveStart (e.g. an overnight first shift), and the declaration itself is still valid; calculation is deferred rather than rejecting real reported data

## Process Flow

```mermaid
flowchart TD
    A[Worker/admin declares a block: type, interval, workDate] --> B[Resolve Assignment effective on workDate]
    B --> C{Assignment found and Timecard OPEN?}
    C -- No --> D[Reject: ASSIGNMENT_NOT_FOUND / TIMECARD_NOT_OPEN]
    C -- Yes --> E{Overlaps an existing current WORK block?}
    E -- Yes --> F[Reject: OVERLAPPING_BLOCK]
    E -- No --> G[Create ReportedTimeBlock with sourceKind = MANUAL, no correctionReason]
    G --> H[Invoke CalculateTimeBlocks for the same Assignment/workDate, in the same transaction]
    H --> I{Calculation succeeds?}
    I -- WORK_RULE_NOT_FOUND --> J[Roll back the whole declaration]
    I -- ASSIGNMENT_NOT_FOUND --> K[Log and skip calculation; commit the declaration]
    I -- Yes --> L[Committed]
```

## External Dependencies

- [time-tracking::Timecard](../model/Timecard.md) - governs whether the covering period is still open for manual declaration (OPEN only)

## Error Scenarios

- **ASSIGNMENT_NOT_FOUND**: no workforce Assignment is effective for the relevant worker/date
- **TIMECARD_NOT_OPEN**: the Timecard covering the relevant workDate is not OPEN (SUBMITTED/APPROVED/LOCKED require `reopenTimecard` first)
- **FUTURE_INTERVAL**: `startAt` or `endAt` is in the future
- **OVERLAPPING_BLOCK**: the interval overlaps an existing current WORK block for the same Assignment and workday
- **INVALID_BLOCK_TYPE**: `blockType` is not one of WORK, BREAK, STEP_OUT
- **INVALID_INTERVAL**: the interval is empty or inverted — `endAt` must be after `startAt`
- **NON_WHOLE_MINUTE_INTERVAL**: the interval boundaries must fall on whole minutes
- **BLOCK_CONTAINMENT_VIOLATION**: a BREAK/STEP_OUT interval is not fully contained within a covering current WORK block for the same Assignment and workday

## Test Cases

- a worker without a punch device declares a WORK block directly (sourceKind = MANUAL)
- `blockType` and `sourceKind` are stored with normalized enum naming
- current (non-superseded) WORK blocks for an Assignment and workday do not overlap
- block durations are whole minutes (no float hours)
- an original declaration has no correctionReason
- declaring a BREAK with no covering current WORK block is rejected
- declaring a BREAK within a covering current WORK block succeeds
- declaring a WORK block needs no containment check and still succeeds
- rejects a declared interval whose startAt/endAt is in the future
- rejects a declared interval whose endAt is not after startAt
- rejects a declared interval whose boundaries are not on whole minutes
