# Business rules

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

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.

```text
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 |

```js
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.

```js
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.
