@kernhq/module-hr 0.2.0 → 0.3.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/approvals.d.ts +281 -0
- package/dist/contract/approvals.d.ts.map +1 -0
- package/dist/contract/approvals.js +127 -0
- package/dist/contract/approvals.js.map +1 -0
- package/dist/contract/capabilities.d.ts +1 -1
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +44 -0
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/events.d.ts +42 -0
- package/dist/contract/events.d.ts.map +1 -1
- package/dist/contract/events.js +42 -0
- package/dist/contract/events.js.map +1 -1
- package/dist/contract/index.d.ts +2 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +2 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/contract/leave.d.ts +168 -0
- package/dist/contract/leave.d.ts.map +1 -0
- package/dist/contract/leave.js +150 -0
- package/dist/contract/leave.js.map +1 -0
- package/dist/contract/permissions.d.ts +57 -0
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +69 -0
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/router.d.ts +1446 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +235 -0
- package/dist/contract/router.js.map +1 -1
- package/dist/server/router.d.ts +1646 -58
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +812 -2
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +2084 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +209 -0
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/approvals.d.ts +163 -0
- package/dist/server/services/approvals.d.ts.map +1 -0
- package/dist/server/services/approvals.js +325 -0
- package/dist/server/services/approvals.js.map +1 -0
- package/dist/server/services/ledger.d.ts +135 -0
- package/dist/server/services/ledger.d.ts.map +1 -0
- package/dist/server/services/ledger.js +178 -0
- package/dist/server/services/ledger.js.map +1 -0
- package/dist/server/services/people.d.ts +3 -3
- package/migrations/0002_leave.sql +237 -0
- package/migrations/meta/0002_snapshot.json +3009 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/contract/approvals.ts +149 -0
- package/src/contract/capabilities.ts +44 -0
- package/src/contract/events.ts +57 -0
- package/src/contract/index.ts +2 -0
- package/src/contract/leave.ts +171 -0
- package/src/contract/permissions.ts +71 -0
- package/src/contract/router.ts +285 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { KernError, uuidv7 } from '@kernhq/kernel';
|
|
2
|
+
import { and, eq, inArray, sql } from 'drizzle-orm';
|
|
3
|
+
import { leaveBalanceCursor, leaveLedger, leaveRequestDays, leaveTypes } from '../schema.js';
|
|
4
|
+
/** A minute count in whatever unit the type displays. Hours to one place, days to two. */
|
|
5
|
+
export const toUnit = (minutes, unit) => unit === 'hour' ? Math.round((minutes / 60) * 10) / 10 : Math.round((minutes / (60 * 8)) * 100) / 100;
|
|
6
|
+
/** Minutes in one working day. A constant here; a policy in Phase 4, per contract. */
|
|
7
|
+
export const MINUTES_PER_DAY = 8 * 60;
|
|
8
|
+
export class LedgerService {
|
|
9
|
+
/**
|
|
10
|
+
* Balances for one person, per leave type.
|
|
11
|
+
*
|
|
12
|
+
* Summed from the ledger rather than read from the cursor: the cursor exists to be locked, and a
|
|
13
|
+
* cache that is also the source of truth is a cache that eventually disagrees with it. The sum is
|
|
14
|
+
* one indexed aggregate over a person's own rows, which is small.
|
|
15
|
+
*/
|
|
16
|
+
async balances(tx, workspaceId, personId, periodYear) {
|
|
17
|
+
const types = await tx
|
|
18
|
+
.select()
|
|
19
|
+
.from(leaveTypes)
|
|
20
|
+
.where(and(eq(leaveTypes.workspaceId, workspaceId), sql `${leaveTypes.archivedAt} is null`));
|
|
21
|
+
const sums = await tx
|
|
22
|
+
.select({
|
|
23
|
+
leaveTypeId: leaveLedger.leaveTypeId,
|
|
24
|
+
total: sql `coalesce(sum(${leaveLedger.amountMinutes}), 0)::int`,
|
|
25
|
+
})
|
|
26
|
+
.from(leaveLedger)
|
|
27
|
+
.where(and(eq(leaveLedger.workspaceId, workspaceId), eq(leaveLedger.personId, personId), eq(leaveLedger.periodYear, periodYear)))
|
|
28
|
+
.groupBy(leaveLedger.leaveTypeId);
|
|
29
|
+
const byType = new Map(sums.map((r) => [r.leaveTypeId, Number(r.total)]));
|
|
30
|
+
// Pending requests are not spent, but they are not available either — showing a balance that
|
|
31
|
+
// ignores them is how somebody books the same day twice and only finds out at approval.
|
|
32
|
+
const pending = await tx
|
|
33
|
+
.select({
|
|
34
|
+
leaveTypeId: sql `lr.leave_type_id`,
|
|
35
|
+
status: sql `lr.status`,
|
|
36
|
+
minutes: sql `coalesce(sum(lr.minutes), 0)::int`,
|
|
37
|
+
})
|
|
38
|
+
.from(sql `${leaveRequestDays} d join mod_hr.leave_requests lr on lr.id = d.request_id`)
|
|
39
|
+
.where(sql `d.workspace_id = ${workspaceId} and d.person_id = ${personId}
|
|
40
|
+
and lr.status in ('pending','approved')`)
|
|
41
|
+
.groupBy(sql `lr.leave_type_id, lr.status`);
|
|
42
|
+
const pendingBy = new Map();
|
|
43
|
+
const bookedBy = new Map();
|
|
44
|
+
for (const row of pending) {
|
|
45
|
+
const target = row.status === 'approved' ? bookedBy : pendingBy;
|
|
46
|
+
target.set(row.leaveTypeId, (target.get(row.leaveTypeId) ?? 0) + Number(row.minutes));
|
|
47
|
+
}
|
|
48
|
+
return types.map((type) => {
|
|
49
|
+
const balanceMinutes = byType.get(type.id) ?? 0;
|
|
50
|
+
const pendingMinutes = pendingBy.get(type.id) ?? 0;
|
|
51
|
+
return {
|
|
52
|
+
personId,
|
|
53
|
+
leaveTypeId: type.id,
|
|
54
|
+
leaveTypeName: type.name,
|
|
55
|
+
unit: type.unit,
|
|
56
|
+
periodYear,
|
|
57
|
+
balanceMinutes,
|
|
58
|
+
bookedMinutes: bookedBy.get(type.id) ?? 0,
|
|
59
|
+
pendingMinutes,
|
|
60
|
+
availableMinutes: balanceMinutes - pendingMinutes,
|
|
61
|
+
balance: toUnit(balanceMinutes, type.unit),
|
|
62
|
+
available: toUnit(balanceMinutes - pendingMinutes, type.unit),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Take the lock for one person and leave type, creating the cursor row if it is missing.
|
|
68
|
+
*
|
|
69
|
+
* Everything that *spends* balance must call this first, inside the same transaction. It returns
|
|
70
|
+
* the balance as of the moment the lock was acquired, which is the only balance safe to decide on.
|
|
71
|
+
*/
|
|
72
|
+
async lockAndRead(tx, workspaceId, personId, leaveTypeId, periodYear) {
|
|
73
|
+
await tx
|
|
74
|
+
.insert(leaveBalanceCursor)
|
|
75
|
+
.values({ id: uuidv7(), workspaceId, personId, leaveTypeId, periodYear })
|
|
76
|
+
.onConflictDoNothing();
|
|
77
|
+
await tx.execute(sql `
|
|
78
|
+
select 1 from ${leaveBalanceCursor}
|
|
79
|
+
where workspace_id = ${workspaceId}
|
|
80
|
+
and person_id = ${personId}
|
|
81
|
+
and leave_type_id = ${leaveTypeId}
|
|
82
|
+
and period_year = ${periodYear}
|
|
83
|
+
for update
|
|
84
|
+
`);
|
|
85
|
+
const [row] = await tx
|
|
86
|
+
.select({ total: sql `coalesce(sum(${leaveLedger.amountMinutes}), 0)::int` })
|
|
87
|
+
.from(leaveLedger)
|
|
88
|
+
.where(and(eq(leaveLedger.workspaceId, workspaceId), eq(leaveLedger.personId, personId), eq(leaveLedger.leaveTypeId, leaveTypeId), eq(leaveLedger.periodYear, periodYear)));
|
|
89
|
+
return Number(row?.total ?? 0);
|
|
90
|
+
}
|
|
91
|
+
/** Append an entry. The only way anything enters the ledger. */
|
|
92
|
+
async append(tx, workspaceId, entry) {
|
|
93
|
+
const [row] = await tx
|
|
94
|
+
.insert(leaveLedger)
|
|
95
|
+
.values({ id: uuidv7(), workspaceId, ...entry })
|
|
96
|
+
.returning();
|
|
97
|
+
await tx
|
|
98
|
+
.update(leaveBalanceCursor)
|
|
99
|
+
.set({
|
|
100
|
+
cachedBalanceMinutes: sql `${leaveBalanceCursor.cachedBalanceMinutes} + ${entry.amountMinutes}`,
|
|
101
|
+
asOfEntryId: row.id,
|
|
102
|
+
version: sql `${leaveBalanceCursor.version} + 1`,
|
|
103
|
+
updatedAt: new Date(),
|
|
104
|
+
})
|
|
105
|
+
.where(and(eq(leaveBalanceCursor.workspaceId, workspaceId), eq(leaveBalanceCursor.personId, entry.personId), eq(leaveBalanceCursor.leaveTypeId, entry.leaveTypeId), eq(leaveBalanceCursor.periodYear, entry.periodYear)));
|
|
106
|
+
return row;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Undo an entry by writing its opposite.
|
|
110
|
+
*
|
|
111
|
+
* Never a delete and never an update. A cancelled request has to leave both the consumption and
|
|
112
|
+
* the reversal visible, because "she booked it and then cancelled" and "she never booked it" are
|
|
113
|
+
* different facts and only one of them is true.
|
|
114
|
+
*/
|
|
115
|
+
async reverse(tx, workspaceId, entryId, reason, actorId, on) {
|
|
116
|
+
const [original] = await tx
|
|
117
|
+
.select()
|
|
118
|
+
.from(leaveLedger)
|
|
119
|
+
.where(and(eq(leaveLedger.workspaceId, workspaceId), eq(leaveLedger.id, entryId)))
|
|
120
|
+
.limit(1);
|
|
121
|
+
if (!original)
|
|
122
|
+
throw KernError.notFound('Ledger entry');
|
|
123
|
+
const [already] = await tx
|
|
124
|
+
.select({ id: leaveLedger.id })
|
|
125
|
+
.from(leaveLedger)
|
|
126
|
+
.where(and(eq(leaveLedger.workspaceId, workspaceId), eq(leaveLedger.reversesEntryId, entryId)))
|
|
127
|
+
.limit(1);
|
|
128
|
+
// Reversing twice would credit the balance twice. The check is here rather than in a constraint
|
|
129
|
+
// because a partial unique index on a nullable column is easy to get subtly wrong, and this
|
|
130
|
+
// path is always inside a transaction that already holds the cursor lock.
|
|
131
|
+
if (already)
|
|
132
|
+
throw KernError.conflict('That entry has already been reversed');
|
|
133
|
+
return this.append(tx, workspaceId, {
|
|
134
|
+
personId: original.personId,
|
|
135
|
+
leaveTypeId: original.leaveTypeId,
|
|
136
|
+
kind: 'reversal',
|
|
137
|
+
amountMinutes: -original.amountMinutes,
|
|
138
|
+
effectiveOn: on,
|
|
139
|
+
periodYear: original.periodYear,
|
|
140
|
+
requestId: original.requestId,
|
|
141
|
+
reversesEntryId: original.id,
|
|
142
|
+
reason,
|
|
143
|
+
createdBy: actorId,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** Every entry a request produced, for cancelling it. */
|
|
147
|
+
async entriesFor(tx, workspaceId, requestId) {
|
|
148
|
+
return tx
|
|
149
|
+
.select()
|
|
150
|
+
.from(leaveLedger)
|
|
151
|
+
.where(and(eq(leaveLedger.workspaceId, workspaceId), eq(leaveLedger.requestId, requestId)));
|
|
152
|
+
}
|
|
153
|
+
/** Rebuild every cursor from the ledger. Cheap insurance after a bulk import or a bad migration. */
|
|
154
|
+
async rebuildCursors(tx, workspaceId, personIds) {
|
|
155
|
+
if (!personIds.length)
|
|
156
|
+
return;
|
|
157
|
+
await tx.execute(sql `
|
|
158
|
+
update ${leaveBalanceCursor} c
|
|
159
|
+
set cached_balance_minutes = coalesce(s.total, 0),
|
|
160
|
+
version = c.version + 1,
|
|
161
|
+
updated_at = now()
|
|
162
|
+
from (
|
|
163
|
+
select person_id, leave_type_id, period_year, sum(amount_minutes)::int as total
|
|
164
|
+
from ${leaveLedger}
|
|
165
|
+
where workspace_id = ${workspaceId}
|
|
166
|
+
and person_id in (${sql.join(personIds.map((p) => sql `${p}`), sql `, `)})
|
|
167
|
+
group by 1, 2, 3
|
|
168
|
+
) s
|
|
169
|
+
where c.workspace_id = ${workspaceId}
|
|
170
|
+
and c.person_id = s.person_id
|
|
171
|
+
and c.leave_type_id = s.leave_type_id
|
|
172
|
+
and c.period_year = s.period_year
|
|
173
|
+
`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export const yearOf = (isoDate) => Number(isoDate.slice(0, 4));
|
|
177
|
+
export { inArray };
|
|
178
|
+
//# sourceMappingURL=ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../../src/server/services/ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,7 +22,6 @@ export declare class PeopleService {
|
|
|
22
22
|
createdAt: string;
|
|
23
23
|
updatedAt: string;
|
|
24
24
|
id: string;
|
|
25
|
-
workspaceId: string;
|
|
26
25
|
userId: string | null;
|
|
27
26
|
employeeNo: string | null;
|
|
28
27
|
displayName: string;
|
|
@@ -33,6 +32,7 @@ export declare class PeopleService {
|
|
|
33
32
|
hiredOn: string | null;
|
|
34
33
|
terminatedOn: string | null;
|
|
35
34
|
timezone: string | null;
|
|
35
|
+
workspaceId: string;
|
|
36
36
|
};
|
|
37
37
|
static toEmployment(row: EmploymentRow): {
|
|
38
38
|
employmentType: EmploymentType;
|
|
@@ -100,10 +100,10 @@ export declare class PeopleService {
|
|
|
100
100
|
workspaceId: string;
|
|
101
101
|
personId: string;
|
|
102
102
|
effectiveFrom: string;
|
|
103
|
-
officeId: string;
|
|
104
|
-
isPrimary: boolean;
|
|
105
103
|
effectiveTo: string | null;
|
|
106
104
|
reason: string | null;
|
|
105
|
+
officeId: string;
|
|
106
|
+
isPrimary: boolean;
|
|
107
107
|
};
|
|
108
108
|
today: () => string;
|
|
109
109
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
CREATE TABLE "mod_hr"."approval_chains" (
|
|
2
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
3
|
+
"workspace_id" uuid NOT NULL,
|
|
4
|
+
"name" text NOT NULL,
|
|
5
|
+
"subject_type" text NOT NULL,
|
|
6
|
+
"spec" jsonb NOT NULL,
|
|
7
|
+
"is_default" boolean DEFAULT false NOT NULL,
|
|
8
|
+
"archived_at" timestamp with time zone,
|
|
9
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
10
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
11
|
+
);
|
|
12
|
+
--> statement-breakpoint
|
|
13
|
+
CREATE TABLE "mod_hr"."approval_decisions" (
|
|
14
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
15
|
+
"workspace_id" uuid NOT NULL,
|
|
16
|
+
"step_id" uuid NOT NULL,
|
|
17
|
+
"approver_id" uuid NOT NULL,
|
|
18
|
+
"on_behalf_of_id" uuid,
|
|
19
|
+
"decision" text NOT NULL,
|
|
20
|
+
"comment" text,
|
|
21
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
22
|
+
);
|
|
23
|
+
--> statement-breakpoint
|
|
24
|
+
CREATE TABLE "mod_hr"."approval_requests" (
|
|
25
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
26
|
+
"workspace_id" uuid NOT NULL,
|
|
27
|
+
"subject_type" text NOT NULL,
|
|
28
|
+
"subject_id" uuid NOT NULL,
|
|
29
|
+
"summary" text DEFAULT '' NOT NULL,
|
|
30
|
+
"chain" jsonb NOT NULL,
|
|
31
|
+
"status" text DEFAULT 'pending' NOT NULL,
|
|
32
|
+
"current_step" integer DEFAULT 0 NOT NULL,
|
|
33
|
+
"requested_by" uuid,
|
|
34
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
35
|
+
"decided_at" timestamp with time zone,
|
|
36
|
+
"version" integer DEFAULT 0 NOT NULL
|
|
37
|
+
);
|
|
38
|
+
--> statement-breakpoint
|
|
39
|
+
CREATE TABLE "mod_hr"."approval_steps" (
|
|
40
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
41
|
+
"workspace_id" uuid NOT NULL,
|
|
42
|
+
"request_id" uuid NOT NULL,
|
|
43
|
+
"step_index" integer NOT NULL,
|
|
44
|
+
"name" text DEFAULT '' NOT NULL,
|
|
45
|
+
"mode" text DEFAULT 'any' NOT NULL,
|
|
46
|
+
"min_approvals" integer DEFAULT 1 NOT NULL,
|
|
47
|
+
"approver_ids" uuid[] DEFAULT '{}'::uuid[] NOT NULL,
|
|
48
|
+
"status" text DEFAULT 'pending' NOT NULL,
|
|
49
|
+
"due_at" timestamp with time zone,
|
|
50
|
+
"escalated_at" timestamp with time zone
|
|
51
|
+
);
|
|
52
|
+
--> statement-breakpoint
|
|
53
|
+
CREATE TABLE "mod_hr"."delegations" (
|
|
54
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
55
|
+
"workspace_id" uuid NOT NULL,
|
|
56
|
+
"from_person_id" uuid NOT NULL,
|
|
57
|
+
"to_person_id" uuid NOT NULL,
|
|
58
|
+
"subject_type" text,
|
|
59
|
+
"starts_on" date NOT NULL,
|
|
60
|
+
"ends_on" date NOT NULL,
|
|
61
|
+
"reason" text,
|
|
62
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
63
|
+
);
|
|
64
|
+
--> statement-breakpoint
|
|
65
|
+
CREATE TABLE "mod_hr"."leave_balance_cursor" (
|
|
66
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
67
|
+
"workspace_id" uuid NOT NULL,
|
|
68
|
+
"person_id" uuid NOT NULL,
|
|
69
|
+
"leave_type_id" uuid NOT NULL,
|
|
70
|
+
"period_year" integer NOT NULL,
|
|
71
|
+
"cached_balance_minutes" integer DEFAULT 0 NOT NULL,
|
|
72
|
+
"as_of_entry_id" uuid,
|
|
73
|
+
"version" integer DEFAULT 0 NOT NULL,
|
|
74
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
75
|
+
);
|
|
76
|
+
--> statement-breakpoint
|
|
77
|
+
CREATE TABLE "mod_hr"."leave_ledger" (
|
|
78
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
79
|
+
"workspace_id" uuid NOT NULL,
|
|
80
|
+
"person_id" uuid NOT NULL,
|
|
81
|
+
"leave_type_id" uuid NOT NULL,
|
|
82
|
+
"kind" text NOT NULL,
|
|
83
|
+
"amount_minutes" integer NOT NULL,
|
|
84
|
+
"effective_on" date NOT NULL,
|
|
85
|
+
"period_year" integer NOT NULL,
|
|
86
|
+
"request_id" uuid,
|
|
87
|
+
"reverses_entry_id" uuid,
|
|
88
|
+
"policy_hash" text,
|
|
89
|
+
"reason" text,
|
|
90
|
+
"created_by" uuid,
|
|
91
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
92
|
+
);
|
|
93
|
+
--> statement-breakpoint
|
|
94
|
+
CREATE TABLE "mod_hr"."leave_request_days" (
|
|
95
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
96
|
+
"workspace_id" uuid NOT NULL,
|
|
97
|
+
"request_id" uuid NOT NULL,
|
|
98
|
+
"person_id" uuid NOT NULL,
|
|
99
|
+
"date" date NOT NULL,
|
|
100
|
+
"fraction" numeric(3, 2) DEFAULT '1' NOT NULL,
|
|
101
|
+
"counted" boolean DEFAULT true NOT NULL,
|
|
102
|
+
"status" text DEFAULT 'pending' NOT NULL
|
|
103
|
+
);
|
|
104
|
+
--> statement-breakpoint
|
|
105
|
+
CREATE TABLE "mod_hr"."leave_requests" (
|
|
106
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
107
|
+
"workspace_id" uuid NOT NULL,
|
|
108
|
+
"person_id" uuid NOT NULL,
|
|
109
|
+
"leave_type_id" uuid NOT NULL,
|
|
110
|
+
"starts_on" date NOT NULL,
|
|
111
|
+
"ends_on" date NOT NULL,
|
|
112
|
+
"start_part" text DEFAULT 'full' NOT NULL,
|
|
113
|
+
"end_part" text DEFAULT 'full' NOT NULL,
|
|
114
|
+
"hours" numeric(5, 2),
|
|
115
|
+
"working_days" numeric(6, 2) DEFAULT '0' NOT NULL,
|
|
116
|
+
"minutes" integer DEFAULT 0 NOT NULL,
|
|
117
|
+
"status" text DEFAULT 'pending' NOT NULL,
|
|
118
|
+
"reason" text,
|
|
119
|
+
"document_file_id" uuid,
|
|
120
|
+
"approval_request_id" uuid,
|
|
121
|
+
"idempotency_key" text,
|
|
122
|
+
"decided_at" timestamp with time zone,
|
|
123
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
124
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
125
|
+
);
|
|
126
|
+
--> statement-breakpoint
|
|
127
|
+
CREATE TABLE "mod_hr"."leave_types" (
|
|
128
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
129
|
+
"workspace_id" uuid NOT NULL,
|
|
130
|
+
"key" text NOT NULL,
|
|
131
|
+
"name" text NOT NULL,
|
|
132
|
+
"paid" boolean DEFAULT true NOT NULL,
|
|
133
|
+
"unit" text DEFAULT 'day' NOT NULL,
|
|
134
|
+
"color" text,
|
|
135
|
+
"icon" text,
|
|
136
|
+
"requires_document_after_days" integer,
|
|
137
|
+
"counts_working_days_only" boolean DEFAULT true NOT NULL,
|
|
138
|
+
"allow_negative" boolean DEFAULT false NOT NULL,
|
|
139
|
+
"max_negative_minutes" integer DEFAULT 0 NOT NULL,
|
|
140
|
+
"order" integer DEFAULT 0 NOT NULL,
|
|
141
|
+
"archived_at" timestamp with time zone,
|
|
142
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
143
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
144
|
+
);
|
|
145
|
+
--> statement-breakpoint
|
|
146
|
+
CREATE INDEX "hr_approval_chains_idx" ON "mod_hr"."approval_chains" USING btree ("workspace_id","subject_type","archived_at");--> statement-breakpoint
|
|
147
|
+
CREATE UNIQUE INDEX "hr_approval_decisions_uq" ON "mod_hr"."approval_decisions" USING btree ("step_id","approver_id");--> statement-breakpoint
|
|
148
|
+
CREATE INDEX "hr_approval_requests_subject_idx" ON "mod_hr"."approval_requests" USING btree ("workspace_id","subject_type","subject_id");--> statement-breakpoint
|
|
149
|
+
CREATE INDEX "hr_approval_requests_status_idx" ON "mod_hr"."approval_requests" USING btree ("workspace_id","status");--> statement-breakpoint
|
|
150
|
+
CREATE UNIQUE INDEX "hr_approval_steps_uq" ON "mod_hr"."approval_steps" USING btree ("request_id","step_index");--> statement-breakpoint
|
|
151
|
+
CREATE INDEX "hr_approval_steps_due_idx" ON "mod_hr"."approval_steps" USING btree ("workspace_id","status","due_at");--> statement-breakpoint
|
|
152
|
+
CREATE INDEX "hr_delegations_idx" ON "mod_hr"."delegations" USING btree ("workspace_id","to_person_id","starts_on");--> statement-breakpoint
|
|
153
|
+
CREATE UNIQUE INDEX "hr_balance_cursor_uq" ON "mod_hr"."leave_balance_cursor" USING btree ("workspace_id","person_id","leave_type_id","period_year");--> statement-breakpoint
|
|
154
|
+
CREATE INDEX "hr_ledger_person_idx" ON "mod_hr"."leave_ledger" USING btree ("workspace_id","person_id","leave_type_id","effective_on");--> statement-breakpoint
|
|
155
|
+
CREATE INDEX "hr_ledger_request_idx" ON "mod_hr"."leave_ledger" USING btree ("workspace_id","request_id");--> statement-breakpoint
|
|
156
|
+
CREATE INDEX "hr_ledger_year_idx" ON "mod_hr"."leave_ledger" USING btree ("workspace_id","period_year");--> statement-breakpoint
|
|
157
|
+
CREATE INDEX "hr_leave_days_person_idx" ON "mod_hr"."leave_request_days" USING btree ("workspace_id","person_id","date");--> statement-breakpoint
|
|
158
|
+
CREATE INDEX "hr_leave_days_request_idx" ON "mod_hr"."leave_request_days" USING btree ("request_id");--> statement-breakpoint
|
|
159
|
+
CREATE INDEX "hr_leave_requests_person_idx" ON "mod_hr"."leave_requests" USING btree ("workspace_id","person_id","starts_on");--> statement-breakpoint
|
|
160
|
+
CREATE INDEX "hr_leave_requests_status_idx" ON "mod_hr"."leave_requests" USING btree ("workspace_id","status","starts_on");--> statement-breakpoint
|
|
161
|
+
CREATE UNIQUE INDEX "hr_leave_requests_idem_uq" ON "mod_hr"."leave_requests" USING btree ("workspace_id","idempotency_key");--> statement-breakpoint
|
|
162
|
+
CREATE UNIQUE INDEX "hr_leave_types_ws_key_uq" ON "mod_hr"."leave_types" USING btree ("workspace_id","key");--> statement-breakpoint
|
|
163
|
+
-- ---------------------------------------------------------------------------------------------
|
|
164
|
+
-- Row-level security on the new tenant tables.
|
|
165
|
+
alter table "mod_hr"."leave_types" enable row level security;--> statement-breakpoint
|
|
166
|
+
alter table "mod_hr"."leave_types" force row level security;--> statement-breakpoint
|
|
167
|
+
create policy "leave_types_ws_isolation" on "mod_hr"."leave_types"
|
|
168
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
169
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
170
|
+
|
|
171
|
+
alter table "mod_hr"."leave_ledger" enable row level security;--> statement-breakpoint
|
|
172
|
+
alter table "mod_hr"."leave_ledger" force row level security;--> statement-breakpoint
|
|
173
|
+
create policy "leave_ledger_ws_isolation" on "mod_hr"."leave_ledger"
|
|
174
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
175
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
176
|
+
|
|
177
|
+
alter table "mod_hr"."leave_balance_cursor" enable row level security;--> statement-breakpoint
|
|
178
|
+
alter table "mod_hr"."leave_balance_cursor" force row level security;--> statement-breakpoint
|
|
179
|
+
create policy "leave_balance_cursor_ws_isolation" on "mod_hr"."leave_balance_cursor"
|
|
180
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
181
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
182
|
+
|
|
183
|
+
alter table "mod_hr"."leave_requests" enable row level security;--> statement-breakpoint
|
|
184
|
+
alter table "mod_hr"."leave_requests" force row level security;--> statement-breakpoint
|
|
185
|
+
create policy "leave_requests_ws_isolation" on "mod_hr"."leave_requests"
|
|
186
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
187
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
188
|
+
|
|
189
|
+
alter table "mod_hr"."leave_request_days" enable row level security;--> statement-breakpoint
|
|
190
|
+
alter table "mod_hr"."leave_request_days" force row level security;--> statement-breakpoint
|
|
191
|
+
create policy "leave_request_days_ws_isolation" on "mod_hr"."leave_request_days"
|
|
192
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
193
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
194
|
+
|
|
195
|
+
alter table "mod_hr"."approval_chains" enable row level security;--> statement-breakpoint
|
|
196
|
+
alter table "mod_hr"."approval_chains" force row level security;--> statement-breakpoint
|
|
197
|
+
create policy "approval_chains_ws_isolation" on "mod_hr"."approval_chains"
|
|
198
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
199
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
200
|
+
|
|
201
|
+
alter table "mod_hr"."approval_requests" enable row level security;--> statement-breakpoint
|
|
202
|
+
alter table "mod_hr"."approval_requests" force row level security;--> statement-breakpoint
|
|
203
|
+
create policy "approval_requests_ws_isolation" on "mod_hr"."approval_requests"
|
|
204
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
205
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
206
|
+
|
|
207
|
+
alter table "mod_hr"."approval_steps" enable row level security;--> statement-breakpoint
|
|
208
|
+
alter table "mod_hr"."approval_steps" force row level security;--> statement-breakpoint
|
|
209
|
+
create policy "approval_steps_ws_isolation" on "mod_hr"."approval_steps"
|
|
210
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
211
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
212
|
+
|
|
213
|
+
alter table "mod_hr"."approval_decisions" enable row level security;--> statement-breakpoint
|
|
214
|
+
alter table "mod_hr"."approval_decisions" force row level security;--> statement-breakpoint
|
|
215
|
+
create policy "approval_decisions_ws_isolation" on "mod_hr"."approval_decisions"
|
|
216
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
217
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
218
|
+
|
|
219
|
+
alter table "mod_hr"."delegations" enable row level security;--> statement-breakpoint
|
|
220
|
+
alter table "mod_hr"."delegations" force row level security;--> statement-breakpoint
|
|
221
|
+
create policy "delegations_ws_isolation" on "mod_hr"."delegations"
|
|
222
|
+
using (workspace_id::text = current_setting('app.workspace_id', true))
|
|
223
|
+
with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
224
|
+
|
|
225
|
+
-- ---------------------------------------------------------------------------------------------
|
|
226
|
+
-- The invariant that keeps a balance honest under concurrency.
|
|
227
|
+
--
|
|
228
|
+
-- Two overlapping requests can both read the same balance, both see enough for the last day, and
|
|
229
|
+
-- both succeed — leaving somebody minus a day and nobody able to say which request caused it. The
|
|
230
|
+
-- cursor lock serialises the *spend*; this index refuses the *overlap*, so a person cannot hold two
|
|
231
|
+
-- live requests covering one date whatever the application layer believes.
|
|
232
|
+
--
|
|
233
|
+
-- Partial on purpose: a cancelled or rejected request must not block rebooking the same day, and a
|
|
234
|
+
-- weekend inside a range is not counted so it does not conflict with anything either.
|
|
235
|
+
create unique index "hr_leave_days_no_double_booking"
|
|
236
|
+
on "mod_hr"."leave_request_days" (workspace_id, person_id, date)
|
|
237
|
+
where counted and status in ('pending', 'approved');
|