@kernhq/module-hr 0.1.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.
Files changed (75) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +71 -0
  3. package/dist/contract/capabilities.d.ts +45 -0
  4. package/dist/contract/capabilities.d.ts.map +1 -0
  5. package/dist/contract/capabilities.js +114 -0
  6. package/dist/contract/capabilities.js.map +1 -0
  7. package/dist/contract/events.d.ts +74 -0
  8. package/dist/contract/events.d.ts.map +1 -0
  9. package/dist/contract/events.js +63 -0
  10. package/dist/contract/events.js.map +1 -0
  11. package/dist/contract/index.d.ts +15 -0
  12. package/dist/contract/index.d.ts.map +1 -0
  13. package/dist/contract/index.js +15 -0
  14. package/dist/contract/index.js.map +1 -0
  15. package/dist/contract/models.d.ts +457 -0
  16. package/dist/contract/models.d.ts.map +1 -0
  17. package/dist/contract/models.js +381 -0
  18. package/dist/contract/models.js.map +1 -0
  19. package/dist/contract/permissions.d.ts +174 -0
  20. package/dist/contract/permissions.d.ts.map +1 -0
  21. package/dist/contract/permissions.js +209 -0
  22. package/dist/contract/permissions.js.map +1 -0
  23. package/dist/contract/router.d.ts +2720 -0
  24. package/dist/contract/router.d.ts.map +1 -0
  25. package/dist/contract/router.js +520 -0
  26. package/dist/contract/router.js.map +1 -0
  27. package/dist/contract/settings.d.ts +20 -0
  28. package/dist/contract/settings.d.ts.map +1 -0
  29. package/dist/contract/settings.js +34 -0
  30. package/dist/contract/settings.js.map +1 -0
  31. package/dist/policy/calendar.d.ts +64 -0
  32. package/dist/policy/calendar.d.ts.map +1 -0
  33. package/dist/policy/calendar.js +113 -0
  34. package/dist/policy/calendar.js.map +1 -0
  35. package/dist/server/index.d.ts +8 -0
  36. package/dist/server/index.d.ts.map +1 -0
  37. package/dist/server/index.js +141 -0
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/packs/index.d.ts +50 -0
  40. package/dist/server/packs/index.d.ts.map +1 -0
  41. package/dist/server/packs/index.js +121 -0
  42. package/dist/server/packs/index.js.map +1 -0
  43. package/dist/server/router.d.ts +3095 -0
  44. package/dist/server/router.d.ts.map +1 -0
  45. package/dist/server/router.js +1537 -0
  46. package/dist/server/router.js.map +1 -0
  47. package/dist/server/schema.d.ts +2828 -0
  48. package/dist/server/schema.d.ts.map +1 -0
  49. package/dist/server/schema.js +313 -0
  50. package/dist/server/schema.js.map +1 -0
  51. package/dist/server/services/db.d.ts +22 -0
  52. package/dist/server/services/db.d.ts.map +1 -0
  53. package/dist/server/services/db.js +23 -0
  54. package/dist/server/services/db.js.map +1 -0
  55. package/dist/server/services/people.d.ts +110 -0
  56. package/dist/server/services/people.d.ts.map +1 -0
  57. package/dist/server/services/people.js +182 -0
  58. package/dist/server/services/people.js.map +1 -0
  59. package/dist/server/services/resolve.d.ts +66 -0
  60. package/dist/server/services/resolve.d.ts.map +1 -0
  61. package/dist/server/services/resolve.js +145 -0
  62. package/dist/server/services/resolve.js.map +1 -0
  63. package/migrations/0000_init.sql +223 -0
  64. package/migrations/0001_rls.sql +141 -0
  65. package/migrations/meta/0000_snapshot.json +1715 -0
  66. package/migrations/meta/_journal.json +20 -0
  67. package/package.json +59 -0
  68. package/src/client/index.ts +62 -0
  69. package/src/contract/capabilities.ts +117 -0
  70. package/src/contract/events.ts +84 -0
  71. package/src/contract/index.ts +14 -0
  72. package/src/contract/models.ts +437 -0
  73. package/src/contract/permissions.ts +217 -0
  74. package/src/contract/router.ts +611 -0
  75. package/src/contract/settings.ts +35 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,313 @@
