# Pay rates — design

Status: built (November 2026). Decided with the client (Smart Healthcare).

## Decisions

- **One figure per level.** `rate_schedules.base_rate` holds one hourly figure, with:
  - `rate_source`: `own` (the agency typed it) or `fair_work` (the award's ordinary rate, followed when Fair Work
    changes it)
  - `rate_basis`: `casual` (the figure already includes casual loading) or `ordinary` (it doesn't). Fair Work rates
    are always `ordinary`.
- **The other figure is calculated**, never stored: casual = ordinary × (1 + the award's casual loading), ordinary =
  casual ÷ (1 + loading), each rounded half up to the cent in whole cents (`PayRateService::withLoading` /
  `withoutLoading`). Storing both would let them drift.
- **Casual loading belongs to the award**: `casual_loading` in `database/reference/awards.php` → central
  `awards.casual_loading` (25% in the Nurses, Aged Care and SCHADS awards). Not hard-coded and not a tenant setting:
  it's law. A profession without an award uses 25%.
- **Employment type is on the worker** (`hcps.employment_type`: casual (default) / part_time / full_time), set on the
  profile (`PATCH /hcps/{id}/employment-type`). Casual workers get the casual rate, permanent ones the ordinary rate
  (`PayRateService::forWorker`). Nothing pays from it yet: Participants and Payroll will.
- **Agency billing always uses the casual rate** (`ResolvedRates::baseRate()` → `PayRateService::casualRate()`):
  locum work is casual, whoever works the shift.
- **Penalty rates (afternoon, Saturday, overtime…) are not stored here.** They're results of the ordinary rate and
  each award's definitions: Award Interpretation's job. The agency's **loadings** stay what they are: the commercial
  percentages charged to service recipients (and Smart Healthcare's legacy payroll lines until Award Interpretation
  is on).
- **Below award is a warning, never a block.**

## Smart Healthcare

Every existing rate became `own` + `casual` (the column defaults): the agency entered Fair Work's casual figures (or
its own) as the base, and billing always used `base_rate` as it is, so `casualRate()` returns exactly the stored
figure. Verified by re-pricing 2,500 past approved shifts through `ShiftService::calculatePrice` before and after the
change: 1,362 priced, 1,138 failing for reasons that predate it (no rate on that old date), every figure identical.
Their levels aren't mapped to award pay points yet, so they show "Not mapped" until they are.

## How it's built

| Piece | What |
|---|---|
| `PayRateService` | casual / ordinary figures, `forWorker`, the award's loading, Fair Work's figures for a level (`fairWork`, `fairWorkNext`), `checks()` |
| `RateScheduleService` | `create` (own or Fair Work; replaces a change that hasn't started yet), `applyFairWork`, `raise`, `overview` |
| `AwardRateService::nextChange` | the next operative date Fair Work has published for a pay point |
| `StarterPackService` | award-pack levels start on Fair Work's rate from today, markup 0 |
| API | `GET /pay-rates[?profession_id=]`, `POST /pay-rates/fair-work`, `POST /pay-rates/raise`, `POST /rate-schedules` (+ `rate_source`, `rate_basis`), `PATCH /hcps/{id}/employment-type` |
| SPA | Workforce → **Pay rates** (`PayRatesPage`, `components/payRates`), the profession's Levels & pay rates tab (same table, one profession), dashboard `PayRatesBanner`, worker profile Employment |
| Loadings | moved to Agency Billing → Billing configuration → **Loadings** (`/admin/accounting/loadings`; the old `/admin/settings/pay-rates` redirects) |

### Checks (per level, casual compared with Fair Work's casual)

| Code | When |
|---|---|
| `below_award` | an own rate is under Fair Work's casual rate today |
| `update_available` | a Fair Work-linked rate is on an older Fair Work figure |
| `update_upcoming` | Fair Work has published a later change (e.g. 1 July) not yet scheduled |
| `below_upcoming` | an own rate will be under that later Fair Work rate |
| `no_rate` | nothing in force: shifts at the level can't be priced |
| `unmapped` | no award pay point (information) |

Fair Work changes are **prompted, not applied**: the weekly `awards:sync-rates` brings them in, Pay rates and the
dashboard banner ask, and **Use Fair Work rates** applies today's figure (if older) and schedules the next one from
its start date, keeping the markup.

### Versioning

Unchanged: a new rate closes the open one the day before it starts. New: a change that hasn't started yet is
replaced (soft-deleted, its predecessor reopened) when a new one starts on or before it, so the 1 July figures can
be corrected before 1 July. A replaced row is closed before it's deleted: the one-open-rate index ignores
`deleted_at`.

## Later

- Payroll export drivers, Payroll ID and pay item mapping (discussed, not part of this).
- Award Interpretation reading `forWorker()` for permanent workers; penalty rates per award.
- Read-only Fair Work penalty figures per level on Pay rates (from the synced MAPD data).
- Email when Fair Work publishes new rates.
