feat(payroll): include payroll in export, import and factory reset; map pay cadences; cover processing

Merged
EV
15d ago
+1140-85

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_item were 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's employeeId nulled by the employees the same reset deleted.
  • Item 4 — PayPeriodEnum (employee, TWICE_PER_MONTH) and PayrollFrequencyEnum (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, ANNUALLY and a missing value answer undefined. A Map, so a value read from outside can never resolve an inherited member such as constructor; the reverse direction is derived from the forward one. Neither enum is renamed.

@gauzy/core

  • RepositoriesService (export / import): registers PayrollRun then PayrollItem, after Payment, i.e. after every table they hold a foreign key to (tenant, organization, user, employee, run), so the import remaps payrollRunId, employeeId and approvedByUserId through rows imported earlier. Both carry excludeGlobalDefaults: 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 every testing/ folder; the add-item spec now uses it instead of its inline copy) and a small core/testing/registered-repositories fixture that runs a service's registerCoreRepositories() 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. Labels MENU.IMPORT_EXPORT.PAYROLL_RUN / PAYROLL_ITEM added to all 14 dictionaries, translated with the terms the existing ORG_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 with excludeGlobalDefaults.
  • 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) — two process() 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.3 pays 2500.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 Expense rows, Payment rows, 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

  • RepositoriesService registers payroll_run before payroll_item, each after every table they hold a foreign key to, so import remaps payrollRunId, employeeId, and approvedByUserId through rows imported earlier; both use excludeGlobalDefaults.
  • FactoryResetService deletes payroll lines, then runs, before the employees the lines are paid to.
  • payroll-frequency.util.ts maps PayPeriodEnum ↔ PayrollFrequencyEnum both ways; cadences only one side knows (NONE, QUARTERLY, ANNUALLY) answer undefined.
  • 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.