PayCaptain Developers

Field validation

Business rules

The rules that span more than one field or more than one record. Enforce them alongside the field rules.

View as Markdown

Field pages cover what a single value must look like. This page covers everything else. Every rule here is mandatory.

Who to send#

PayCaptain processes UK PAYE payroll. Send a person only if they are paid through your PAYE payroll.

Send Do not send
Full-time PAYE employees Contractors and freelancers
Part-time PAYE employees Self-employed individuals
PAYE casual workers Agency workers paid by the agency

Identity and payroll codes#

hrEmployeeId identifies the person. payrollCode identifies one employment. They are different things.

Person        hrEmployeeId "P001"      never changes
Employment 1  payrollCode  "EMP-101"   2022-03-01 to 2023-06-30   ended
Employment 2  payrollCode  "EMP-205"   2024-01-15 to (none)       active
  • One hrEmployeeId per person, the same across all of their employments.
  • Every employment has its own payrollCode. No two records share one.
  • A payrollCode never changes once assigned and is never reused.
  • A person who leaves and returns gets a new payrollCode.
  • Employment periods for the same hrEmployeeId must never overlap.

Dates#

All dates are ISO 8601 calendar dates, YYYY-MM-DD.

Rule Check
Leave date is not before start date leaveDate >= startDate, when leaveDate is set
Pay effective date is inside the employment startDate <= payEffectiveDate <= leaveDate
New employees start pay on day one payEffectiveDate === startDate on first creation
Employments do not overlap No two records with the same hrEmployeeId have overlapping date ranges
function validateEmployeeDates({ startDate, leaveDate, payEffectiveDate, isNew }) {
  // ISO dates compare correctly as strings
  if (leaveDate && leaveDate < startDate) throw new Error("leaveDate is before startDate");
  if (payEffectiveDate < startDate) throw new Error("payEffectiveDate is before startDate");
  if (leaveDate && payEffectiveDate > leaveDate) throw new Error("payEffectiveDate is after leaveDate");
  if (isNew && payEffectiveDate !== startDate) throw new Error("new employee: payEffectiveDate must equal startDate");
}

Pay changes#

payBasis decides which pay field is required: salary (annual) for "Salaried", rateOfPay (hourly) for "Hourly".

Must Whenever payBasis, salary or rateOfPay changes, set payEffectiveDate to the date of that change.

Approval#

Must Only approved shifts may be sent. Your application needs an approval step, and a shift must not reach POST /shift until an authorised person has approved it.

Archiving#

Records are never deleted. To remove a shift or payment from payroll, resubmit it with the same shiftId or paymentId and archived: true.

Change detection#

Must Only send a record when at least one field value has changed since the last successful submission.

Keep the last successfully submitted version of each record and compare before every call.

function hasChanged(current, lastSent) {
  if (!lastSent) return true; // never sent
  return Object.keys(current).some((field) => current[field] !== lastSent[field]);
}

if (hasChanged(employee, lastSentEmployee)) {
  await submitToPayCaptain(employee);
  saveLastSent(employee);
}

If a check fails#

Do not call the API. Log the failure, tell the user what to correct, and submit only once every field rule and every rule on this page passes.