@kernhq/module-hr 0.3.0 → 0.4.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 (61) hide show
  1. package/dist/contract/attendance.d.ts +240 -0
  2. package/dist/contract/attendance.d.ts.map +1 -0
  3. package/dist/contract/attendance.js +154 -0
  4. package/dist/contract/attendance.js.map +1 -0
  5. package/dist/contract/capabilities.d.ts.map +1 -1
  6. package/dist/contract/capabilities.js +36 -0
  7. package/dist/contract/capabilities.js.map +1 -1
  8. package/dist/contract/events.d.ts +18 -0
  9. package/dist/contract/events.d.ts.map +1 -1
  10. package/dist/contract/events.js +18 -0
  11. package/dist/contract/events.js.map +1 -1
  12. package/dist/contract/index.d.ts +1 -0
  13. package/dist/contract/index.d.ts.map +1 -1
  14. package/dist/contract/index.js +1 -0
  15. package/dist/contract/index.js.map +1 -1
  16. package/dist/contract/permissions.d.ts +43 -0
  17. package/dist/contract/permissions.d.ts.map +1 -1
  18. package/dist/contract/permissions.js +50 -0
  19. package/dist/contract/permissions.js.map +1 -1
  20. package/dist/contract/router.d.ts +994 -0
  21. package/dist/contract/router.d.ts.map +1 -1
  22. package/dist/contract/router.js +158 -0
  23. package/dist/contract/router.js.map +1 -1
  24. package/dist/policy/time.d.ts +48 -0
  25. package/dist/policy/time.d.ts.map +1 -0
  26. package/dist/policy/time.js +144 -0
  27. package/dist/policy/time.js.map +1 -0
  28. package/dist/policy/working-time.d.ts +98 -0
  29. package/dist/policy/working-time.d.ts.map +1 -0
  30. package/dist/policy/working-time.js +190 -0
  31. package/dist/policy/working-time.js.map +1 -0
  32. package/dist/server/index.d.ts.map +1 -1
  33. package/dist/server/index.js +2 -0
  34. package/dist/server/index.js.map +1 -1
  35. package/dist/server/jobs.d.ts +16 -0
  36. package/dist/server/jobs.d.ts.map +1 -0
  37. package/dist/server/jobs.js +166 -0
  38. package/dist/server/jobs.js.map +1 -0
  39. package/dist/server/router.d.ts +1085 -0
  40. package/dist/server/router.d.ts.map +1 -1
  41. package/dist/server/router.js +495 -2
  42. package/dist/server/router.js.map +1 -1
  43. package/dist/server/schema.d.ts +1261 -1
  44. package/dist/server/schema.d.ts.map +1 -1
  45. package/dist/server/schema.js +116 -0
  46. package/dist/server/schema.js.map +1 -1
  47. package/dist/server/services/attendance.d.ts +150 -0
  48. package/dist/server/services/attendance.d.ts.map +1 -0
  49. package/dist/server/services/attendance.js +239 -0
  50. package/dist/server/services/attendance.js.map +1 -0
  51. package/dist/server/services/ledger.d.ts +2 -2
  52. package/migrations/0003_attendance.sql +206 -0
  53. package/migrations/meta/0003_snapshot.json +3681 -0
  54. package/migrations/meta/_journal.json +7 -0
  55. package/package.json +1 -1
  56. package/src/contract/attendance.ts +180 -0
  57. package/src/contract/capabilities.ts +36 -0
  58. package/src/contract/events.ts +24 -0
  59. package/src/contract/index.ts +1 -0
  60. package/src/contract/permissions.ts +51 -0
  61. package/src/contract/router.ts +196 -0
