# VoidPunch

## Permission Scope

`punch`

## Overview

VoidPunch retracts a mistaken or duplicate punch by appending a `VOID` record to the separate append-only `TimeClockEventVoid` table, referencing the target event — without ever mutating or deleting the original `TimeClockEvent`.

## Business Rules

- Raw punches are immutable: a void is expressed as a new record in `TimeClockEventVoid`, never as a modification or deletion of the target event (ADR-014, ADR-023)
- The void record references the target via `targetEventId`, and belongs to the same Assignment as the target event
- Effective void state is decided by the highest-`sequence` `TimeClockEventVoid` record for the target: `VOID` means currently voided, `UNVOID` (or no record) means active
- A currently-voided event cannot be voided again; the appended record carries `action = VOID` and `sequence` one greater than the prior record (or 1 when first)
- Every void record captures the actor, the instant, a `reasonCode`, a mandatory non-empty `reasonNote`, and the `authority` under which it was made — a void without a reason is disallowed
- The recorded actor is the authenticated caller (`ctx.actorId`), never a client-supplied field, so a void cannot be attributed to another user (ADR-023 audit integrity)
- The full punch trail — the original event and its void/unvoid history — survives for audit and labor-dispute defense (issue #6)
- Voiding a punch does not itself re-form ReportedTimeBlocks or recalculate CalculatedTimeBlocks. `TimeClockEvent`s are raw point-in-time facts consumed only at the moment blocks are formed; correcting the resulting recorded/calculated time is done by editing the `ReportedTimeBlock` directly (CorrectReportedBlock / DeclareReportedBlock / ImportReportedBlocks), which re-derives calculation. This is a deliberate one-directional boundary — the reported layer is the correction surface, not the raw punch stream

## Process Flow

```mermaid
flowchart TD
    A[Request to void a punch, referencing targetEventId] --> B[Look up target TimeClockEvent]
    B --> C{Target exists and same Assignment?}
    C -- No --> D[Reject: PUNCH_NOT_FOUND]
    C -- Yes --> E{Latest void record is VOID?}
    E -- Yes --> F[Reject: PUNCH_ALREADY_VOIDED]
    E -- No --> G[Append TimeClockEventVoid action=VOID with next sequence, actor, reason]
    G --> H[Target event remains unmutated; excluded from block formation while voided]
```

## External Dependencies

- [time-tracking::TimeClockEvent](../model/TimeClockEvent.md) - the immutable raw punch record being voided
- [time-tracking::TimeClockEventVoid](../model/TimeClockEventVoid.md) - the append-only table the VOID record is written to

## Error Scenarios

- **PUNCH_NOT_FOUND**: no TimeClockEvent exists for the given id
- **PUNCH_ALREADY_VOIDED**: the target event's latest void record is already a VOID

## Test Cases

- events are never modified or deleted; a void appends a TimeClockEventVoid record referencing the target
- assigns the next sequence above the prior record
- a void record captures actor, occurredAt, reasonCode, a non-empty reasonNote, and authority
- rejects with PUNCH_NOT_FOUND when no TimeClockEvent exists for the given id
- rejects with PUNCH_NOT_FOUND when the target event belongs to a different Assignment
- rejects a whitespace-only reasonNote before touching the database (ADR-023 non-empty reason)
- trims surrounding whitespace from a valid reasonNote before persisting
- rejects with PUNCH_ALREADY_VOIDED when the target event's latest record is already a VOID
