Payroll Code Modules Reference
The payroll system computes allowances and deductions through sheet-driven settlement. The arithmetic lives in @hereby/code-modules — one code module per source module, each a pure compute(inputs, parameters) function — and the sheet compute loader (packages/api/src/routers/sheet-rows/compute.ts) loads its inputs from the database and runs it per column. Per-item configuration (item code, source module, value key, rate/multiplier parameters) lives on sheet column metadata + the org parameter store, and AllowanceRecord / DeductionRecord are inserted only on sheet commit (commitSheet, ✓ 커밋). See Payroll Schema Refactor and Payroll Data Provenance.
The former
packages/api/src/calculators/pipeline (AllowancePipeline/DeductionPipeline) became a dead path once settlement moved to sheets — it produced no payroll and was only read for Module Manager metadata — and was removed in 2026-09. This page documents the current path only.
Architecture
The same modules run in the hr-system browser (apps/hr-system/src/sheets/workbook/commit-compute.ts) for previews, so preview and server commit agree by construction.
Settlement Flow
- Load inputs per column
sourceModule—loadInputsByModule(ctx, sourceModule, rows, period)queries the source tables (employment contracts,overtime_records, …) for the sheet period and builds a per-row inputs map. - Run
compute(inputs, parameters)per row × module column — parameters are column metadata + org parameter store merged viamergeWithDefaults. - The column's value key selects one output for the cell.
- On ✓ 커밋,
commitSheetwrites cell values toAllowanceRecord/DeductionRecordwith sheet lineage (sourceSheetId/sourceSheetRowId/sourceSheetColumnKey).
Interfaces (packages/code-modules/src/types.ts)
CodeModule
| Field | Type | Description |
|---|---|---|
sourceModule | SourceModule | Registry key (matches the enum value) |
parameterSchema | ParameterField[] | Configurable parameter definitions (form rendering + validation) |
defaultParameters | Parameters | Default parameter values |
keyDescriptions | KeyDescription[] | Output keys returned by compute() |
compute(inputs, parameters) | function | Pure computation — returns Record<string, number> |
KeyDescription
Used to populate UI hints when configuring a sheet column's value key.
| Field | Type | Description |
|---|---|---|
key | string | Value key identifier |
label | string | Display label |
description | string | Human-readable description |
unit | string | Unit ("원", "시간", "건", "%") |
ParameterField
| Field | Type | Description |
|---|---|---|
key | string | Parameter identifier |
label | string | Display label |
description | string | Human-readable description |
type | enum | number, string, or boolean |
defaultValue | mixed | Default value |
min / max / step | number | Constraints for number type (optional) |
suffix | string | Unit label after the input (e.g., "%", "시간", "원") |
Code Modules (packages/code-modules/src/registry.ts)
Allowance
| Module | Source Module | Inputs loaded from | Description |
|---|---|---|---|
employmentContractModule | employment_contract | employment_contracts | Base salary, 호봉, longevity from the active contract |
overtimeRecordModule | overtime_record | overtime_records + overtime_types | Approved overtime premiums (category multipliers + managed type multipliers) |
fixedIncentiveModule | fixed_incentive | fixed_incentives | Approved fixed incentives in period |
educationSubsidyModule | education_subsidy | education_subsidies | Approved education subsidies in period |
resignationRecordModule | resignation_record | resignation_records etc. | Severance / settlement amounts |
yearEndSettlementModule | year_end_settlement | year_end_settlements | Year-end tax settlement refund / charge |
Deduction
| Module | Source Module | Description |
|---|---|---|
insuranceModule | payroll_rate_config | 4대보험 (National Pension, Health, Employment, Industrial Accident) |
taxBracketModule | tax_bracket_config | Progressive income tax + local income tax |
Manual Module
Columns with sourceModule = "manual" bypass computation. Their unitPrice × multiplier is used directly as the amount.
Overtime Bucket Mapping (silent-underpayment guard)
The overtime_record branch of loadInputsByModule maps OvertimeCategory onto the module's input buckets. The source of truth is .claude/rules/payroll/korean-payroll-compliance.md.
| Category | Buckets filled | Effective rate (defaults) |
|---|---|---|
WEEKDAY | weekday | 1.5x |
NIGHT | weekday + night | 1.5x + 0.5x = 2.0x |
HOLIDAY | holiday | 1.5x |
HOLIDAY_NIGHT | holiday + night | 1.5x + 0.5x = 2.0x |
SATURDAY (deprecated) | weekend | 0.5x (legacy data only) |
- Records with a managed type (
typeId) skip the category buckets and accrue only throughtypeWeightedHours(hours × type multiplier). Night / holiday day counts (night meal allowance etc.) are still counted by category. - Only
approved·report_pending·report_approved·report_rejectedrecords are aggregated. - Within-schedule night / holiday hours (
attendances.nightHours/holidayRegularHours/holidayNightHours) are not fed into this module; sheets reference them throughattendancesource ref columns instead. - Regression guard:
packages/api/src/routers/sheet-rows/payroll-correctness.test.tspins the modulecompute(). The loader-level mapping needs a database and is not unit-tested yet.
Adding a New Code Module
- Implement
CodeModuleinpackages/code-modules/src/modules/my-module.ts(parameterSchema,defaultParameters,keyDescriptions,compute) - Register it in
packages/code-modules/src/registry.tsand export fromindex.ts - Add an input-loading branch to
loadInputsByModuleinpackages/api/src/routers/sheet-rows/compute.ts(and to hr-systemcommit-compute.tsif browser previews need it) - Add a sheet column bound to the new source module (item code, value key, parameters on column metadata + org parameter store)
- Its
keyDescriptionskeys surface in the sheet column config UI and Module Manager
Module Registry Router
The module-registry router reads listCodeModules() and exposes metadata for every source module; the Module Manager app renders it.
const result = await client.moduleRegistry.list.query();
// Returns: { modules: [{ key, name, type, hasCalculator, sourceTables, targetTables,
// outputKeys, parameterSchema, defaultParameters, status, … }],
// summary: { total, withCalculator, withoutCalculator, … } }hasCalculator is a compatibility field name only; it means "a code module is registered for this source module."