# Timecard Approval

## Overview

Timecard Approval is the **period aggregation and sign-off (period-close) workflow**. A `Timecard` rolls up the CalculatedTimeBlocks for an Assignment over a period (day / week / month) into per-category minute totals, then moves through the lifecycle **OPEN → SUBMITTED → APPROVED → LOCKED**, with a REOPENED path back to OPEN. While OPEN, reported corrections flow normally via supersede; once SUBMITTED or APPROVED, corrections are blocked (`TIMECARD_NOT_OPEN`) until the card is reopened. Locking closes the period; from that point any change is an explicit **historical correction** that flags the Timecard and is journaled in `TimeCorrectionLog` (UKG post-sign-off discipline, issue #7).

Approval routing (steps, assignees, decision audit) is delegated to the bundled approval module via the wrapper pattern (ADR-003), in **direct mode**: `submitTimecard` creates a one-step approval request in the same transaction it moves OPEN→SUBMITTED, with a single assignee — the `timecard-approver` role — and `roleQuorum: ANY`. `approveTimecard` resolves that request's step and, in the same transaction, syncs the Timecard to APPROVED. `reopenTimecard` resolves the in-flight request (withdraw if the actor is the submitter, send-back if the actor is the assignee) before returning the card to OPEN. ADR-003's error names apply: `NOT_ASSIGNEE`, `SELF_APPROVAL`, and `APPROVAL_STEP_FAILED` as the catch-all for other non-ok approval-step outcomes. There is no REJECTED status — a period close never simply "ends rejected"; the send-back/reopen path is how a correction cycle is expressed instead.

APPROVED and LOCKED both make the card non-editable in the ordinary sense, but they close two different things. Approve is a manager's per-card sign-off — "this worker's data for this period is correct." Lock is a payroll-finalization event — the period close performed by a payroll operator/administrator once every card in scope has been approved, at the organization level rather than the individual-card level. Reopening from APPROVED is an ordinary send-back: the prior sign-off trail is cleared and the card goes back through OPEN → SUBMITTED → APPROVED normally, with no permanent flag. Reopening from LOCKED is an exception path: it requires a reason, preserves the prior sign-off trail, and sets `historicalCorrection` permanently — the card carries the mark of a post-close change forever. LOCKED marks the point downstream payroll has consumed the data, and every change after that point must be journaled in `TimeCorrectionLog`; without a distinct LOCKED state, either routine pre-payroll fixes would need audit-log entries they don't warrant, or payroll would have no stable, frozen snapshot to read from.

## Business Purpose

- Provide the period sign-off unit that closes attendance for payroll hand-off (ADR-014)
- Aggregate calculated minutes by category into a reviewable, approvable summary
- Enforce a clear lifecycle with an auditable approver trail (submittedAt / approvedAt / approvedBy), blocking self-approval
- Keep corrections OPEN-only: once a period is SUBMITTED or APPROVED, its reported data is frozen from ordinary editing, and reopening is the only way back to a correctable state
- Treat post-lock changes as explicit historical corrections rather than silent edits, preserving the sign-off record (issue #7)
- Reuse the bundled approval engine via the wrapper pattern (direct mode, one step, `timecard-approver` role, quorum ANY) rather than building bespoke routing (ADR-003)
- Keep APPROVED and LOCKED as distinct closes: APPROVED is a manager's per-card sign-off that a worker's period is correct; LOCKED is a payroll-finalization event (period close) run once every card in scope has been approved, at the organization level rather than the individual-card level

The close is a **period-level bulk operation**, not a per-card action. A labor/HR operator confirms a period's approvals are in, chases any stragglers, then runs `closeTimecardPeriod` for the whole period at once. The close is partial and idempotent: it locks every APPROVED card in the period (recording `lockedAt`/`lockedBy`), returns cards not yet APPROVED as blockers (`skipped`) without aborting, and treats already-LOCKED cards as no-ops (`alreadyLocked`) — so the "check → chase → re-run" workflow composes without extra machinery, and a future period-close-date cron is just "call `closeTimecardPeriod` on the cutoff". The per-card `lockTimecard` survives only as the single-card exception path (re-locking one card after a post-lock historical correction). Organizational scope (department / site / company) is resolved to `assignmentIds` at the app layer and passed through; the module itself closes by exact period only.

## Process Flow

```mermaid
flowchart TD
    A[Open Timecard for Assignment + period; aggregate CalculatedTimeBlocks] --> B[Status OPEN: corrections flow via supersede]
    B --> C{Sole eligible approver is the requester?}
    C -- Yes --> C1[Reject submit: no valid approver]
    C -- No --> D[Submit: create direct-mode approval request 1 step / timecard-approver role / quorum ANY]
    D --> E[Status SUBMITTED]
    E --> F{Correction attempted while not OPEN?}
    F -- Yes --> F1[Reject: TIMECARD_NOT_OPEN]
    E --> G{Approver decision}
    G -- Approve --> H[approveApprovalStep; sync Timecard to APPROVED with approvedAt/approvedBy]
    G -- Send back / withdraw --> I[Resolve approval request; reopenTimecard to OPEN]
    I --> B
    H --> J{Reopen while APPROVED?}
    J -- Yes --> I
    J -- No --> K[Lock the period]
    K --> L[Status LOCKED: period closed]
    L --> M{Change needed after lock?}
    M -- Historical correction --> N[Record in TimeCorrectionLog; set historicalCorrection]
    M -- Reopen (reason required) --> I
    N --> L
```

## Scenario Patterns

- **Clean sign-off**: OPEN → SUBMITTED → APPROVED → LOCKED with no corrections
- **Submit mirrors an approval request**: submitTimecard creates a direct-mode approval request (one step, timecard-approver role, quorum ANY) linked by targetEntityId, in the same transaction as OPEN→SUBMITTED
- **Submit blocked with no valid approver**: submitting is rejected when the only eligible approver is the requester, so no Timecard is ever created unapprovable
- **Approve syncs the wrapper**: approveTimecard resolves the approval step and moves SUBMITTED→APPROVED in one transaction, stamping approvedAt/approvedBy
- **Self-approval blocked**: an approver acting on their own Timecard is rejected with SELF_APPROVAL
- **Send back / withdraw resolves on reopen**: reopenTimecard from SUBMITTED resolves the in-flight approval request — withdraw if the actor is the submitter, send-back if the actor is the assignee — before returning the card to OPEN
- **Correction attempted while not OPEN is rejected**: a correction against a SUBMITTED or APPROVED Timecard's reported blocks fails with TIMECARD_NOT_OPEN; reopenTimecard is required first
- **Pre-lock correction**: while OPEN, reported corrections use supersede and re-derivation updates totals
- **Post-lock historical correction**: a change after LOCKED is journaled in TimeCorrectionLog and sets historicalCorrection
- **Reopen locked period**: a LOCKED Timecard is reopened to OPEN with a required reason, preserving the prior sign-off trail as a historical correction
- **Aggregation reconciliation**: category totals always equal the sum of covered CalculatedTimeBlocks
- **Bulk period close**: a labor/HR operator runs closeTimecardPeriod for an exact period, locking every APPROVED card at once; non-APPROVED cards come back as blockers and already-LOCKED cards are no-ops, so the check → chase → re-run workflow is idempotent

## Test Cases

- opening a Timecard aggregates CalculatedTimeBlocks for the period into category totals (integer minutes)
- the lifecycle enforces OPEN → SUBMITTED → APPROVED → LOCKED with REOPENED back to OPEN
- submitting an OPEN Timecard creates a direct-mode approval request (one step, timecard-approver role, quorum ANY) linked to the Timecard
- submitting is rejected when the sole eligible approver is the requester (no valid approver)
- APPROVED/LOCKED always carry approvedAt and approvedBy; the approver is not the Assignment owner
- approving a Timecard as the requester (self-approval) is rejected with SELF_APPROVAL
- attempting a correction against a SUBMITTED or APPROVED Timecard's reported blocks is rejected with TIMECARD_NOT_OPEN and requires reopenTimecard first
- reopening a SUBMITTED Timecard resolves the mirroring approval request (withdraw by the submitter, send-back by the assignee) before returning to OPEN
- while LOCKED, reported blocks are not correctable by supersede; changes go through TimeCorrectionLog
- a post-lock change sets historicalCorrection and appends a TimeCorrectionLog entry referencing the Timecard
- `status` values are stored with normalized enum naming
- category totals reconcile to the sum of covered CalculatedTimeBlocks
- closeTimecardPeriod locks every APPROVED card for an exact period; non-APPROVED cards are reported as blockers, already-LOCKED cards are no-ops, and an empty scope is rejected with NO_TIMECARDS_IN_PERIOD

## Reference Links

- Data-model design research (Timecard sign-off, historical correction after sign-off): https://github.com/tailor-sandbox/Omakase-ERP-attendance/issues/7
- Enterprise-vs-SMB gaps (approval/period close): https://github.com/tailor-sandbox/Omakase-ERP-attendance/issues/9
