> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ontime.hosai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# The day engine

> How punches become day records, why today's data keeps changing, and what makes a day final.

If you are reading attendance out of OnTime, this is the page that determines whether your numbers are right.

## Punches are facts, days are derivations

A **punch** is an immutable event. A **day record** is a derivation over that day's punches, the employee's shift template, approved leave, approved overtime, and the holiday calendar.

```mermaid theme={null}
flowchart LR
  P[Punches] --> E[Derivation]
  S[Shift template] --> E
  L[Approved leave] --> E
  O[Approved overtime] --> E
  H[Holiday calendar] --> E
  E --> D[Day record]
```

The day record is **re-derived**, not incrementally patched. Every writer — a punch landing, an approval, the nightly close — runs the same derivation and produces the same row from the same inputs.

<Note>
  This is why approvals are described as *inputs* to the day rather than writers of it. An approved overtime request is read again on every later derivation, so a manager's decision survives the employee's next punch on that date.
</Note>

## Deduplication

Biometric punches deduplicate on the triple `(deviceId, transId, logTime)`. A terminal replaying its buffer after a network outage cannot double-count — the same event arriving twice is recognised and ignored.

<Warning>
  If you are ingesting attendance and reconciling against your own records, do not assume a punch you see today is one you have not seen before. Use the punch's own `id`.
</Warning>

## Today is not final

<Warning>
  **A day's record changes until the day is closed.** Reading today's attendance gives you a provisional answer.

  Most consequentially: `Absent` is **never written to a day still in progress**. An employee who has not punched yet is not a no-show, so a query for today's absences returns nothing — even at 4pm, even for someone who never turned up.
</Warning>

## The nightly close

A daily sweep finalizes every employee's record for the previous day.

Two properties an integration should rely on:

<CardGroup cols={2}>
  <Card title="It closes yesterday, never today" icon="calendar">
    Closing a day still in progress would hand the derivation a live clock. So `Absent` for a given date lands the following morning, not at midnight.
  </Card>

  <Card title="It re-derives rather than asserts" icon="refresh-cw">
    Running it twice, or after a punch has already reconciled the day, produces the same row. It is a check, not a job — a missed run is picked up by the next one.
  </Card>
</CardGroup>

## What this means for your queries

<Steps>
  <Step title="Read closed days, not today">
    For anything where the number matters — payroll feeds, absence reports, compliance — query dates that have been closed. Yesterday is safe from mid-morning; today is not safe at all.
  </Step>

  <Step title="Expect a day to change after you read it">
    An approved regularization rewrites a past day, and payroll data is corrected all the time. A monthly export should re-read the month rather than accumulating dailies.
  </Step>

  <Step title="Treat a frozen day as final">
    A day inside a frozen payroll month will not re-derive. That is the strongest "this is settled" signal available.
  </Step>
</Steps>

## Frozen days

Freezing a payroll cycle marks every day record in the month frozen. Afterwards:

* New punches for those dates are refused with `409 day_frozen`.
* Approvals that would rewrite one of those days are refused, and the **whole approval rolls back** — the request stays pending rather than being marked approved with no effect.

An administrator can lift the freeze on an individual day through an audited override. There is no bulk unfreeze.

## Day statuses

| Status      | Meaning                                                     |
| ----------- | ----------------------------------------------------------- |
| `Present`   | Worked.                                                     |
| `Absent`    | Expected, no attendance. Only written on a closed day.      |
| `Half`      | Half-day leave, or hours below the shift's half-day cutoff. |
| `Leave`     | Covered by approved leave.                                  |
| `WeeklyOff` | A rest day per the shift template.                          |
| `Holiday`   | On the organization's holiday calendar.                     |

<Note>
  **Two known gaps in status derivation.** A holiday that is *worked* still reconciles as `Present` rather than a distinct holiday-worked status. And a weekly-off day with no punches reconciles to `Absent` rather than `WeeklyOff` in cases outside the shift template's configured weekly offs.

  If you compute loss of pay from `Absent`, the second one matters — cross-check against the employee's shift and the holiday calendar rather than trusting `Absent` alone.
</Note>
