feat(payroll): include payroll in export, import and factory reset; map pay cadences; cover processing
Summary
First slice of #10184 (the follow-ups #9670 left out): payroll runs and their lines now travel with an organization's export, import and factory reset; the two pay-cadence vocabularies get a mapping; and the money paths of a run (process, totals, statistics) get the tests the issue asked for. No schema change, no new dependency, no behaviour change outside these lists.
Why
#10184 lists seven gaps. This PR covers the three that need no product decision:
- Item 3 —
payroll_run/payroll_itemwere on neither hand-written repository list, so an export left payroll out of the archive, an import skipped it, and a factory reset kept the organization's payroll with every line'semployeeIdnulled by the employees the same reset deleted. - Item 4 —
PayPeriodEnum(employee,TWICE_PER_MONTH) andPayrollFrequencyEnum(run,SEMI_MONTHLY) had no mapping. - Item 7 — only an add-item spec existed;
process()'s conditional UPDATE, the integer-cent totals and the per-currency statistics were untested.
What changed
@gauzy/contracts
payroll-frequency.util.ts:payPeriodToPayrollFrequency()/payrollFrequencyToPayPeriod(). WEEKLY, BI_WEEKLY, TWICE_PER_MONTH↔SEMI_MONTHLY, MONTHLY map both ways;NONE,QUARTERLY,ANNUALLYand a missing value answerundefined. AMap, so a value read from outside can never resolve an inherited member such asconstructor; the reverse direction is derived from the forward one. Neither enum is renamed.
@gauzy/core
RepositoriesService(export / import): registersPayrollRunthenPayrollItem, after Payment, i.e. after every table they hold a foreign key to (tenant, organization, user, employee, run), so the import remapspayrollRunId,employeeIdandapprovedByUserIdthrough rows imported earlier. Both carryexcludeGlobalDefaults: true: payroll never has shared default rows, so a tenantless row is never stamped into the exporting tenant's archive. TypeORM-only constructor params, like the other recent registrations; edits are limited to the payroll entries.FactoryResetService: deletes the reset organizations' payroll lines, then their runs, before the employees the lines are paid to (same scope as every other table: tenant + the reset organizations).- Tests: a shared SQLite payroll store fixture (
payroll-run/testing/, excluded from the lib build like everytesting/folder; the add-item spec now uses it instead of its inline copy) and a smallcore/testing/registered-repositoriesfixture that runs a service'sregisterCoreRepositories()as written against stub repositories.
apps/gauzy + @gauzy/ui-core/i18n
- The export page sends the server only the tables it lists (
GET /export/filter), so without an entry payroll would never reach an archive from the UI even with "All entities" ticked. Adds a "Payroll Run" group with "Payroll Item", selected by default like every other table. LabelsMENU.IMPORT_EXPORT.PAYROLL_RUN/PAYROLL_ITEMadded to all 14 dictionaries, translated with the terms the existingORG_PAYROLL_*permission labels use.
Flags / settings and defaults
None. Who can export, import or reset is unchanged: export needs EXPORT_ADD, import IMPORT_ADD, reset ACCESS_DELETE_ALL_DATA — all ADMINISTRATION-group permissions (Super Admin / Admin by default), whose holders already export expenses, incomes, payments and employee salaries (recurring expenses). An archive without payroll CSVs (any export made before this change) imports exactly as before: a clean import wipes only the tables the archive carries.
Tests
All run locally on this branch (Node 26, Windows):
npx jest -c packages/contracts/jest.config.ts --rootDir packages/contracts payroll-frequency
-> 1 suite, 16 tests passed
NODE_OPTIONS=--experimental-vm-modules npx jest -c packages/core/jest.config.ts --rootDir packages/core "payroll-run/|repositories\.service|factory-reset"
-> 6 suites, 67 tests passed (the four new/changed payroll suites plus the existing
repositories.service.skip-export and payroll-run.resolver specs)
npx jest -c apps/gauzy/jest.config.ts --rootDir apps/gauzy import-export/export
-> 1 suite, 3 tests passed
New specs:
payroll-frequency.util.spec.ts— both directions, the unmapped cadences, missing values, inherited-member names, and a drift guard that fails when either enum gains a member nobody decided.repositories.service.payroll.spec.ts— both tables registered; each registered after every table it holds a many-to-one foreign key to, read from the entities' own relation metadata (so a reference added later is checked too, with a check that the lookup is not vacuous); both kept in the graph withexcludeGlobalDefaults.factory-reset.service.payroll.spec.ts— lines then runs deleted, scoped to the tenant and the reset organizations, before employees.payroll-run.service.process.spec.ts(real SQLite) — twoprocess()calls that both read the run as APPROVED: exactly one pays, the other is refused by the claim; paid / draft / pending / cancelled runs refused and left as they were; PROCESSING processable; another organization's run unreachable; totals recomputed at process time in integer cents (0.1 + 0.2 + 2500.3pays2500.6); statistics only for the organization's paid runs, per currency, with distinct employees.export.component.spec.ts— payroll offered and selected by default, unticking it drops both tables, labels present in every dictionary.
Type-check: npx tsc -p packages/contracts/tsconfig.lib.json --noEmit and npx tsc -p packages/core/tsconfig.lib.json --noEmit both clean; the export page change is covered by its spec (ts-jest compiles it), the Angular app was not built here. The gauzy spec needs the git-ignored packages/ui-config/src/lib/environments/environment*.ts, generated with npx ts-node ./.scripts/configure.ts --environment=dev as the app's own scripts do. Lint: npx eslint --quiet clean on every touched file except one pre-existing no-unsafe-function-type error in repositories.service.ts (isExportSkipped(... as Function), not a line this PR touches).
Manual verification still needed
- A real export → clean import round trip on PostgreSQL with payroll rows (not run here; the ordering is covered by the metadata-driven spec, and the import's FK remapping is the generic path every other table uses).
- The export page in a browser (component logic and labels are covered by the spec).
- A factory reset on a populated organization.
Risk and rollback
Low. Additive list entries plus specs and labels; no migration. Rollback is reverting the PR: payroll simply drops out of export, import and reset again.
Left for later (rest of #10184)
- Item 1 (draft lines from employees, recurring salary expenses and time logs) and item 2 (processing a run writes money rows + a CQRS event in the same transaction) — item 2 needs the owner to decide whether a paid run produces
Expenserows,Paymentrows, or both. - Item 5 (per-item currency) is a schema change; item 6 (overlapping-period warning vs constraint) is a product decision; the payroll UI is its own piece of work.
Part of #10184
🤖 Generated with Claude Code
Summary by cubic
Adds payroll runs and their lines to organization export, import, and factory reset, so payroll now travels with the organization's data instead of being left out of the archive, skipped on import, or kept behind with nulled employee IDs after a reset.
Also adds a two-way mapping between employee pay periods and payroll-run frequencies, and covers payroll processing, totals, and statistics with new tests.
What changed
RepositoriesServiceregisterspayroll_runbeforepayroll_item, each after every table they hold a foreign key to, so import remapspayrollRunId,employeeId, andapprovedByUserIdthrough rows imported earlier; both useexcludeGlobalDefaults.FactoryResetServicedeletes payroll lines, then runs, before the employees the lines are paid to.payroll-frequency.util.tsmapsPayPeriodEnum↔PayrollFrequencyEnumboth ways; cadences only one side knows (NONE,QUARTERLY,ANNUALLY) answerundefined.- The export page now offers a "Payroll Run" group with "Payroll Item", selected by default, with labels added to all 14 dictionaries.
- New specs cover the conditional claim that prevents a run paying twice, integer-cent totals, per-currency statistics, and the registration/reset ordering — the ordering checks read each entity's foreign keys from its own relation metadata.
No schema change, no new dependency, no behavior change outside these lists; archives made before this change still import exactly as before.