1
+ import { moduleSchema } from '@kernhq/kernel';
2
+ import { sql } from 'drizzle-orm';
3
+ import { boolean, customType, date, index, integer, jsonb, numeric, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
4
+ /**
5
+ * HR's tables, in `mod_hr`.
6
+ *
7
+ * Two rules apply to every tenant table here, neither optional: `workspace_id` with an index that
8
+ * starts with it, and a row-level security policy hand-written in the migration. Three more apply to
9
+ * this module in particular, and they are what most of the design is about:
10
+ *
11
+ * - **Effective-dated tables are never updated in place.** `employments` and `office_assignments`
12
+ * record what was true over a period. A change closes the open row and inserts a new one, so
13
+ * "who did she report to in March" stays answerable — which a leave approval from March needs.
14
+ * - **History is append-only.** `person_history` records what changed, when and by whom, and nothing
15
+ * rewrites it. It is what a KVKK or GDPR subject-access request is built from.
16
+ * - **Constraints, not application code, enforce the invariants that matter.** One primary office
17
+ * per person per day, and no two overlapping employment rows, are guaranteed by exclusion
18
+ * constraints in migration 0001. Two concurrent requests cannot both win.
19
+ */
20
+ export const schema = moduleSchema('hr');
21
+ const ltree = customType({ dataType: () => 'ltree' });
22
+ const id = () => uuid('id').primaryKey().default(sql `uuidv7()`);
23
+ const ws = () => uuid('workspace_id').notNull();
24
+ const ts = (name) => timestamp(name, { withTimezone: true, mode: 'date' });
25
+ const created = () => ts('created_at').notNull().defaultNow();
26
+ const updated = () => ts('updated_at').notNull().defaultNow();
27
+ // =====================================================================================
28
+ // offices, entities — the unit of inheritance
29
+ // =====================================================================================
30
+ export const legalEntities = schema.table('legal_entities', {
31
+ id: id(),
32
+ workspaceId: ws(),
33
+ name: text('name').notNull(),
34
+ registrationNo: text('registration_no'),
35
+ taxNo: text('tax_no'),
36
+ country: text('country').notNull(),
37
+ currency: text('currency'),
38
+ archivedAt: ts('archived_at'),
39
+ createdAt: created(),
40
+ updatedAt: updated(),
41
+ }, (t) => [index('hr_entities_ws_idx').on(t.workspaceId, t.archivedAt)]);
42
+ export const offices = schema.table('offices', {
43
+ id: id(),
44
+ workspaceId: ws(),
45
+ name: text('name').notNull(),
46
+ code: text('code'),
47
+ kind: text('kind').notNull().default('branch'),
48
+ /** A campus with buildings. Geography, not the org chart. */
49
+ parentOfficeId: uuid('parent_office_id'),
50
+ legalEntityId: uuid('legal_entity_id'),
51
+ country: text('country').notNull(),
52
+ region: text('region'),
53
+ city: text('city'),
54
+ /** IANA, never an offset — an offset cannot survive a daylight-saving transition. */
55
+ timezone: text('timezone').notNull().default('UTC'),
56
+ calendarId: uuid('calendar_id'),
57
+ address: jsonb('address').$type(),
58
+ /**
59
+ * Exactly one per workspace, enforced by a partial unique index in 0001.
60
+ *
61
+ * Always present, even when the `offices` capability is off: HR creates it from the workspace
62
+ * country and nobody sees the word. That is what makes enabling the capability a reveal rather
63
+ * than a migration, and why nothing in this module has a "no office" branch.
64
+ */
65
+ isDefault: boolean('is_default').notNull().default(false),
66
+ headPersonId: uuid('head_person_id'),
67
+ archivedAt: ts('archived_at'),
68
+ createdAt: created(),
69
+ updatedAt: updated(),
70
+ }, (t) => [
71
+ index('hr_offices_ws_idx').on(t.workspaceId, t.archivedAt),
72
+ index('hr_offices_ws_country_idx').on(t.workspaceId, t.country),
73
+ ]);
74
+ export const costCenters = schema.table('cost_centers', {
75
+ id: id(),
76
+ workspaceId: ws(),
77
+ code: text('code').notNull(),
78
+ name: text('name').notNull(),
79
+ officeId: uuid('office_id'),
80
+ orgUnitId: uuid('org_unit_id'),
81
+ legalEntityId: uuid('legal_entity_id'),
82
+ archivedAt: ts('archived_at'),
83
+ createdAt: created(),
84
+ }, (t) => [uniqueIndex('hr_cost_centers_ws_code_uq').on(t.workspaceId, t.code)]);
85
+ // =====================================================================================
86
+ // org structure
87
+ // =====================================================================================
88
+ export const orgUnits = schema.table('org_units', {
89
+ id: id(),
90
+ workspaceId: ws(),
91
+ parentId: uuid('parent_id'),
92
+ /**
93
+ * Materialised ltree path. A subtree is `path <@ 'root.eng'`, which is one GiST index lookup —
94
+ * and the same query answers both "who is in this department" and the office/team permission
95
+ * scope, so it is on a hot path twice.
96
+ */
97
+ path: ltree('path').notNull(),
98
+ name: text('name').notNull(),
99
+ code: text('code'),
100
+ headPersonId: uuid('head_person_id'),
101
+ archivedAt: ts('archived_at'),
102
+ createdAt: created(),
103
+ updatedAt: updated(),
104
+ }, (t) => [index('hr_org_units_ws_idx').on(t.workspaceId, t.archivedAt)]);
105
+ export const positions = schema.table('positions', {
106
+ id: id(),
107
+ workspaceId: ws(),
108
+ title: text('title').notNull(),
109
+ code: text('code'),
110
+ jobFamily: text('job_family'),
111
+ level: text('level'),
112
+ archivedAt: ts('archived_at'),
113
+ createdAt: created(),
114
+ }, (t) => [index('hr_positions_ws_idx').on(t.workspaceId, t.archivedAt)]);
115
+ // =====================================================================================
116
+ // people
117
+ // =====================================================================================
118
+ export const people = schema.table('people', {
119
+ id: id(),
120
+ workspaceId: ws(),
121
+ /** Nullable: plenty of employees never sign in, and HR is populated before anyone is invited. */
122
+ userId: uuid('user_id'),
123
+ employeeNo: text('employee_no'),
124
+ displayName: text('display_name').notNull(),
125
+ workEmail: text('work_email'),
126
+ personalEmail: text('personal_email'),
127
+ phone: text('phone'),
128
+ photoFileId: uuid('photo_file_id'),
129
+ status: text('status').notNull().default('active'),
130
+ hiredOn: date('hired_on'),
131
+ terminatedOn: date('terminated_on'),
132
+ /** Overrides the primary office's zone for somebody who genuinely works elsewhere. */
133
+ timezone: text('timezone'),
134
+ custom: jsonb('custom').$type().notNull().default(sql `'{}'::jsonb`),
135
+ createdAt: created(),
136
+ updatedAt: updated(),
137
+ }, (t) => [
138
+ index('hr_people_ws_status_idx').on(t.workspaceId, t.status, t.displayName),
139
+ uniqueIndex('hr_people_ws_user_uq').on(t.workspaceId, t.userId),
140
+ uniqueIndex('hr_people_ws_empno_uq').on(t.workspaceId, t.employeeNo),
141
+ ]);
142
+ /**
143
+ * The fields that need a second permission, in their own table.
144
+ *
145
+ * Not optional columns on `people`: an optional column gets returned by a `select *` that somebody
146
+ * wrote in a hurry, and this is the data a KVKK or GDPR breach is measured in. A separate table
147
+ * means reading it is a deliberate join, and `hr.person.view_sensitive` guards the one procedure
148
+ * that performs it.
149
+ */
150
+ export const peopleSensitive = schema.table('people_sensitive', {
151
+ personId: uuid('person_id').primaryKey(),
152
+ workspaceId: ws(),
153
+ /** Encrypted through `kernel.secrets`; the column holds ciphertext, never the number. */
154
+ nationalIdEnc: text('national_id_enc'),
155
+ birthDate: date('birth_date'),
156
+ ibanEnc: text('iban_enc'),
157
+ emergencyContact: jsonb('emergency_contact').$type(),
158
+ updatedAt: updated(),
159
+ }, (t) => [index('hr_people_sensitive_ws_idx').on(t.workspaceId)]);
160
+ /**
161
+ * Effective-dated employment. One row per period the job was a given shape.
162
+ *
163
+ * `effective_to IS NULL` is the present. Migration 0001 adds an exclusion constraint so two rows for
164
+ * one person can never overlap — which is what stops "what was her FTE in March" having two answers
165
+ * after a backdated correction races a forward-dated one.
166
+ */
167
+ export const employments = schema.table('employments', {
168
+ id: id(),
169
+ workspaceId: ws(),
170
+ personId: uuid('person_id').notNull(),
171
+ effectiveFrom: date('effective_from').notNull(),
172
+ effectiveTo: date('effective_to'),
173
+ orgUnitId: uuid('org_unit_id'),
174
+ positionId: uuid('position_id'),
175
+ legalEntityId: uuid('legal_entity_id'),
176
+ costCenterId: uuid('cost_center_id'),
177
+ managerPersonId: uuid('manager_person_id'),
178
+ employmentType: text('employment_type').notNull().default('full_time'),
179
+ fte: numeric('fte', { precision: 4, scale: 3 }).notNull().default('1.000'),
180
+ contractHoursWeek: numeric('contract_hours_week', { precision: 5, scale: 2 }),
181
+ reason: text('reason'),
182
+ createdAt: created(),
183
+ }, (t) => [
184
+ index('hr_employments_person_idx').on(t.workspaceId, t.personId, t.effectiveFrom),
185
+ index('hr_employments_ws_manager_idx').on(t.workspaceId, t.managerPersonId),
186
+ index('hr_employments_ws_unit_idx').on(t.workspaceId, t.orgUnitId),
187
+ ]);
188
+ /**
189
+ * Who works where, over time. Several concurrent rows allowed; exactly one primary.
190
+ *
191
+ * The primary decides holidays, timezone and policy. The others grant presence — appearing in that
192
+ * office's directory, being visible to its local HR — and decide nothing. Migration 0001 enforces
193
+ * both halves: no duplicate assignment over a period, and no two primaries on one day.
194
+ */
195
+ export const officeAssignments = schema.table('office_assignments', {
196
+ id: id(),
197
+ workspaceId: ws(),
198
+ personId: uuid('person_id').notNull(),
199
+ officeId: uuid('office_id').notNull(),
200
+ isPrimary: boolean('is_primary').notNull().default(true),
201
+ effectiveFrom: date('effective_from').notNull(),
202
+ effectiveTo: date('effective_to'),
203
+ reason: text('reason'),
204
+ createdAt: created(),
205
+ }, (t) => [
206
+ index('hr_office_assign_person_idx').on(t.workspaceId, t.personId, t.effectiveFrom),
207
+ index('hr_office_assign_office_idx').on(t.workspaceId, t.officeId, t.effectiveFrom),
208
+ ]);
209
+ /** Append-only. What changed, when, by whom — and what a subject-access request is built from. */
210
+ export const personHistory = schema.table('person_history', {
211
+ id: id(),
212
+ workspaceId: ws(),
213
+ personId: uuid('person_id').notNull(),
214
+ field: text('field').notNull(),
215
+ from: jsonb('from_value'),
216
+ to: jsonb('to_value'),
217
+ at: created(),
218
+ actorId: uuid('actor_id'),
219
+ source: text('source').notNull().default('app'),
220
+ }, (t) => [index('hr_person_history_idx').on(t.workspaceId, t.personId, t.at)]);
221
+ export const personDocuments = schema.table('person_documents', {
222
+ id: id(),
223
+ workspaceId: ws(),
224
+ personId: uuid('person_id').notNull(),
225
+ fileId: uuid('file_id').notNull(),
226
+ name: text('name').notNull(),
227
+ kind: text('kind').notNull().default('other'),
228
+ issuedOn: date('issued_on'),
229
+ expiresOn: date('expires_on'),
230
+ uploadedBy: uuid('uploaded_by'),
231
+ createdAt: created(),
232
+ }, (t) => [
233
+ index('hr_person_docs_idx').on(t.workspaceId, t.personId, t.createdAt),
234
+ index('hr_person_docs_expiry_idx').on(t.workspaceId, t.expiresOn),
235
+ ]);
236
+ export const customFieldDefs = schema.table('custom_field_defs', {
237
+ id: id(),
238
+ workspaceId: ws(),
239
+ key: text('key').notNull(),
240
+ name: text('name').notNull(),
241
+ type: text('type').notNull(),
242
+ options: jsonb('options').$type(),
243
+ required: boolean('required').notNull().default(false),
244
+ sensitive: boolean('sensitive').notNull().default(false),
245
+ section: text('section').notNull().default('profile'),
246
+ order: integer('order').notNull().default(0),
247
+ archivedAt: ts('archived_at'),
248
+ createdAt: created(),
249
+ }, (t) => [
250
+ // The key *is* the `people.custom` key, so two definitions sharing one would share a value.
251
+ uniqueIndex('hr_fields_ws_key_uq').on(t.workspaceId, t.key),
252
+ ]);
253
+ // =====================================================================================
254
+ // calendars
255
+ // =====================================================================================
256
+ export const calendars = schema.table('calendars', {
257
+ id: id(),
258
+ workspaceId: ws(),
259
+ name: text('name').notNull(),
260
+ /** Composition, not copying: this calendar's days sit on top of the one it extends. */
261
+ extendsId: uuid('extends_id'),
262
+ country: text('country'),
263
+ region: text('region'),
264
+ workingWeek: jsonb('working_week')
265
+ .$type()
266
+ .notNull()
267
+ .default(sql `'{"mon":1,"tue":1,"wed":1,"thu":1,"fri":1,"sat":0,"sun":0}'::jsonb`),
268
+ source: text('source').notNull().default('custom'),
269
+ packKey: text('pack_key'),
270
+ packVersion: text('pack_version'),
271
+ archivedAt: ts('archived_at'),
272
+ createdAt: created(),
273
+ updatedAt: updated(),
274
+ }, (t) => [index('hr_calendars_ws_idx').on(t.workspaceId, t.archivedAt)]);
275
+ export const calendarDays = schema.table('calendar_days', {
276
+ id: id(),
277
+ workspaceId: ws(),
278
+ calendarId: uuid('calendar_id').notNull(),
279
+ date: date('date').notNull(),
280
+ kind: text('kind').notNull().default('public_holiday'),
281
+ name: text('name').notNull(),
282
+ /** 0 is off, 0.5 a half day, 1 a day worked despite the pack saying otherwise. */
283
+ workingFraction: numeric('working_fraction', { precision: 3, scale: 2 }).notNull().default('0'),
284
+ /**
285
+ * Per **day**, not per calendar. This one column is what lets HR add their own holidays to a
286
+ * country pack safely: an upgrade rewrites `pack` rows and never touches `custom` ones.
287
+ */
288
+ source: text('source').notNull().default('custom'),
289
+ paid: boolean('paid').notNull().default(true),
290
+ note: text('note'),
291
+ createdAt: created(),
292
+ }, (t) => [
293
+ index('hr_calendar_days_idx').on(t.workspaceId, t.calendarId, t.date),
294
+ uniqueIndex('hr_calendar_days_uq').on(t.calendarId, t.date, t.kind),
295
+ ]);
296
+ /** Every tenant table, so the RLS migration is checked against one list rather than memory. */
297
+ export const TENANT_TABLES = [
298
+ 'legal_entities',
299
+ 'offices',
300
+ 'cost_centers',
301
+ 'org_units',
302
+ 'positions',
303
+ 'people',
304
+ 'people_sensitive',
305
+ 'employments',
306
+ 'office_assignments',
307
+ 'person_history',
308
+ 'person_documents',
309
+ 'custom_field_defs',
310
+ 'calendars',
311
+ 'calendar_days',
312
+ ];
313
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { Tx } from '@kernhq/kernel';
2
+ import type { PgColumn } from 'drizzle-orm/pg-core';
3
+ /** Today in UTC as `YYYY-MM-DD`. Callers wanting a person's local today pass their zone instead. */
4
+ export declare const todayIso: () => string;
5
+ /** `YYYY-MM-DD` in a given IANA zone — what "today" means for somebody in Istanbul at 01:00 UTC. */
6
+ export declare function todayIn(timezone: string, at?: Date): string;
7
+ /**
8
+ * The predicate for "this effective-dated row was in force on `on`".
9
+ *
10
+ * `effective_from <= on AND (effective_to IS NULL OR effective_to >= on)`. Written once because it
11
+ * appears in every read of `employments` and `office_assignments`, and getting the boundary wrong by
12
+ * a day is the kind of bug that only shows up on somebody's last day of employment.
13
+ */
14
+ export declare const inForceOn: (from: PgColumn, to: PgColumn, on: string) => import("drizzle-orm").SQL<unknown> | undefined;
15
+ /** The currently open row: no end date at all. */
16
+ export declare const isOpen: (to: PgColumn) => import("drizzle-orm").SQL<unknown>;
17
+ /** Narrow anything to one workspace. Every query in this module starts here. */
18
+ export declare const inWorkspace: (col: PgColumn, workspaceId: string) => import("drizzle-orm").SQL<unknown>;
19
+ /** Postgres `numeric` comes back as a string; every ratio in this module is a real number. */
20
+ export declare const num: (v: string | number | null | undefined, fallback?: number) => number;
21
+ export type { Tx };
22
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../../src/server/services/db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAA;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAEnD,oGAAoG;AACpG,eAAO,MAAM,QAAQ,cAA8C,CAAA;AAEnE,oGAAoG;AACpG,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,GAAE,IAAiB,GAAG,MAAM,CAGvE;AAED;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,QAAQ,EAAE,IAAI,QAAQ,EAAE,IAAI,MAAM,mDACT,CAAA;AAEzD,kDAAkD;AAClD,eAAO,MAAM,MAAM,GAAI,IAAI,QAAQ,uCAAe,CAAA;AAElD,gFAAgF;AAChF,eAAO,MAAM,WAAW,GAAI,KAAK,QAAQ,EAAE,aAAa,MAAM,uCAAyB,CAAA;AAEvF,8FAA8F;AAC9F,eAAO,MAAM,GAAG,GAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,iBAAY,KAAG,MACmB,CAAA;AAE7F,YAAY,EAAE,EAAE,EAAE,CAAA"}
@@ -0,0 +1,23 @@
1
+ import { and, eq, isNull, lte, or, sql } from 'drizzle-orm';
2
+ /** Today in UTC as `YYYY-MM-DD`. Callers wanting a person's local today pass their zone instead. */
3
+ export const todayIso = () => new Date().toISOString().slice(0, 10);
4
+ /** `YYYY-MM-DD` in a given IANA zone — what "today" means for somebody in Istanbul at 01:00 UTC. */
5
+ export function todayIn(timezone, at = new Date()) {
6
+ // `en-CA` formats as YYYY-MM-DD, which is the one locale that gives an ISO date for free.
7
+ return new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format(at);
8
+ }
9
+ /**
10
+ * The predicate for "this effective-dated row was in force on `on`".
11
+ *
12
+ * `effective_from <= on AND (effective_to IS NULL OR effective_to >= on)`. Written once because it
13
+ * appears in every read of `employments` and `office_assignments`, and getting the boundary wrong by
14
+ * a day is the kind of bug that only shows up on somebody's last day of employment.
15
+ */
16
+ export const inForceOn = (from, to, on) => and(lte(from, on), or(isNull(to), sql `${to} >= ${on}`));
17
+ /** The currently open row: no end date at all. */
18
+ export const isOpen = (to) => isNull(to);
19
+ /** Narrow anything to one workspace. Every query in this module starts here. */
20
+ export const inWorkspace = (col, workspaceId) => eq(col, workspaceId);
21
+ /** Postgres `numeric` comes back as a string; every ratio in this module is a real number. */
22
+ export const num = (v, fallback = 0) => v === null || v === undefined ? fallback : typeof v === 'number' ? v : Number.parseFloat(v);
23
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../../src/server/services/db.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AAG3D,oGAAoG;AACpG,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AAEnE,oGAAoG;AACpG,MAAM,UAAU,OAAO,CAAC,QAAgB,EAAE,KAAW,IAAI,IAAI,EAAE;IAC7D,0FAA0F;IAC1F,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AAC5E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAc,EAAE,EAAY,EAAE,EAAU,EAAE,EAAE,CACpE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,GAAG,CAAA,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AAEzD,kDAAkD;AAClD,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EAAY,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AAElD,gFAAgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,GAAa,EAAE,WAAmB,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,CAAC,CAAA;AAEvF,8FAA8F;AAC9F,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,CAAqC,EAAE,QAAQ,GAAG,CAAC,EAAU,EAAE,CACjF,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA"}
@@ -0,0 +1,110 @@
1
+ import { type Kernel, type Tx } from '@kernhq/kernel';
2
+ import type { EmploymentType, HrSettings, PersonStatus } from '../../contract/index.js';
3
+ import { employments, officeAssignments, people } from '../schema.js';
4
+ /** Row shapes the router turns into contract objects. */
5
+ export type PersonRow = typeof people.$inferSelect;
6
+ export type EmploymentRow = typeof employments.$inferSelect;
7
+ /**
8
+ * Writing people, employment and office moves — the three things that must stay consistent.
9
+ *
10
+ * Everything effective-dated goes through here rather than through the router, because "close the
11
+ * open row, then insert the new one" has to happen in one transaction and in that order. Done the
12
+ * other way round, a unique or exclusion constraint fires on the overlap and the change looks like
13
+ * a validation error instead of a race.
14
+ */
15
+ export declare class PeopleService {
16
+ private readonly kernel;
17
+ constructor(kernel: Kernel);
18
+ /** Serialise a row for the wire. Postgres `numeric` arrives as a string; the contract says number. */
19
+ static toPerson(row: PersonRow): {
20
+ status: PersonStatus;
21
+ custom: Record<string, unknown>;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ id: string;
25
+ workspaceId: string;
26
+ userId: string | null;
27
+ employeeNo: string | null;
28
+ displayName: string;
29
+ workEmail: string | null;
30
+ personalEmail: string | null;
31
+ phone: string | null;
32
+ photoFileId: string | null;
33
+ hiredOn: string | null;
34
+ terminatedOn: string | null;
35
+ timezone: string | null;
36
+ };
37
+ static toEmployment(row: EmploymentRow): {
38
+ employmentType: EmploymentType;
39
+ fte: number;
40
+ contractHoursWeek: number | null;
41
+ createdAt: string;
42
+ id: string;
43
+ workspaceId: string;
44
+ personId: string;
45
+ effectiveFrom: string;
46
+ effectiveTo: string | null;
47
+ orgUnitId: string | null;
48
+ positionId: string | null;
49
+ legalEntityId: string | null;
50
+ costCenterId: string | null;
51
+ managerPersonId: string | null;
52
+ reason: string | null;
53
+ };
54
+ load(tx: Tx, workspaceId: string, personId: string): Promise<PersonRow>;
55
+ byUserId(tx: Tx, workspaceId: string, userId: string): Promise<PersonRow | undefined>;
56
+ /**
57
+ * The next employee number, from workspace settings.
58
+ *
59
+ * Read-modify-write rather than a sequence, because the prefix and the counter are things an
60
+ * administrator edits — and a Postgres sequence cannot be edited without surprising them. Two
61
+ * people created in the same instant can therefore collide; the unique index catches it and the
62
+ * caller retries, which is the right trade for something that happens a handful of times a day.
63
+ */
64
+ nextEmployeeNo(workspaceId: string, settings: HrSettings): Promise<string | null>;
65
+ /** Append-only. Every field change lands here, and nothing rewrites it. */
66
+ record(tx: Tx, workspaceId: string, personId: string, actorId: string | null, changes: Array<{
67
+ field: string;
68
+ from: unknown;
69
+ to: unknown;
70
+ }>, source?: string): Promise<void>;
71
+ /**
72
+ * Close the open employment row and open a new one from `effectiveFrom`.
73
+ *
74
+ * The new row inherits everything the caller did not name, so "change her manager" does not
75
+ * silently blank her department. A change dated before the current row's start is refused rather
76
+ than than quietly reordering history — that is a correction, and corrections need their own path.
77
+ */
78
+ changeEmployment(tx: Tx, workspaceId: string, personId: string, effectiveFrom: string, patch: Partial<Omit<EmploymentRow, 'id' | 'workspaceId' | 'personId' | 'createdAt'>>): Promise<EmploymentRow>;
79
+ /**
80
+ * Assign somebody to an office from a date, optionally as their primary.
81
+ *
82
+ * Making one primary demotes the other primary rather than leaving two — the database refuses two
83
+ * anyway, and finding that out as a constraint violation would turn an ordinary office move into
84
+ * an error the person doing it cannot act on.
85
+ */
86
+ assignOffice(tx: Tx, workspaceId: string, personId: string, officeId: string, isPrimary: boolean, effectiveFrom: string, reason: string | null): Promise<{
87
+ id: string;
88
+ workspaceId: string;
89
+ personId: string;
90
+ officeId: string;
91
+ isPrimary: boolean;
92
+ effectiveFrom: string;
93
+ effectiveTo: string | null;
94
+ reason: string | null;
95
+ createdAt: Date;
96
+ }[]>;
97
+ static toAssignment(row: typeof officeAssignments.$inferSelect): {
98
+ createdAt: string;
99
+ id: string;
100
+ workspaceId: string;
101
+ personId: string;
102
+ effectiveFrom: string;
103
+ officeId: string;
104
+ isPrimary: boolean;
105
+ effectiveTo: string | null;
106
+ reason: string | null;
107
+ };
108
+ today: () => string;
109
+ }
110
+ //# sourceMappingURL=people.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"people.d.ts","sourceRoot":"","sources":["../../../src/server/services/people.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,MAAM,EAAE,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAExE,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,EAAiB,MAAM,cAAc,CAAA;AAGpF,yDAAyD;AACzD,MAAM,MAAM,SAAS,GAAG,OAAO,MAAM,CAAC,YAAY,CAAA;AAClD,MAAM,MAAM,aAAa,GAAG,OAAO,WAAW,CAAC,YAAY,CAAA;AAE3D;;;;;;;GAOG;AACH,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;IAE3C,sGAAsG;IACtG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,SAAS;gBAMJ,YAAY;;;;;;;;;;;;;;;;;IAOtC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,aAAa;wBAGI,cAAc;;;;;;;;;;;;;;;;IAOlD,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAUvE,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAS3F;;;;;;;OAOG;IACG,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUvF,2EAA2E;IACrE,MAAM,CACV,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,EAC7D,MAAM,SAAQ;IAiBhB;;;;;;OAMG;IACG,gBAAgB,CACpB,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,aAAa,GAAG,UAAU,GAAG,WAAW,CAAC,CAAC,GACnF,OAAO,CAAC,aAAa,CAAC;IAkDzB;;;;;;OAMG;IACG,YAAY,CAChB,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,OAAO,EAClB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,GAAG,IAAI;;;;;;;;;;;IAkDvB,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,iBAAiB,CAAC,YAAY;;;;;;;;;;;IAI9D,KAAK,eAAW;CACjB"}