--- type: SQLite Table title: payroll description: Computed + admin-adjusted pay per employee per month — base hours pay, tips, bonus, other, and payment status. resource: backend/src/db/index.ts tags: [data-model, sqlite, payroll] timestamp: 2026-08-16T00:00:00Z --- # payroll One row per employee per period, created the first time the admin either saves a draft adjustment or locks the month. # Schema | Column | Type | Description | |---|---|---| | `id` | INTEGER PK | autoincrement | | `employee_id` | INTEGER | references [employees](./employees.md) | | `period` | TEXT | `YYYY-MM` | | `worked_minutes` | INTEGER | snapshotted at last save/lock, not live | | `base_amount` | REAL | worked hours × the rate active on each shift's own date — see [pay_rates](./pay-rates.md) | | `tips_amount` / `bonus_amount` / `other_amount` | REAL | admin-entered, default 0 | | `final_amount` | REAL | sum of the four amounts above | | `status` | TEXT | `draft` \| `ready` \| `paid` | | `payment_date` | TEXT \| NULL | set when marked `paid` | | `updated_at` | TEXT | ISO 8601 UTC | `UNIQUE (employee_id, period)`. # Status lifecycle ``` draft --admin locks the month closure--> ready --admin marks paid--> paid ready --admin reopens--> draft ``` - **`draft`**: editable. `saveDraftAdjustments` (`backend/src/services/payroll.ts`) recomputes `base_amount` / `worked_minutes` fresh from [attendance_events](./attendance-events.md) on every save, and requires the linked [month_closures](./month-closures.md) row to be at least `confirmed` (not still `waiting_employee`). - **`ready`**: frozen. Set by `lockAndFinalizePayroll`, which locks the closure in the same call. `saveDraftAdjustments` refuses to touch a non-`draft` row — this is what turns "already locked" into the right error message instead of the generic "not confirmed yet" one (a real bug caught by `payroll.test.ts`: the two checks used to overlap). - **`paid`**: terminal. Set by `markPaid`, stamps today's date. Not reopenable — a mistake past this point needs a manual correction, not a silent rewrite. This collapses the source system's four-stage accountant pipeline (`READY_FOR_ACCOUNTANT` / `PROCESSING` / `PROCESSED` / `PAID` in `gscript/PayrollService.js`) down to two — `ready` and `paid` — since there's no separate accountant role here for the intermediate stages to mean anything. # Related - [month_closures](./month-closures.md) - [pay_rates](./pay-rates.md) - [Closure API](/docs/api/closure-routes.md), [Admin API](/docs/api/admin-routes.md)