# ApproveTimecard

## Permission Scope

`timecard`

## Overview

ApproveTimecard delegates the decision to the bundled approval module's `approveApprovalStep` and, in the same transaction, syncs the Timecard from SUBMITTED to APPROVED, recording `approvedAt` and `approvedBy`.

## Business Rules

- Only a Timecard currently in SUBMITTED state may be approved (SUBMITTED → APPROVED)
- APPROVED state always carries `approvedAt` and `approvedBy`
- The approve decision, self-approval guard, and decision audit trail are delegated to the bundled approval module via `approveApprovalStep` (ADR-003 wrapper pattern); self-approval is rejected by the approval engine itself, not by time-tracking
- On a successful approval step, the Timecard is synced to APPROVED in the same transaction as the approval-module decision
- Approval does not itself change category totals; it only advances status and stamps the approval trail

## Process Flow

```mermaid
flowchart TD
    A[Approver decision on a SUBMITTED Timecard] --> B[Look up Timecard]
    B --> C{Timecard exists and status = SUBMITTED?}
    C -- No, not found --> D[Reject: TIMECARD_NOT_FOUND]
    C -- No, wrong status --> E[Reject: INVALID_STATUS_TRANSITION]
    C -- Yes --> F[Call approval module: approveApprovalStep]
    F -- Resolver not an eligible assignee --> G[Reject: NOT_ASSIGNEE]
    F -- Resolver is the requester --> H[Reject: SELF_APPROVAL]
    F -- Other non-ok result --> I[Reject: APPROVAL_STEP_FAILED]
    F -- Step approved --> J[Set status = APPROVED, approvedAt = now, approvedBy = approver, in the same transaction]
    J --> K[Timecard ready for locking]
```

## External Dependencies

- approval module (cross-module, bundled) - `approveApprovalStep` drives the approve decision, self-approval guard, and decision audit trail; the Timecard is synced to APPROVED in the same transaction (ADR-003)

## Error Scenarios

- **TIMECARD_NOT_FOUND**: no Timecard exists for the given id
- **INVALID_STATUS_TRANSITION**: the Timecard's current status does not permit this transition
- **NOT_ASSIGNEE**: the resolver is not an eligible assignee of the approval step
- **SELF_APPROVAL**: the resolver is the requester (renamed from `SELF_APPROVAL_BLOCKED`)
- **APPROVAL_STEP_FAILED**: the bundled approval-module step failed to resolve for any other reason

## Test Cases

- rejects with TIMECARD_NOT_FOUND when no Timecard exists for the given id
- rejects with INVALID_STATUS_TRANSITION when the Timecard is not SUBMITTED
- delegates to approveApprovalStep and stamps approvedAt/approvedBy = the approver
- rejects with NOT_ASSIGNEE when the actor owns no pending assignee on the step
- maps the approval engine's self-decision guard to SELF_APPROVAL
- maps any other non-ok approval result to APPROVAL_STEP_FAILED
