@kernhq/module-hr 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +16 -0
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/exports.d.ts +459 -0
- package/dist/contract/exports.d.ts.map +1 -0
- package/dist/contract/exports.js +331 -0
- package/dist/contract/exports.js.map +1 -0
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/contract/permissions.d.ts +8 -0
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +32 -3
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/router.d.ts +272 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +67 -0
- package/dist/contract/router.js.map +1 -1
- package/dist/server/router.d.ts +283 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +116 -0
- package/dist/server/router.js.map +1 -1
- package/dist/server/services/exports.d.ts +321 -0
- package/dist/server/services/exports.d.ts.map +1 -0
- package/dist/server/services/exports.js +765 -0
- package/dist/server/services/exports.js.map +1 -0
- package/package.json +1 -1
- package/src/client/messages.ts +13 -0
- package/src/contract/capabilities.ts +16 -0
- package/src/contract/exports.ts +354 -0
- package/src/contract/index.ts +1 -0
- package/src/contract/permissions.ts +34 -3
- package/src/contract/router.ts +73 -0
package/dist/server/router.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ApprovalService } from './services/approvals.js';
|
|
|
12
12
|
import { AttendanceService } from './services/attendance.js';
|
|
13
13
|
import { accessLogSort, HrAuditService } from './services/audit.js';
|
|
14
14
|
import { inForceOn, todayIso } from './services/db.js';
|
|
15
|
+
import { assembleExport, exportManifest, PayrollExportService, } from './services/exports.js';
|
|
15
16
|
import { LedgerService, MINUTES_PER_DAY, yearOf } from './services/ledger.js';
|
|
16
17
|
import { PeopleService } from './services/people.js';
|
|
17
18
|
import { hashConfig, PolicyService } from './services/policies.js';
|
|
@@ -446,6 +447,7 @@ export function implement_(kernel) {
|
|
|
446
447
|
const approvals = new ApprovalService(kernel, subjects.appliersFor(null));
|
|
447
448
|
const privacy = new PrivacyService();
|
|
448
449
|
const reports = new ReportsService(resolve);
|
|
450
|
+
const payroll = new PayrollExportService(reports);
|
|
449
451
|
const audit = new HrAuditService(kernel, access);
|
|
450
452
|
const db = kernel.database;
|
|
451
453
|
const settingsOf = (workspaceId) => kernel.settings.module(workspaceId, MODULE_ID, HrSettings);
|
|
@@ -3538,6 +3540,92 @@ export function implement_(kernel) {
|
|
|
3538
3540
|
};
|
|
3539
3541
|
})),
|
|
3540
3542
|
},
|
|
3543
|
+
// ================================================================= payroll export
|
|
3544
|
+
/**
|
|
3545
|
+
* The monthly handover to whoever runs payroll, for one legal entity, frozen at v1.
|
|
3546
|
+
*
|
|
3547
|
+
* **Three keys, and the middle one is new.** `hr.payroll.export` ships in this change because
|
|
3548
|
+
* this is the change that makes a writer exist — it is granted to nobody by default, like
|
|
3549
|
+
* `hr.person.view_sensitive` and `hr.privacy.manage`, so on a fresh workspace only an owner can
|
|
3550
|
+
* reach it. On top of it, the same second-check rule the reports follow:
|
|
3551
|
+
* `hr.attendance.view_team` for the hours file and `hr.leave.view_team` for the leave file,
|
|
3552
|
+
* because an export must not answer what the row-level procedure would refuse.
|
|
3553
|
+
*
|
|
3554
|
+
* **One capability gate, and it carries two more inside it.** `payroll_export` declares
|
|
3555
|
+
* `dependsOn: ['core', 'periods', 'attendance']`, and `kernel.capabilities` prunes a capability
|
|
3556
|
+
* whose dependencies are off — so a workspace with attendance off has no day sheet to hand over
|
|
3557
|
+
* and this answers 404, and a workspace with periods off would have every day open, the refusal
|
|
3558
|
+
* below would fire on every call, and a switch that only ever produces an error is worse than no
|
|
3559
|
+
* switch. Gating on the three separately would say the same thing three times and let them drift;
|
|
3560
|
+
* the dependency list is where that belongs.
|
|
3561
|
+
*
|
|
3562
|
+
* That pruning is measured rather than assumed, because the whole gate rests on it: against a real
|
|
3563
|
+
* kernel, `payroll.export.v1` answers `NOT_FOUND` with `attendance` off, with `periods` off, and
|
|
3564
|
+
* with `payroll_export` itself off, and reaches the handler only when all three are on. Worth
|
|
3565
|
+
* knowing when reading a support ticket: all three refusals say *`hr.payroll_export` is not
|
|
3566
|
+
* enabled*, so an administrator looking at a switch that is plainly on is looking at a dependency
|
|
3567
|
+
* that is off.
|
|
3568
|
+
*
|
|
3569
|
+
* **Nothing here writes**, including no `sensitive_access_log` row: this export reads no
|
|
3570
|
+
* sensitive field. Adding `iban` would make it a bulk decrypt of every employee's bank account
|
|
3571
|
+
* and would owe one audit row per person with `via: 'export'` — a different procedure with its
|
|
3572
|
+
* own key, not a column on this one.
|
|
3573
|
+
*/
|
|
3574
|
+
payroll: {
|
|
3575
|
+
export: {
|
|
3576
|
+
/**
|
|
3577
|
+
* One entity, one period, two CSVs and a manifest.
|
|
3578
|
+
*
|
|
3579
|
+
* Synchronous, and that is checked rather than preferred: `core.files.createUpload` needs a
|
|
3580
|
+
* user principal and returns a presigned PUT for a browser, so a background job cannot mint a
|
|
3581
|
+
* `FileObject` at all, and writing bytes straight into `kernel.storage` would orphan an
|
|
3582
|
+
* object `core.files.*` cannot see and nothing will ever delete.
|
|
3583
|
+
*
|
|
3584
|
+
* It throws the first refusal `collect` found rather than emitting a row of zeros — an open
|
|
3585
|
+
* period without `draft`, an entity with nobody in it, or somebody with no employment record
|
|
3586
|
+
* covering their days here. The preview below returns all of them instead, so a screen can
|
|
3587
|
+
* show the reader every reason at once before anybody downloads anything.
|
|
3588
|
+
*/
|
|
3589
|
+
v1: scoped.payroll.export.v1
|
|
3590
|
+
.use(cap('payroll_export'))
|
|
3591
|
+
.use(requires('hr.payroll.export'))
|
|
3592
|
+
.use(requires('hr.attendance.view_team'))
|
|
3593
|
+
.use(requires('hr.leave.view_team'))
|
|
3594
|
+
.handler(({ input }) => db.withWorkspace(input.workspaceId, async (tx) => {
|
|
3595
|
+
const data = await payroll.collect(tx, input);
|
|
3596
|
+
const [first] = data.refusals;
|
|
3597
|
+
// `conflict` rather than `badRequest`: the request is well formed and the state is not
|
|
3598
|
+
// ready. `hr.period.not_locked` mirrors the spelling of `hr.period.locked`, which
|
|
3599
|
+
// `PolicyService.assertOpen` throws pointed the other way.
|
|
3600
|
+
if (first)
|
|
3601
|
+
throw KernError.conflict(first.message, first.code);
|
|
3602
|
+
return assembleExport(payrollAssembly(data, input.draft));
|
|
3603
|
+
})),
|
|
3604
|
+
/**
|
|
3605
|
+
* The same rows as JSON, with no file and no refusal thrown.
|
|
3606
|
+
*
|
|
3607
|
+
* The manifest it carries is the manifest the export *would* write, filenames included, so a
|
|
3608
|
+
* screen can name the files before they exist and a reader can see `DRAFT` in the name before
|
|
3609
|
+
* choosing to send it.
|
|
3610
|
+
*/
|
|
3611
|
+
preview: scoped.payroll.export.preview
|
|
3612
|
+
.use(cap('payroll_export'))
|
|
3613
|
+
.use(requires('hr.payroll.export'))
|
|
3614
|
+
.use(requires('hr.attendance.view_team'))
|
|
3615
|
+
.use(requires('hr.leave.view_team'))
|
|
3616
|
+
.handler(({ input }) => db.withWorkspace(input.workspaceId, async (tx) => {
|
|
3617
|
+
const data = await payroll.collect(tx, input);
|
|
3618
|
+
return {
|
|
3619
|
+
manifest: exportManifest(payrollAssembly(data, input.draft)),
|
|
3620
|
+
refusals: data.refusals,
|
|
3621
|
+
exportable: data.refusals.length === 0,
|
|
3622
|
+
totals: data.totals,
|
|
3623
|
+
hours: data.hours,
|
|
3624
|
+
leave: data.leave,
|
|
3625
|
+
};
|
|
3626
|
+
})),
|
|
3627
|
+
},
|
|
3628
|
+
},
|
|
3541
3629
|
// ================================================================= privacy
|
|
3542
3630
|
/**
|
|
3543
3631
|
* Subject access, erasure and retention.
|
|
@@ -4103,6 +4191,34 @@ export function implement_(kernel) {
|
|
|
4103
4191
|
rows,
|
|
4104
4192
|
};
|
|
4105
4193
|
}
|
|
4194
|
+
/**
|
|
4195
|
+
* The provenance half of a payroll export's manifest — the part that is the router's to supply.
|
|
4196
|
+
*
|
|
4197
|
+
* `kernVersion` is the platform version this image was built as, recorded so a file can be traced
|
|
4198
|
+
* back to what wrote it. It is **not** the contract identity: `PAYROLL_EXPORT_CONTRACT` is a
|
|
4199
|
+
* literal that moves only when the column set does, and reading the module version into that field
|
|
4200
|
+
* would rename the format on every patch release.
|
|
4201
|
+
*
|
|
4202
|
+
* The three permissions are the ones the middlewares above actually asked for, written out rather
|
|
4203
|
+
* than inferred, for the reason `ReportScope` exists: two readers must never hold one file's
|
|
4204
|
+
* figures under one title without being told which grants produced them.
|
|
4205
|
+
*/
|
|
4206
|
+
function payrollAssembly(data, draft) {
|
|
4207
|
+
return {
|
|
4208
|
+
entity: data.entity,
|
|
4209
|
+
period: data.period,
|
|
4210
|
+
draft,
|
|
4211
|
+
generatedAt: new Date().toISOString(),
|
|
4212
|
+
kernVersion: kernel.version,
|
|
4213
|
+
permissions: ['hr.payroll.export', 'hr.attendance.view_team', 'hr.leave.view_team'],
|
|
4214
|
+
dayLengthMinutes: MINUTES_PER_DAY,
|
|
4215
|
+
population: data.population,
|
|
4216
|
+
counted: data.counted,
|
|
4217
|
+
attendance: data.attendance,
|
|
4218
|
+
hours: data.hours,
|
|
4219
|
+
leave: data.leave,
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4106
4222
|
/** The request a retry is a retry *of*, or undefined the first time a key is seen. */
|
|
4107
4223
|
async function byIdempotencyKey(tx, workspaceId, key) {
|
|
4108
4224
|
const [row] = await tx
|