Basic Payroll
This tutorial sets up and tests a simple monthly payroll. The sample data is in Examples/StartPayroll. A running backend is required — see Container Setup.
Payroll Model
A payroll is built from a small set of objects that form the domain model:
Employees belong to a tenant and are assigned to one or more divisions. A payroll targets a single division, which means an employee can be covered by payrolls from different divisions independently.
Payroll Setup
This tutorial uses the Payroll Console and YAML. The same objects can also be created via the REST API or interactively in the web application.
The setup objects and their REST endpoints:
| Object | Web Application | REST Endpoint |
|---|---|---|
| Tenant | Tenants > Add 1) | CreateTenant |
| User | Users > Add | CreateUser |
| Division | Divisions > Add | CreateDivision |
| Employee | Employees > Add | CreateEmployee |
| Regulation | Regulations > Add | CreateRegulation |
| Case / Case Field | Regulation > Cases > Add | CreateCase / CreateCaseField |
| Collector | Regulation > Collectors > Add | CreateCollector |
| Wage Type | Regulation > Wage Types > Add | CreateWageType |
| Payroll / Layer | Payrolls > Add | CreatePayroll / CreatePayrollLayer |
| Payrun | Payruns > Add | CreatePayrun |
1) The initial tenant must be created via the REST API or Payroll Console.
Example payroll Basic.yaml:
# yaml-language-server: $schema=PayrollEngine.Exchange.schema.json
createdObjectDate: 2023-01-01T00:00:00Z # default creation date for all objects
tenants:
- identifier: StartTenant # unique tenant identifier
culture: en-US
users:
- identifier: lucy.smith@foo.com
firstName: Lucy
lastName: Smith
culture: en-US
userType: TenantAdministrator
divisions:
- name: StartDivision
employees:
- identifier: mario.nuñez@foo.com
firstName: Mario
lastName: Nuñez
divisions:
- StartDivision # employee assigned to one or more divisions
regulations:
- name: StartRegulation
cases:
- name: Salary
caseType: Employee # scope: Employee, Company, National, Global
buildActions:
- ^:Salary = Range(^:Salary, 500, 25000) # clamp input to valid range on entry
validateActions:
- '? Within(^:Salary, 500, 25000)' # reject values outside valid range
fields:
- name: Salary
valueType: Money
timeType: CalendarPeriod # value is valid for a full calendar period
collectors:
- name: Income
- name: Deduction
negated: true # deduction collector sums as negative
wageTypes:
- wageTypeNumber: 100 # controls processing order within the payrun
name: Salary
valueActions:
- ^^Salary # read current Salary case field value
collectors:
- Income
payrolls:
- name: StartPayroll
divisionName: StartDivision
layers:
- regulationName: StartRegulation
level: 1 # layer priority — higher level overrides lower
payruns:
- name: StartPayrun
payrollName: StartPayroll
The regulation defines what the payrun calculates. A Case holds employee input data; WageTypes produce numeric results; Collectors aggregate those results. The Payroll links a division to one or more regulation layers, and the Payrun is the recurring job definition.
Identifier,NameandWageTypeNumbermust each be unique within their scope.
Load the payroll definition into the backend with:
Basic.Setup.pecmd
Payroll Test
The test file sets up case data, triggers a payrun job, and declares the expected results in one YAML file. The test runner checks actual vs. expected values automatically.
Example payroll test Basic.Test.et.yaml:
# yaml-language-server: $schema=PayrollEngine.Exchange.schema.json
tenants:
- identifier: StartTenant
payrolls:
- name: StartPayroll
cases:
- userIdentifier: lucy.smith@foo.com
employeeIdentifier: mario.nuñez@foo.com
divisionName: StartDivision
case:
caseName: Salary
values:
- caseFieldName: Salary
value: "5000"
start: 2023-01-01T00:00:00Z
created: 2022-11-04T00:00:00Z # simulates a prior salary change
updateMode: NoUpdate # do not modify the payroll definition
payrunJobInvocations:
- name: StartPayrunJob.Jan23
payrunName: StartPayrun
userIdentifier: lucy.smith@foo.com
jobStatus: Complete # run and complete immediately
periodStart: 2023-01-01T00:00:00Z
evaluationDate: 2023-02-01T00:00:00Z # simulates payrun executed in February
reason: Test Payrun Jan 23
employeeIdentifiers:
- mario.nuñez@foo.com
payrollResults:
- payrunJobName: StartPayrunJob.Jan23
employeeIdentifier: mario.nuñez@foo.com
wageTypeResults:
- wageTypeNumber: 100
value: 5000 # expected: Salary wage type = 5000
collectorResults:
- collectorName: Income
value: 5000 # expected: Income collector = 5000
- collectorName: Deduction # expected: Deduction collector = 0 (no value = pass)
updateMode: NoUpdate # do not modify the tenant
The test file is split into three sections:
- Case data — the employee's salary is set to 5 000, backdated to simulate an earlier entry
- Payrun job — a January 2023 payrun is invoked and completed immediately for the test employee
- Expected results — the test runner compares wage type
100and both collectors against the declared values; a missingvalueonDeductionmeans the expected value is 0
UpdateMode
updateMode: NoUpdate prevents the import from modifying objects that already exist at that level. This allows test files to reference the payroll and tenant without accidentally overwriting their definitions. It is the standard pattern for splitting a payroll across multiple YAML files: the root file defines the structure, child files add objects with updateMode: NoUpdate on the parent.
Run the test with:
Basic.Test.pecmd
mario.nuñez@foo.com — the production employee is not affected.
Next Steps
- Advanced Payroll — multiple regulations and lookup tables
- Forecasts — what-if scenarios without affecting production
- Reports — generating payroll documents
- Examples — all payroll examples and demos
- Articles — blog posts and technical articles