Skip to content

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:

Payroll 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, Name and WageTypeNumber must 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 100 and both collectors against the declared values; a missing value on Deduction means 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
The test runs on a temporary copy of 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