# Timecard

## Description

Timecard is the **period aggregation and sign-off unit** — the record that rolls up a worker's CalculatedTimeBlocks over a period (day / week / month) and carries the approval lifecycle that closes that period (ADR-014). It holds `assignmentId`, `periodStart`, `periodEnd`, `status`, the four headline total minutes it denormalizes (regular / overtime / late-night / holiday, all integer minutes), the sign-off trail (`submittedAt`, `approvedAt`, `approvedBy`), the period-close trail (`lockedAt`, `lockedBy`), and a `historicalCorrection` flag. Those four totals cover the full `CalculatedTimeBlock.category` set (REGULAR / OVERTIME / NIGHT / HOLIDAY); any finer breakdown is obtained by aggregating that layer directly for the period.

Its lifecycle is **OPEN → SUBMITTED → APPROVED → LOCKED**, with a REOPENED path back to OPEN when a period must be re-worked. While OPEN, reported corrections flow normally through supersede; once SUBMITTED or APPROVED, corrections are blocked (`TIMECARD_NOT_OPEN`) and `reopenTimecard` must return the card to OPEN before a correction can be made. Once LOCKED (signed off), the period is closed: any further change is an explicit **historical correction** that sets `historicalCorrection` and is journaled in `TimeCorrectionLog`, following UKG's post-sign-off discipline (issue #7).

APPROVED and LOCKED are both non-editable, but they close two different things (see also Timecard Approval feature docs). Approve is a manager's per-card sign-off that a worker's period is correct; Lock is a payroll-finalization event — the period close run once every card in scope has been approved. Reopening from APPROVED is an ordinary send-back with no permanent trace; reopening from LOCKED is an exception path requiring a reason, and it permanently sets `historicalCorrection`. LOCKED marks the audit boundary past which every change must be journaled in `TimeCorrectionLog`, because that is the point downstream payroll consumes the data.

The approval steps themselves are delegated to the bundled approval module via the wrapper pattern (ADR-003): `submitTimecard` creates a direct-mode approval request (one step, `timecard-approver` role, quorum ANY) in the same transaction as OPEN→SUBMITTED; `approveTimecard` resolves that step and syncs the card to APPROVED; `reopenTimecard` resolves the in-flight request (withdraw by the submitter, send-back by the assignee) before returning to OPEN. Where approval is discussed, note that the self-approval error is named `SELF_APPROVAL` (renamed from `SELF_APPROVAL_BLOCKED`) per the ADR-003 error-naming convention.

## Domain Model Definitions

### Model type

Stateful

#### State Transitions

```mermaid
stateDiagram-v2
    [*] --> OPEN: openTimecard
    OPEN --> SUBMITTED: submitTimecard
    SUBMITTED --> APPROVED: approveTimecard
    SUBMITTED --> OPEN: reopenTimecard
    APPROVED --> LOCKED: lockTimecard
    APPROVED --> OPEN: reopenTimecard
    LOCKED --> OPEN: reopenTimecard
    LOCKED --> [*]
```

### Command Definitions

Command docs are out of scope for this design phase (ADR-011). Anticipated commands:

- openTimecard — open a Timecard for an Assignment and period, aggregating current CalculatedTimeBlocks (entry into OPEN)
- submitTimecard — submit an OPEN Timecard for approval (OPEN to SUBMITTED)
- approveTimecard — approve a SUBMITTED Timecard via the approval wrapper (SUBMITTED to APPROVED)
- closeTimecardPeriod — period-level close: bulk-lock every APPROVED Timecard for an exact period (partial and idempotent — non-APPROVED cards reported as blockers, already-LOCKED cards are no-ops), recording `lockedAt`/`lockedBy`; the normal close path run by a labor/HR operator (APPROVED to LOCKED)
- lockTimecard — lock a single APPROVED Timecard (APPROVED to LOCKED); the single-card exception path (re-lock after a post-lock historical correction), while normal period close uses closeTimecardPeriod
- reopenTimecard — reopen a SUBMITTED, APPROVED, or LOCKED Timecard back to OPEN; reopening a LOCKED card flags a historical correction

### Query Definitions

- getTimecard — retrieve a single Timecard by id (any status)
- listOpenTimecards — Timecards currently OPEN for review, paginated
- listTimecardsByAssignment — a worker's Timecards across periods, paginated
### Models

- Timecard

### Invariants

- `status` is one of OPEN, SUBMITTED, APPROVED, LOCKED, stored with normalized enum naming
- `periodStart` is on or before `periodEnd`; the period defines the set of workdays whose CalculatedTimeBlocks it aggregates
- Every Timecard references exactly one workforce `Assignment` (via `assignmentId`); at most one Timecard exists per Assignment and non-overlapping period
- `categoryTotals` is a list of `{ category, minutes }` entries keyed by the strategy-defined category key (not fixed JP columns); each `minutes` is a non-negative integer (no float hours) and reconciles to the sum of the covered CalculatedTimeBlocks in that category for the period
- APPROVED and LOCKED states always carry `approvedAt` and `approvedBy`; `approvedBy` is never the owner of the Assignment (self-approval is blocked)
- LOCKED state always carries `lockedAt` and `lockedBy` (the closing actor); a non-LOCKED Timecard has them null
- SUBMITTED state always carries `submittedAt`
- Correction mode on the covered ReportedTimeBlocks follows the Timecard's status in a 3-way split: while OPEN, corrections use supersede; while SUBMITTED or APPROVED, corrections are rejected with `TIMECARD_NOT_OPEN` and `reopenTimecard` is required first; while LOCKED, corrections are rejected with the same `TIMECARD_NOT_OPEN` and are instead recorded as a historical correction. A historical correction is journaled in TimeCorrectionLog, sets `historicalCorrection = true`, and updates the corrected category's total in `categoryTotals` on the Timecard in place — the Timecard is always the current snapshot, the log is the audit trail, not an overlay to replay
- `historicalCorrection` is true if and only if at least one change was applied after the Timecard first reached LOCKED
- Reopening returns the Timecard to OPEN; a reopen from LOCKED preserves the prior sign-off trail and records the reason as a historical correction

### Relationships

- **References Assignment** (workforce): `assignmentId` references the Assignment whose period is being aggregated and signed off
- **Aggregates CalculatedTimeBlock**: each `categoryTotals` entry sums the CalculatedTimeBlocks of that category whose `workDate` falls within `[periodStart, periodEnd]`; the set of categories is whatever the calculation strategy emits (the JP strategy: REGULAR/OVERTIME/NIGHT/HOLIDAY), not a fixed core list
- **Governs ReportedTimeBlock correction mode**: a covered ReportedTimeBlock uses supersede only while the Timecard is OPEN; correction is blocked (`TIMECARD_NOT_OPEN`) while SUBMITTED or APPROVED and requires `reopenTimecard` first; once LOCKED, correction is historical (TimeCorrectionLog) instead of supersede
- **Journaled by TimeCorrectionLog**: post-lock changes append TimeCorrectionLog entries referencing this Timecard
- **Approved by User** (user-management): `approvedBy` references the approver
- **Locked by User** (user-management): `lockedBy` references the labor/HR operator who closed the period
- **Wrapped by approval** (approval, bundled module): submit/approve/reopen are driven by a direct-mode approval request (one step, `timecard-approver` role, quorum ANY) via the wrapper pattern (ADR-003) — submitTimecard creates the request, approveTimecard resolves it and syncs APPROVED, reopenTimecard resolves it via withdraw (submitter) or send-back (assignee)
