# Work evidence, effective time entries, and corrections

The organization API separates immutable scan evidence from the effective work
intervals used by crewQI. This distinction keeps investigations trustworthy
while allowing a missed check-in or checkout to be corrected safely.

All examples use synthetic TEST identifiers and the TEST host. The public path
remains `/v1` and the OpenAPI version remains `1.0.0` under the pre-adoption
contract-alignment decision.

## Raw evidence is read-only

`GET /work-events` and `GET /work-events/{workEventId}` require
`work_events:read`. They return safe event identity, employee/location/task
references, event type, client/server timestamps, offline status, and the
authoritative `VERIFIED` or `REVIEW_REQUIRED` state.

The serializer never returns QR public IDs, QR or tag secrets/hashes, clear
credentials, device identifiers, precise positions, or internal validation
metadata. The organization API cannot write this collection. Employee scan
ingestion stays behind crewQI's authenticated credential-validating RPC, so an
integration cannot manufacture scan evidence with a service-role insert.

## Canonical effective time

`GET /timesheets` and `GET /timesheets/{timeEntryId}` require
`timesheets:read`. Each stable entry includes:

- employee and discriminated `REGISTERED_LOCATION` or `VEHICLE_AD_HOC`
  context;
- effective start, end, break, and worked minutes;
- `SCANNED` or `MANUAL` origin and `OPEN`, `CLOSED`, or `VOIDED` state;
- adjusted flag, monotonically increasing revision, and ETag;
- a durable selected mobile address summary without precise output
  coordinates.

When the same key also has `work_events:read`, the response includes source
event IDs and original source start/end/break values. A timesheet entry has no
independently mutable verification state: only eligible verified evidence can
project into a scanned entry.

Range filters use overlap semantics. For `from=2026-07-01T00:00:00Z` and
`to=2026-08-01T00:00:00Z`, a session that starts in June and ends in July is
included. `from` is inclusive, `to` is exclusive. `employeeId`, `state`,
`limit`, and opaque `cursor` filters are also supported.

```bash
curl --fail-with-body --get \
  --header "Authorization: Bearer $CQI_API_KEY" \
  --header 'Accept: application/json' \
  --data-urlencode 'from=2026-07-01T00:00:00Z' \
  --data-urlencode 'to=2026-08-01T00:00:00Z' \
  "https://api-test.crewqi.com/v1/organizations/$CQI_ORGANIZATION_ID/timesheets"
```

## Create a missed complete entry

`POST /timesheets` requires `timesheets:write` and an `Idempotency-Key`.
It creates one closed `MANUAL` entry and never puts the employee into an active
checked-in state. A registered context needs `locationId`. An ad-hoc context
needs a durable address label plus input latitude/longitude; precise input is
validated and stored as restricted evidence but is not echoed by this public
serializer.

```bash
curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer $CQI_API_KEY" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: synthetic-missed-entry-20260731-v1' \
  --data '{
    "employeeId": "00000000-0000-4000-8000-000000000101",
    "context": {
      "kind": "REGISTERED_LOCATION",
      "locationId": "00000000-0000-4000-8000-000000000201"
    },
    "startedAt": "2026-07-31T08:00:00+02:00",
    "endedAt": "2026-07-31T12:15:00+02:00",
    "breakMinutes": 15,
    "reason": "Employee forgot to scan at arrival",
    "overlapOverride": false
  }' \
  "https://api-test.crewqi.com/v1/organizations/$CQI_ORGANIZATION_ID/timesheets"
```

Manual intervals must be complete: end follows start, break is non-negative
and shorter than elapsed time, employee/context belong to the URL-bound
organization, and all timestamps are RFC 3339 instants. Local form clients
must resolve daylight-saving ambiguity to an explicit offset before calling
the API.

## Correct with optimistic concurrency

Read an entry and retain its ETag, such as
`"time-entry:00000000-0000-4000-8000-000000000301:r2"`. Then call
`PATCH /timesheets/{timeEntryId}` with both `If-Match` and a new
`Idempotency-Key`.

```bash
curl --fail-with-body \
  --request PATCH \
  --header "Authorization: Bearer $CQI_API_KEY" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: synthetic-correction-000301-r2' \
  --header 'If-Match: "time-entry:00000000-0000-4000-8000-000000000301:r2"' \
  --data '{
    "startedAt": "2026-07-31T08:00:00+02:00",
    "endedAt": "2026-07-31T12:30:00+02:00",
    "breakMinutes": 15,
    "reason": "Forgotten checkout confirmed by supervisor",
    "overlapOverride": false
  }' \
  "https://api-test.crewqi.com/v1/organizations/$CQI_ORGANIZATION_ID/timesheets/00000000-0000-4000-8000-000000000301"
```

A scanned entry permits effective start/end/break correction only. Employee,
context, location/vehicle, captured position, and source-event references are
immutable evidence. If that anchor is wrong, void the entry and create a
separate manual replacement. A manual-origin entry may supply a complete
replacement `context` in a correction.

An overlap returns `409 TIME_ENTRY_OVERLAP`. To proceed deliberately, repeat
with `overlapOverride: true` and a non-empty `overlapReason`. A stale ETag
returns `412 ETAG_MISMATCH`; missing `If-Match` returns `428 ETAG_REQUIRED`.
Reusing an idempotency key with different input returns a conflict rather than
applying a second change.

## Void without deleting evidence

`POST /timesheets/{timeEntryId}/void` requires the current ETag,
`Idempotency-Key`, and a reason. It creates a new `VOIDED` revision; it does not
delete the time entry, raw work events, or audit history.

```json
{ "reason": "Scan belongs to the wrong context; replacing it manually" }
```

Every create, correction, and void is committed atomically with an
organization audit entry whose actor is the actual API key and whose request
ID is the API response's `X-Request-Id`. The API never fabricates a human actor.

## Audit and corrected exports

`GET /audit` requires `audit:read`. It returns only allowlisted actor labels,
actions, entity references, reason, selected safe before/after fields, overlap
flags, stale manifest IDs, request ID, and timestamp. It never returns
unrestricted database snapshots, typed addresses, precise positions,
credentials, financial data, or private actor IDs.

When a correction changes a previously generated CSV, PDF, payroll dispatch,
or monthly report, the mutation response lists `staleExportManifestIds`. The
old manifest remains traceable. Internal reports rebuild from current
revisions; payroll is never resent automatically. An administrator must issue
an explicit revision-specific replacement whose manifest links to the stale
dispatch.

## Capability gate and errors

`timesheets:write` cannot be granted while the organization's
`timesheet_corrections` entitlement and proven rollout gate are disabled.
Writes attempted with a previously issued scope still return
`409 FEATURE_NOT_ENABLED`. Reads remain available independently.

Other failures use `application/problem+json`: `403 SCOPE_REQUIRED` or
`ACCESS_DENIED`, `404 RESOURCE_NOT_FOUND`, `409 TIME_ENTRY_OVERLAP`,
`412 ETAG_MISMATCH`, `422 VALIDATION_ERROR`, and `428 ETAG_REQUIRED`. Keep and
share `X-Request-Id` with support; never log the bearer token or request bodies
containing typed mobile addresses or coordinates.

Before this in-place pre-adoption alignment is released, issued-key inventory
and available gateway/access logs must confirm there is no external consumer.
If one is found, release stops for compatibility and version review.
