# UnvoidPunch

## Permission Scope

`punch`

## Overview

UnvoidPunch retracts a prior void, restoring a punch to active, by appending an `UNVOID` record to the append-only `TimeClockEventVoid` table — the "undo the void" action is itself a new fact, never a deletion of the earlier VOID record.

## Business Rules

- Both the target event and its void history are immutable: an unvoid is expressed as a new `TimeClockEventVoid` record with `action = UNVOID`, never by editing or deleting the prior VOID (ADR-023)
- The unvoid record references the target via `targetEventId`, and belongs to the same Assignment as the target event
- Only a currently-voided event can be unvoided: the highest-`sequence` record for the target must be a `VOID`
- The appended record carries `action = UNVOID` and `sequence` one greater than the prior record, so it becomes the new effective state (active)
- Every unvoid record captures the actor, the instant, a `reasonCode`, a mandatory non-empty `reasonNote`, and the `authority` under which it was made
- The recorded actor is the authenticated caller (`ctx.actorId`), never a client-supplied field, so an unvoid cannot be attributed to another user (ADR-023 audit integrity)
- The full history — original event, its VOID, and this UNVOID — survives for audit; the effective state is always the latest record

## Process Flow

```mermaid
flowchart TD
    A[Request to unvoid 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 -- No --> F[Reject: PUNCH_NOT_VOIDED]
    E -- Yes --> G[Append TimeClockEventVoid action=UNVOID with next sequence, actor, reason]
    G --> H[Target event active again; eligible for block formation]
```

## External Dependencies

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

## Error Scenarios

- **PUNCH_NOT_FOUND**: no TimeClockEvent exists for the given id
- **PUNCH_NOT_VOIDED**: the target event is not currently voided (its latest record is not a VOID)

## Test Cases

- an unvoid appends a TimeClockEventVoid record with action=UNVOID above the prior VOID
- rejects a whitespace-only reasonNote before touching the database (ADR-023 non-empty reason)
- rejects with PUNCH_NOT_FOUND when no TimeClockEvent exists for the given id
- rejects with PUNCH_NOT_VOIDED when the target event is not currently voided
