Pays Tool
Overview
Section titled “Overview”The Pays tool gives employees visibility into their pay information. It supports two payroll providers: Xero Payroll AU (domestic employees) and Remote (international EOR employees). It shows a summary of annual salary, year-to-date earnings, and next pay date, plus a list of historical payslips with full detail drill-down (Xero only).
Features
Section titled “Features”- Annual Salary — Current salary from Xero pay template or Remote compensation
- Earnings YTD — Gross earnings summed for the current Australian financial year (Xero only)
- Next Pay Day — Payment date from the next draft pay run (Xero only)
- Payslip list — Historical payslips with gross, tax, super, and net pay (Xero only)
- Payslip detail — Breakdown of earnings lines, deductions, tax, superannuation, and leave accruals (Xero only)
For Remote employees, the tool shows annual salary and pay calendar (e.g. “Remote · Australia”) but not individual payslips — those are managed in the Remote platform.
Permissions
Section titled “Permissions”| Level | View | Update | Manage |
|---|---|---|---|
| Executive | Own data | Refresh | Yes |
| Head | Own data | No | No |
| Manager | Own data | No | No |
| Lead | Own data | No | No |
| Employee | Own data | No | No |
All users can only see their own pay data. The tool requires a payroll mapping (Xero employee ID or Remote employment ID) in Settings > Connections > User Mappings.
Provider Routing
Section titled “Provider Routing”The system checks the current user’s people record in this order:
remote_employment_id→ fetches from Remote API (compensation only)xero_employee_id→ fetches from Xero Payroll AU API (full payslip support)
Caching
Section titled “Caching”Pay summary data is cached for 4 hours to avoid excessive API calls. Stale data is served if the provider is unavailable. Users can force a refresh using the Sync button.
Sync Architecture (Xero)
Section titled “Sync Architecture (Xero)”The Xero sync is split into a cheap bulk sync and an on-demand detail fetch to stay well inside Xero’s 60-calls/min and 5,000-calls/day caps.
Bulk sync — payruns only
Section titled “Bulk sync — payruns only”refreshPayrollData (interactive) and the scheduled cron both fetch only the PayRuns endpoint. Each pay run already includes a Payslips[] array with per-employee Wages, Deductions, Tax, Super, Reimbursements, NetPay. The bulk sync writes those summary fields straight into the payslips table with raw_json = NULL.
Cost: 1 + (new payruns) Xero calls per sync. A non-payday cron firing makes 1 call; payday firings make 2–3.
Capture-then-claim (unmapped employees)
Section titled “Capture-then-claim (unmapped employees)”The cron captures payslips for every employee in each pay run, not just the ones currently mapped to a Nucleus person. Unmapped rows land in the payslips table with person_id = NULL and xero_employee_id set to the raw Xero EmployeeID.
When a mapping is created — manually via the User Mappings table or automatically by the bulk auto-match — claimXeroPayslips runs an UPDATE payslips SET person_id = ? WHERE xero_employee_id = ? AND person_id IS NULL. The newly mapped person instantly has access to everything the cron has captured since it started running, no refresh needed. pay_summaries.earnings_ytd is recomputed from the claimed rows in the same operation.
The cron also runs an idempotent self-heal at the start of Phase A — if any rows ended up with person_id = NULL but the corresponding people.xero_employee_id now exists, they get claimed. This protects against drift if a mapping was set via a direct DB write or before the claim hook shipped.
GET /payslips, /payslips/stats, and /payslips/:id all filter by person_id, so unmapped rows are invisible to the UI until claimed.
On-demand payslip detail
Section titled “On-demand payslip detail”When a user clicks a payslip in the UI, GET /api/pays/payslips/:id:
- Returns parsed
raw_jsonif already cached (hot path, zero Xero calls). - Otherwise calls
fetchPayItems+fetchEmployee+fetchPayslipDetail(≈ 4 Xero calls), enriches the result with named earnings/leave types and a salary vs non-salary split, then persists intoraw_jsonso subsequent views are free.
last_increase_at backfill
Section titled “last_increase_at backfill”The last_increase_at signal needs line-item breakdowns of the two most recent payslips. It’s not blocking sync — a scheduled cron Phase B drips this in (≤ 10 Xero calls per firing). On-demand detail clicks also opportunistically refresh the signal when one of the top-2 payslips gets its raw_json populated.
Scheduled cron
Section titled “Scheduled cron”The payroll cron runs every 6 hours alongside the existing Xero invoice cron. Phase A pulls new pay runs tenant-wide; Phase B drips through the salary-increase backfill within a per-firing budget.
Components
Section titled “Components”| Component | Purpose |
|---|---|
pays-page.tsx | Main page: summary cards and payslip table |
payslip-detail-sheet.tsx | Slide-out sheet with full payslip breakdown |
Data Model
Section titled “Data Model”| Table | Purpose |
|---|---|
pay_summaries | Cached salary, YTD earnings, next pay date (4hr TTL) |
payslips | Cached payslip summaries + raw detail JSON |