@@ -0,0 +1,239 @@
1
+ import { KernError, uuidv7 } from '@kernhq/kernel';
2
+ import { and, asc, eq, gte, inArray, isNull, lte, sql } from 'drizzle-orm';
3
+ import { weekdayOf } from '../../policy/calendar.js';
4
+ import { businessDateFor, computeDay, } from '../../policy/working-time.js';
5
+ import { attendanceDays, leaveRequestDays, punches, scheduleAssignments, schedules } from '../schema.js';
6
+ import { inForceOn } from './db.js';
7
+ /** A schedule with no shifts: somebody with no assignment still clocks in, they just owe no hours. */
8
+ export const NO_SCHEDULE = {
9
+ shiftFor: () => null,
10
+ rounding: { stepMinutes: 0, direction: 'nearest' },
11
+ autoClockOutAfterMinutes: null,
12
+ scheduleId: null,
13
+ };
14
+ export class AttendanceService {
15
+ /** The schedule in force for a person on a date, as something the pure layer can use. */
16
+ async scheduleFor(tx, workspaceId, personId, on) {
17
+ const [assignment] = await tx
18
+ .select()
19
+ .from(scheduleAssignments)
20
+ .where(and(eq(scheduleAssignments.workspaceId, workspaceId), eq(scheduleAssignments.personId, personId), inForceOn(scheduleAssignments.effectiveFrom, scheduleAssignments.effectiveTo, on)))
21
+ .limit(1);
22
+ if (!assignment)
23
+ return NO_SCHEDULE;
24
+ const [schedule] = await tx
25
+ .select()
26
+ .from(schedules)
27
+ .where(and(eq(schedules.workspaceId, workspaceId), eq(schedules.id, assignment.scheduleId)))
28
+ .limit(1);
29
+ if (!schedule)
30
+ return NO_SCHEDULE;
31
+ const week = (schedule.week ?? {});
32
+ return {
33
+ scheduleId: schedule.id,
34
+ shiftFor: (date) => {
35
+ const day = week[weekdayOf(date)];
36
+ if (!day)
37
+ return null;
38
+ return {
39
+ start: day.start,
40
+ end: day.end,
41
+ breakMinutes: day.breakMinutes ?? 0,
42
+ graceInMinutes: schedule.graceInMinutes,
43
+ graceOutMinutes: schedule.graceOutMinutes,
44
+ };
45
+ },
46
+ rounding: {
47
+ stepMinutes: schedule.roundingStepMinutes,
48
+ direction: schedule.roundingDirection,
49
+ },
50
+ autoClockOutAfterMinutes: schedule.autoClockOutAfterMinutes,
51
+ };
52
+ }
53
+ /**
54
+ * Record a punch.
55
+ *
56
+ * `at` is the server's clock, always. `clientReportedAt` is kept beside it with the measured skew
57
+ * so an offline sync can be told from a device whose clock is wrong — and a claim beyond the
58
+ * threshold is marked `disputed` rather than silently accepted or silently dropped.
59
+ */
60
+ async record(tx, workspaceId, input, schedule) {
61
+ const serverNow = new Date();
62
+ const at = input.claimedAt ?? serverNow;
63
+ const clientAt = input.clientReportedAt ? new Date(input.clientReportedAt) : null;
64
+ const skewMs = clientAt ? clientAt.getTime() - serverNow.getTime() : null;
65
+ // A claim more than an hour out is not a slow network; it is a clock somebody should look at.
66
+ const trust = input.claimedAt ? (Math.abs(skewMs ?? 0) > 3600_000 ? 'disputed' : 'claimed') : 'trusted';
67
+ const businessDate = businessDateFor(at.getTime(), input.timezone, schedule.shiftFor(new Date(at.getTime()).toISOString().slice(0, 10)));
68
+ const [row] = await tx
69
+ .insert(punches)
70
+ .values({
71
+ id: uuidv7(),
72
+ workspaceId,
73
+ personId: input.personId,
74
+ direction: input.direction,
75
+ at,
76
+ clientReportedAt: clientAt,
77
+ skewMs,
78
+ businessDate,
79
+ timezone: input.timezone,
80
+ method: input.method,
81
+ officeId: input.officeId ?? null,
82
+ geo: input.geo ?? null,
83
+ trust,
84
+ idempotencyKey: input.idempotencyKey ?? null,
85
+ note: input.note ?? null,
86
+ })
87
+ .returning();
88
+ return row;
89
+ }
90
+ /** Live punches for a person on a business date, oldest first. Voided rows are excluded. */
91
+ async punchesOn(tx, workspaceId, personId, businessDate) {
92
+ return tx
93
+ .select()
94
+ .from(punches)
95
+ .where(and(eq(punches.workspaceId, workspaceId), eq(punches.personId, personId), eq(punches.businessDate, businessDate), isNull(punches.voidedByPunchId)))
96
+ .orderBy(asc(punches.at));
97
+ }
98
+ /**
99
+ * Rebuild one day from its punches.
100
+ *
101
+ * Idempotent by construction, which is what makes it safe to call on every punch, from a nightly
102
+ * sweep, and from a support request without thinking about it. A locked day is left alone and
103
+ * reported, never silently skipped — a recomputation that quietly declines to touch a closed
104
+ * month looks identical to one that had nothing to do.
105
+ */
106
+ async recomputeDay(tx, workspaceId, personId, businessDate, timezone, schedule) {
107
+ const [existing] = await tx
108
+ .select({ locked: attendanceDays.locked })
109
+ .from(attendanceDays)
110
+ .where(and(eq(attendanceDays.workspaceId, workspaceId), eq(attendanceDays.personId, personId), eq(attendanceDays.businessDate, businessDate)))
111
+ .limit(1);
112
+ if (existing?.locked)
113
+ return { locked: true };
114
+ const rows = await this.punchesOn(tx, workspaceId, personId, businessDate);
115
+ const punchInputs = rows.map((r) => ({
116
+ at: r.at.getTime(),
117
+ direction: r.direction,
118
+ }));
119
+ // Approved leave excuses the day: somebody on holiday is not absent, and a sheet that says
120
+ // otherwise generates a disciplinary conversation about a day HR themselves approved.
121
+ const [onLeave] = await tx
122
+ .select({ requestId: leaveRequestDays.requestId })
123
+ .from(leaveRequestDays)
124
+ .where(and(eq(leaveRequestDays.workspaceId, workspaceId), eq(leaveRequestDays.personId, personId), eq(leaveRequestDays.date, businessDate), eq(leaveRequestDays.counted, true), eq(leaveRequestDays.status, 'approved')))
125
+ .limit(1);
126
+ const computed = computeDay({
127
+ businessDate,
128
+ timeZone: timezone,
129
+ shift: schedule.shiftFor(businessDate),
130
+ punches: punchInputs,
131
+ rounding: schedule.rounding,
132
+ excused: !!onLeave,
133
+ });
134
+ const status = onLeave && computed.workedMinutes === 0 ? 'leave' : computed.status;
135
+ await tx
136
+ .insert(attendanceDays)
137
+ .values({
138
+ id: uuidv7(),
139
+ workspaceId,
140
+ personId,
141
+ businessDate,
142
+ scheduledMinutes: computed.scheduledMinutes,
143
+ workedMinutes: computed.workedMinutes,
144
+ breakMinutes: computed.breakMinutes,
145
+ overtimeMinutes: computed.overtimeMinutes,
146
+ lateMinutes: computed.lateMinutes,
147
+ earlyLeaveMinutes: computed.earlyLeaveMinutes,
148
+ status,
149
+ leaveRequestId: onLeave?.requestId ?? null,
150
+ anomalies: computed.anomalies,
151
+ firstIn: computed.firstIn ? new Date(computed.firstIn) : null,
152
+ lastOut: computed.lastOut ? new Date(computed.lastOut) : null,
153
+ policyHash: hashSchedule(schedule),
154
+ computedAt: new Date(),
155
+ })
156
+ .onConflictDoUpdate({
157
+ target: [attendanceDays.workspaceId, attendanceDays.personId, attendanceDays.businessDate],
158
+ set: {
159
+ scheduledMinutes: computed.scheduledMinutes,
160
+ workedMinutes: computed.workedMinutes,
161
+ breakMinutes: computed.breakMinutes,
162
+ overtimeMinutes: computed.overtimeMinutes,
163
+ lateMinutes: computed.lateMinutes,
164
+ earlyLeaveMinutes: computed.earlyLeaveMinutes,
165
+ status,
166
+ leaveRequestId: onLeave?.requestId ?? null,
167
+ anomalies: computed.anomalies,
168
+ firstIn: computed.firstIn ? new Date(computed.firstIn) : null,
169
+ lastOut: computed.lastOut ? new Date(computed.lastOut) : null,
170
+ policyHash: hashSchedule(schedule),
171
+ computedAt: new Date(),
172
+ },
173
+ });
174
+ return { locked: false };
175
+ }
176
+ /** Which business dates a person has punches on, in a range. Drives a bulk recompute. */
177
+ async datesWithPunches(tx, workspaceId, personId, from, to) {
178
+ const rows = await tx
179
+ .selectDistinct({ businessDate: punches.businessDate })
180
+ .from(punches)
181
+ .where(and(eq(punches.workspaceId, workspaceId), eq(punches.personId, personId), gte(punches.businessDate, from), lte(punches.businessDate, to)));
182
+ return rows.map((r) => r.businessDate).sort();
183
+ }
184
+ /**
185
+ * Void a punch by writing a correcting row that points at it.
186
+ *
187
+ * The original stays exactly as it was. Two rows is the point: "this was recorded and then
188
+ * corrected" and "this was never recorded" are different facts, and only one of them is true.
189
+ */
190
+ async voidPunch(tx, workspaceId, punchId, reason, actorPersonId) {
191
+ const [original] = await tx
192
+ .select()
193
+ .from(punches)
194
+ .where(and(eq(punches.workspaceId, workspaceId), eq(punches.id, punchId)))
195
+ .limit(1);
196
+ if (!original)
197
+ throw KernError.notFound('Punch');
198
+ if (original.voidedByPunchId)
199
+ throw KernError.conflict('That punch is already voided');
200
+ const [correction] = await tx
201
+ .insert(punches)
202
+ .values({
203
+ id: uuidv7(),
204
+ workspaceId,
205
+ personId: original.personId,
206
+ direction: original.direction,
207
+ at: original.at,
208
+ businessDate: original.businessDate,
209
+ timezone: original.timezone,
210
+ method: 'manual',
211
+ trust: 'trusted',
212
+ note: `Voids ${punchId}: ${reason}`,
213
+ })
214
+ .returning();
215
+ await tx
216
+ .update(punches)
217
+ .set({ voidedByPunchId: correction.id })
218
+ .where(and(eq(punches.id, punchId), eq(punches.businessDate, original.businessDate)));
219
+ // The correcting row is itself voided: it exists to carry the reason and to point at what it
220
+ // replaced, not to be counted as a punch.
221
+ await tx
222
+ .update(punches)
223
+ .set({ voidedByPunchId: correction.id })
224
+ .where(and(eq(punches.id, correction.id), eq(punches.businessDate, original.businessDate)));
225
+ void actorPersonId;
226
+ return { original, correction: correction };
227
+ }
228
+ }
229
+ /**
230
+ * A short stamp of what produced a derived row.
231
+ *
232
+ * Not cryptographic — it only has to change when the schedule does, so a recomputation can tell a
233
+ * stale row from a current one without re-deriving it.
234
+ */
235
+ function hashSchedule(schedule) {
236
+ return `${schedule.scheduleId ?? 'none'}:${schedule.rounding.stepMinutes}:${schedule.rounding.direction}`;
237
+ }
238
+ export { inArray, sql };
239
+ //# sourceMappingURL=attendance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attendance.js","sourceRoot":"","sources":["../../../src/server/services/attendance.ts"],"names":[],"mappings":"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"}
@@ -79,12 +79,12 @@ export declare class LedgerService {
79
79
  reason: string | null;
80
80
  kind: string;
81
81
  requestId: string | null;
82
+ policyHash: string | null;
82
83
  leaveTypeId: string;
83
84
  effectiveOn: string;
84
85
  amountMinutes: number;
85
86
  periodYear: number;
86
87
  reversesEntryId: string | null;
87
- policyHash: string | null;
88
88
  createdBy: string | null;
89
89
  }>;
