Skip to content

Pays Tool

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).

  • 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.

LevelViewUpdateManage
ExecutiveOwn dataRefreshYes
HeadOwn dataNoNo
ManagerOwn dataNoNo
LeadOwn dataNoNo
EmployeeOwn dataNoNo

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.

The system checks the current user’s people record in this order:

  1. remote_employment_id → fetches from Remote API (compensation only)
  2. xero_employee_id → fetches from Xero Payroll AU API (full payslip support)

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.

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.

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.

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.

When a user clicks a payslip in the UI, GET /api/pays/payslips/:id:

  1. Returns parsed raw_json if already cached (hot path, zero Xero calls).
  2. 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 into raw_json so subsequent views are free.

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.

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.

ComponentPurpose
pays-page.tsxMain page: summary cards and payslip table
payslip-detail-sheet.tsxSlide-out sheet with full payslip breakdown
TablePurpose
pay_summariesCached salary, YTD earnings, next pay date (4hr TTL)
payslipsCached payslip summaries + raw detail JSON