--- type: SQLite Table title: pay_rates description: Hourly rate history per employee, keyed by the date it takes effect. resource: backend/src/db/index.ts tags: [data-model, sqlite, payroll] timestamp: 2026-08-16T00:00:00Z --- # pay_rates Versioned hourly rate per employee. There's no `valid_to` column — the active rate for a given date is simply the row with the largest `valid_from <= that date` (`getRateAt` in `backend/src/services/payRates.ts`). A new rate implicitly ends the previous one. # Schema | Column | Type | Description | |---|---|---| | `id` | INTEGER PK | autoincrement | | `employee_id` | INTEGER | references [employees](./employees.md) | | `hourly_rate` | REAL | Kč/hour | | `valid_from` | TEXT | `YYYY-MM-DD`, takes effect from this date inclusive | | `created_at` | TEXT | ISO 8601 UTC | `UNIQUE (employee_id, valid_from)` — setting a rate again for the same date overwrites it (`ON CONFLICT ... DO UPDATE`) rather than erroring. # Behavior - **First rate ever set for an employee** defaults `valid_from` to the employee's own `created_at` date when the admin doesn't specify one — so hours already worked before the admin got around to entering a rate still get paid correctly, rather than pricing at 0. This was a real bug found live: an admin set a rate today, and it didn't apply to shifts worked earlier that same month. - **Every later rate change** (a raise) defaults to today instead — not retroactive. This asymmetry lives in `defaultValidFrom` in `backend/src/services/payRates.ts` and is covered by a test in `backend/src/services/payroll.test.ts`. # Related - [employees](./employees.md) - [month_closures](./month-closures.md) / [payroll](./payroll.md) — consume `getRateAt` when computing `earned_estimate` / `base_amount`