90
90
  /**
@@ -102,12 +102,12 @@ export declare class LedgerService {
102
102
  reason: string | null;
103
103
  kind: string;
104
104
  requestId: string | null;
105
+ policyHash: string | null;
105
106
  leaveTypeId: string;
106
107
  effectiveOn: string;
107
108
  amountMinutes: number;
108
109
  periodYear: number;
109
110
  reversesEntryId: string | null;
110
- policyHash: string | null;
111
111
  createdBy: string | null;
112
112
  }>;
113
113
  /** Every entry a request produced, for cancelling it. */
@@ -0,0 +1,206 @@
1
+ CREATE TABLE "mod_hr"."attendance_days" (
2
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
3
+ "workspace_id" uuid NOT NULL,
4
+ "person_id" uuid NOT NULL,
5
+ "business_date" date NOT NULL,
6
+ "scheduled_minutes" integer DEFAULT 0 NOT NULL,
7
+ "worked_minutes" integer DEFAULT 0 NOT NULL,
8
+ "break_minutes" integer DEFAULT 0 NOT NULL,
9
+ "overtime_minutes" integer DEFAULT 0 NOT NULL,
10
+ "late_minutes" integer DEFAULT 0 NOT NULL,
11
+ "early_leave_minutes" integer DEFAULT 0 NOT NULL,
12
+ "status" text DEFAULT 'absent' NOT NULL,
13
+ "leave_request_id" uuid,
14
+ "anomalies" text[] DEFAULT '{}'::text[] NOT NULL,
15
+ "first_in" timestamp with time zone,
16
+ "last_out" timestamp with time zone,
17
+ "policy_hash" text,
18
+ "locked" boolean DEFAULT false NOT NULL,
19
+ "computed_at" timestamp with time zone DEFAULT now() NOT NULL
20
+ );
21
+ --> statement-breakpoint
22
+ CREATE TABLE "mod_hr"."punches" (
23
+ "id" uuid DEFAULT uuidv7() NOT NULL,
24
+ "workspace_id" uuid NOT NULL,
25
+ "person_id" uuid NOT NULL,
26
+ "direction" text NOT NULL,
27
+ "at" timestamp with time zone DEFAULT now() NOT NULL,
28
+ "client_reported_at" timestamp with time zone,
29
+ "skew_ms" integer,
30
+ "business_date" date NOT NULL,
31
+ "timezone" text DEFAULT 'UTC' NOT NULL,
32
+ "method" text DEFAULT 'web' NOT NULL,
33
+ "office_id" uuid,
34
+ "device_id" uuid,
35
+ "geo" jsonb,
36
+ "trust" text DEFAULT 'trusted' NOT NULL,
37
+ "voided_by_punch_id" uuid,
38
+ "idempotency_key" text,
39
+ "note" text,
40
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
41
+ ) PARTITION BY RANGE ("business_date");
42
+ --> statement-breakpoint
43
+ -- Partitioned, because five hundred people punching four times a day is half a million rows a year
44
+ -- and retrofitting partitioning onto a live table of those is a migration nobody wants. drizzle-kit
45
+ -- cannot express PARTITION BY, so this block is hand-maintained: **after `pnpm db:generate`,
46
+ -- re-add the `PARTITION BY`, the primary key and the partitions below.**
47
+ --
48
+ -- A partitioned table's primary key must contain the partition column, which is why it is
49
+ -- (id, business_date) rather than (id). Same for the idempotency index: without business_date in it
50
+ -- Postgres refuses to create it at all.
51
+ ALTER TABLE "mod_hr"."punches" ADD PRIMARY KEY ("id", "business_date");
52
+ --> statement-breakpoint
53
+ CREATE UNIQUE INDEX "hr_punches_idem_uq"
54
+ ON "mod_hr"."punches" ("workspace_id", "idempotency_key", "business_date")
55
+ WHERE "idempotency_key" IS NOT NULL;
56
+ --> statement-breakpoint
57
+ -- A DEFAULT partition catches anything outside the ranges below, so a missing monthly partition
58
+ -- degrades to a slow insert rather than a failed punch. Attendance that refuses to record because
59
+ -- a maintenance job did not run is worse than attendance that records into the wrong file.
60
+ CREATE TABLE IF NOT EXISTS "mod_hr"."punches_default"
61
+ PARTITION OF "mod_hr"."punches" DEFAULT;
62
+ --> statement-breakpoint
63
+ ALTER TABLE "mod_hr"."punches_default" ENABLE ROW LEVEL SECURITY;
64
+ --> statement-breakpoint
65
+ ALTER TABLE "mod_hr"."punches_default" FORCE ROW LEVEL SECURITY;
66
+ --> statement-breakpoint
67
+ CREATE POLICY "punches_ws_isolation" ON "mod_hr"."punches_default"
68
+ USING (workspace_id::text = current_setting('app.workspace_id', true))
69
+ WITH CHECK (workspace_id::text = current_setting('app.workspace_id', true));
70
+ --> statement-breakpoint
71
+ -- Creating a partition is not just `CREATE TABLE ... PARTITION OF`.
72
+ --
73
+ -- A policy on a partitioned parent applies when you query **through the parent**. Query a partition
74
+ -- directly and its own row-level security setting applies — and a fresh partition has none. Any
75
+ -- deployment that runs `GRANT SELECT ON ALL TABLES IN SCHEMA mod_hr` therefore hands out an
76
+ -- unfiltered view of one month's punches, silently, to every role that grant touched.
77
+ --
78
+ -- So partition creation goes through this function, which secures what it creates. The monthly job
79
+ -- calls the same function, which is the only way it cannot forget.
80
+ CREATE OR REPLACE FUNCTION "mod_hr".ensure_punch_partition(month_start date)
81
+ RETURNS text
82
+ LANGUAGE plpgsql
83
+ AS $fn$
84
+ DECLARE
85
+ part_name text := 'punches_' || to_char(month_start, 'YYYY_MM');
86
+ BEGIN
87
+ EXECUTE format(
88
+ 'CREATE TABLE IF NOT EXISTS %I.%I PARTITION OF %I.%I FOR VALUES FROM (%L) TO (%L)',
89
+ 'mod_hr', part_name, 'mod_hr', 'punches', month_start, (month_start + interval '1 month')::date
90
+ );
91
+ EXECUTE format('ALTER TABLE %I.%I ENABLE ROW LEVEL SECURITY', 'mod_hr', part_name);
92
+ EXECUTE format('ALTER TABLE %I.%I FORCE ROW LEVEL SECURITY', 'mod_hr', part_name);
93
+ -- `IF NOT EXISTS` on CREATE POLICY needs Postgres 18; the catalogue check works everywhere and
94
+ -- keeps this function safe to call repeatedly, which the monthly job relies on.
95
+ IF NOT EXISTS (
96
+ SELECT 1 FROM pg_policies
97
+ WHERE schemaname = 'mod_hr' AND tablename = part_name AND policyname = 'punches_ws_isolation'
98
+ ) THEN
99
+ EXECUTE format(
100
+ 'CREATE POLICY "punches_ws_isolation" ON %I.%I
101
+ USING (workspace_id::text = current_setting(''app.workspace_id'', true))
102
+ WITH CHECK (workspace_id::text = current_setting(''app.workspace_id'', true))',
103
+ 'mod_hr', part_name
104
+ );
105
+ END IF;
106
+ RETURN part_name;
107
+ END
108
+ $fn$;
109
+ --> statement-breakpoint
110
+ -- Seeds a window either side of today so a fresh instance can record punches immediately.
111
+ -- `hr.ensure-partitions` keeps it rolling.
112
+ DO $$
113
+ DECLARE
114
+ m date := (date_trunc('month', now()) - interval '3 months')::date;
115
+ stop date := (date_trunc('month', now()) + interval '12 months')::date;
116
+ BEGIN
117
+ WHILE m < stop LOOP
118
+ PERFORM "mod_hr".ensure_punch_partition(m);
119
+ m := (m + interval '1 month')::date;
120
+ END LOOP;
121
+ END $$;
122
+ --> statement-breakpoint
123
+ CREATE TABLE "mod_hr"."regularizations" (
124
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
125
+ "workspace_id" uuid NOT NULL,
126
+ "person_id" uuid NOT NULL,
127
+ "business_date" date NOT NULL,
128
+ "punch_id" uuid,
129
+ "proposed" jsonb NOT NULL,
130
+ "reason" text NOT NULL,
131
+ "status" text DEFAULT 'pending' NOT NULL,
132
+ "approval_request_id" uuid,
133
+ "applied_at" timestamp with time zone,
134
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
135
+ );
136
+ --> statement-breakpoint
137
+ CREATE TABLE "mod_hr"."schedule_assignments" (
138
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
139
+ "workspace_id" uuid NOT NULL,
140
+ "person_id" uuid NOT NULL,
141
+ "schedule_id" uuid NOT NULL,
142
+ "effective_from" date NOT NULL,
143
+ "effective_to" date,
144
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
145
+ );
146
+ --> statement-breakpoint
147
+ CREATE TABLE "mod_hr"."schedules" (
148
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
149
+ "workspace_id" uuid NOT NULL,
150
+ "name" text NOT NULL,
151
+ "kind" text DEFAULT 'fixed' NOT NULL,
152
+ "week" jsonb DEFAULT '{}'::jsonb NOT NULL,
153
+ "tz_mode" text DEFAULT 'office' NOT NULL,
154
+ "tz" text,
155
+ "grace_in_minutes" integer DEFAULT 0 NOT NULL,
156
+ "grace_out_minutes" integer DEFAULT 0 NOT NULL,
157
+ "rounding_step_minutes" integer DEFAULT 0 NOT NULL,
158
+ "rounding_direction" text DEFAULT 'nearest' NOT NULL,
159
+ "auto_clock_out_after_minutes" integer,
160
+ "archived_at" timestamp with time zone,
161
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
162
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
163
+ );
164
+ --> statement-breakpoint
165
+ CREATE UNIQUE INDEX "hr_attendance_days_uq" ON "mod_hr"."attendance_days" USING btree ("workspace_id","person_id","business_date");--> statement-breakpoint
166
+ CREATE INDEX "hr_attendance_days_date_idx" ON "mod_hr"."attendance_days" USING btree ("workspace_id","business_date","status");--> statement-breakpoint
167
+ CREATE INDEX "hr_punches_person_idx" ON "mod_hr"."punches" USING btree ("workspace_id","person_id","business_date");--> statement-breakpoint
168
+ CREATE INDEX "hr_regularizations_idx" ON "mod_hr"."regularizations" USING btree ("workspace_id","person_id","business_date");--> statement-breakpoint
169
+ CREATE INDEX "hr_schedule_assign_idx" ON "mod_hr"."schedule_assignments" USING btree ("workspace_id","person_id","effective_from");--> statement-breakpoint
170
+ CREATE INDEX "hr_schedules_ws_idx" ON "mod_hr"."schedules" USING btree ("workspace_id","archived_at");--> statement-breakpoint
171
+ -- ---------------------------------------------------------------------------------------------
172
+ -- Row-level security on the attendance tables.
173
+ --
174
+ -- A policy on a partitioned parent applies to every partition, including ones created later — which
175
+ -- is what makes the rolling partition job safe. The test asserts a freshly created partition
176
+ -- returns nothing without `app.workspace_id`, because "it inherits" is the kind of thing that is
177
+ -- true until a Postgres upgrade says otherwise.
178
+ alter table "mod_hr"."schedules" enable row level security;--> statement-breakpoint
179
+ alter table "mod_hr"."schedules" force row level security;--> statement-breakpoint
180
+ create policy "schedules_ws_isolation" on "mod_hr"."schedules"
181
+ using (workspace_id::text = current_setting('app.workspace_id', true))
182
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
183
+
184
+ alter table "mod_hr"."schedule_assignments" enable row level security;--> statement-breakpoint
185
+ alter table "mod_hr"."schedule_assignments" force row level security;--> statement-breakpoint
186
+ create policy "schedule_assignments_ws_isolation" on "mod_hr"."schedule_assignments"
187
+ using (workspace_id::text = current_setting('app.workspace_id', true))
188
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
189
+
190
+ alter table "mod_hr"."punches" enable row level security;--> statement-breakpoint
191
+ alter table "mod_hr"."punches" force row level security;--> statement-breakpoint
192
+ create policy "punches_ws_isolation" on "mod_hr"."punches"
193
+ using (workspace_id::text = current_setting('app.workspace_id', true))
194
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
195
+
196
+ alter table "mod_hr"."attendance_days" enable row level security;--> statement-breakpoint
197
+ alter table "mod_hr"."attendance_days" force row level security;--> statement-breakpoint
198
+ create policy "attendance_days_ws_isolation" on "mod_hr"."attendance_days"
199
+ using (workspace_id::text = current_setting('app.workspace_id', true))
200
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
201
+
202
+ alter table "mod_hr"."regularizations" enable row level security;--> statement-breakpoint
203
+ alter table "mod_hr"."regularizations" force row level security;--> statement-breakpoint
204
+ create policy "regularizations_ws_isolation" on "mod_hr"."regularizations"
205
+ using (workspace_id::text = current_setting('app.workspace_id', true))
206
+ with check (workspace_id::text = current_setting('app.workspace_id', true));