@kernhq/module-hr 0.2.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.
- 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/attendance.d.ts +240 -0
- package/dist/contract/attendance.d.ts.map +1 -0
- package/dist/contract/attendance.js +154 -0
- package/dist/contract/attendance.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 +80 -0
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/events.d.ts +60 -0
- package/dist/contract/events.d.ts.map +1 -1
- package/dist/contract/events.js +60 -0
- package/dist/contract/events.js.map +1 -1
- package/dist/contract/index.d.ts +3 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +3 -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 +100 -0
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +119 -0
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/router.d.ts +2440 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +393 -0
- package/dist/contract/router.js.map +1 -1
- package/dist/policy/time.d.ts +48 -0
- package/dist/policy/time.d.ts.map +1 -0
- package/dist/policy/time.js +144 -0
- package/dist/policy/time.js.map +1 -0
- package/dist/policy/working-time.d.ts +98 -0
- package/dist/policy/working-time.d.ts.map +1 -0
- package/dist/policy/working-time.js +190 -0
- package/dist/policy/working-time.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/jobs.d.ts +16 -0
- package/dist/server/jobs.d.ts.map +1 -0
- package/dist/server/jobs.js +166 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/router.d.ts +2731 -58
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +1306 -3
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +3344 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +325 -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/attendance.d.ts +150 -0
- package/dist/server/services/attendance.d.ts.map +1 -0
- package/dist/server/services/attendance.js +239 -0
- package/dist/server/services/attendance.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/0003_attendance.sql +206 -0
- package/migrations/meta/0002_snapshot.json +3009 -0
- package/migrations/meta/0003_snapshot.json +3681 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/src/contract/approvals.ts +149 -0
- package/src/contract/attendance.ts +180 -0
- package/src/contract/capabilities.ts +80 -0
- package/src/contract/events.ts +81 -0
- package/src/contract/index.ts +3 -0
- package/src/contract/leave.ts +171 -0
- package/src/contract/permissions.ts +122 -0
- package/src/contract/router.ts +481 -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":"AAAA,OAAO,EAAE,SAAS,EAAW,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AAE1E,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EACL,eAAe,EACf,UAAU,GAIX,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxG,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAsBnC,sGAAsG;AACtG,MAAM,CAAC,MAAM,WAAW,GAAqB;IAC3C,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI;IACpB,QAAQ,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE;IAClD,wBAAwB,EAAE,IAAI;IAC9B,UAAU,EAAE,IAAI;CACjB,CAAA;AAED,MAAM,OAAO,iBAAiB;IAC5B,yFAAyF;IACzF,KAAK,CAAC,WAAW,CAAC,EAAM,EAAE,WAAmB,EAAE,QAAgB,EAAE,EAAU;QACzE,MAAM,CAAC,UAAU,CAAC,GAAG,MAAM,EAAE;aAC1B,MAAM,EAAE;aACR,IAAI,CAAC,mBAAmB,CAAC;aACzB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,mBAAmB,CAAC,WAAW,EAAE,WAAW,CAAC,EAChD,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAC1C,SAAS,CAAC,mBAAmB,CAAC,aAAa,EAAE,mBAAmB,CAAC,WAAW,EAAE,EAAE,CAAC,CAClF,CACF;aACA,KAAK,CAAC,CAAC,CAAC,CAAA;QACX,IAAI,CAAC,UAAU;YAAE,OAAO,WAAW,CAAA;QAEnC,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE;aACxB,MAAM,EAAE;aACR,IAAI,CAAC,SAAS,CAAC;aACf,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;aAC3F,KAAK,CAAC,CAAC,CAAC,CAAA;QACX,IAAI,CAAC,QAAQ;YAAE,OAAO,WAAW,CAAA;QAEjC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAiB,CAAA;QAClD,OAAO;YACL,UAAU,EAAE,QAAQ,CAAC,EAAE;YACvB,QAAQ,EAAE,CAAC,IAAY,EAAE,EAAE;gBACzB,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;gBACjC,IAAI,CAAC,GAAG;oBAAE,OAAO,IAAI,CAAA;gBACrB,OAAO;oBACL,KAAK,EAAE,GAAG,CAAC,KAAK;oBAChB,GAAG,EAAE,GAAG,CAAC,GAAG;oBACZ,YAAY,EAAE,GAAG,CAAC,YAAY,IAAI,CAAC;oBACnC,cAAc,EAAE,QAAQ,CAAC,cAAc;oBACvC,eAAe,EAAE,QAAQ,CAAC,eAAe;iBAC1C,CAAA;YACH,CAAC;YACD,QAAQ,EAAE;gBACR,WAAW,EAAE,QAAQ,CAAC,mBAAmB;gBACzC,SAAS,EAAE,QAAQ,CAAC,iBAAgD;aACrE;YACD,wBAAwB,EAAE,QAAQ,CAAC,wBAAwB;SAC5D,CAAA;IACH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,MAAM,CACV,EAAM,EACN,WAAmB,EACnB,KAYC,EACD,QAA0B;QAE1B,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAA;QAC5B,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,IAAI,SAAS,CAAA;QACvC,MAAM,QAAQ,GAAG,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACjF,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;QAEzE,8FAA8F;QAC9F,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAEvG,MAAM,YAAY,GAAG,eAAe,CAClC,EAAE,CAAC,OAAO,EAAE,EACZ,KAAK,CAAC,QAAQ,EACd,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CACrE,CAAA;QAED,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;aACnB,MAAM,CAAC,OAAO,CAAC;aACf,MAAM,CAAC;YACN,EAAE,EAAE,MAAM,EAAE;YACZ,WAAW;YACX,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,EAAE;YACF,gBAAgB,EAAE,QAAQ;YAC1B,MAAM;YACN,YAAY;YACZ,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,IAAI;YAChC,GAAG,EAAE,KAAK,CAAC,GAAG,IAAI,IAAI;YACtB,KAAK;YACL,cAAc,EAAE,KAAK,CAAC,cAAc,IAAI,IAAI;YAC5C,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,IAAI;SACzB,CAAC;aACD,SAAS,EAAE,CAAA;QACd,OAAO,GAAI,CAAA;IACb,CAAC;IAED,4FAA4F;IAC5F,KAAK,CAAC,SAAS,CAAC,EAAM,EAAE,WAAmB,EAAE,QAAgB,EAAE,YAAoB;QACjF,OAAO,EAAE;aACN,MAAM,EAAE;aACR,IAAI,CAAC,OAAO,CAAC;aACb,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,WAAW,CAAC,EACpC,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAC9B,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,YAAY,CAAC,EACtC,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAChC,CACF;aACA,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;IAC7B,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,YAAY,CAChB,EAAM,EACN,WAAmB,EACnB,QAAgB,EAChB,YAAoB,EACpB,QAAgB,EAChB,QAA0B;QAE1B,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE;aACxB,MAAM,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,MAAM,EAAE,CAAC;aACzC,IAAI,CAAC,cAAc,CAAC;aACpB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,cAAc,CAAC,WAAW,EAAE,WAAW,CAAC,EAC3C,EAAE,CAAC,cAAc,CAAC,QAAQ,EAAE,QAAQ,CAAC,EACrC,EAAE,CAAC,cAAc,CAAC,YAAY,EAAE,YAAY,CAAC,CAC9C,CACF;aACA,KAAK,CAAC,CAAC,CAAC,CAAA;QACX,IAAI,QAAQ,EAAE,MAAM;YAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;QAE7C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAA;QAC1E,MAAM,WAAW,GAAiB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACjD,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE;YAClB,SAAS,EAAE,CAAC,CAAC,SAAoC;SAClD,CAAC,CAAC,CAAA;QAEH,2FAA2F;QAC3F,sFAAsF;QACtF,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,EAAE;aACvB,MAAM,CAAC,EAAE,SAAS,EAAE,gBAAgB,CAAC,SAAS,EAAE,CAAC;aACjD,IAAI,CAAC,gBAAgB,CAAC;aACtB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,gBAAgB,CAAC,WAAW,EAAE,WAAW,CAAC,EAC7C,EAAE,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EACvC,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,YAAY,CAAC,EACvC,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,EAClC,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,UAAU,CAAC,CACxC,CACF;aACA,KAAK,CAAC,CAAC,CAAC,CAAA;QAEX,MAAM,QAAQ,GAAG,UAAU,CAAC;YAC1B,YAAY;YACZ,QAAQ,EAAE,QAAQ;YAClB,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC;YACtC,OAAO,EAAE,WAAW;YACpB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,OAAO,EAAE,CAAC,CAAC,OAAO;SACnB,CAAC,CAAA;QAEF,MAAM,MAAM,GAAG,OAAO,IAAI,QAAQ,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAA;QAElF,MAAM,EAAE;aACL,MAAM,CAAC,cAAc,CAAC;aACtB,MAAM,CAAC;YACN,EAAE,EAAE,MAAM,EAAE;YACZ,WAAW;YACX,QAAQ;YACR,YAAY;YACZ,gBAAgB,EAAE,QAAQ,CAAC,gBAAgB;YAC3C,aAAa,EAAE,QAAQ,CAAC,aAAa;YACrC,YAAY,EAAE,QAAQ,CAAC,YAAY;YACnC,eAAe,EAAE,QAAQ,CAAC,eAAe;YACzC,WAAW,EAAE,QAAQ,CAAC,WAAW;YACjC,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB;YAC7C,MAAM;YACN,cAAc,EAAE,OAAO,EAAE,SAAS,IAAI,IAAI;YAC1C,SAAS,EAAE,QAAQ,CAAC,SAAS;YAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI;YAC7D,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI;YAC7D,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC;YAClC,UAAU,EAAE,IAAI,IAAI,EAAE;SACvB,CAAC;aACD,kBAAkB,CAAC;YAClB,MAAM,EAAE,CAAC,cAAc,CAAC,WAAW,EAAE,cAAc,CAAC,QAAQ,EAAE,cAAc,CAAC,YAAY,CAAC;YAC1F,GAAG,EAAE;gBACH,gBAAgB,EAAE,QAAQ,CAAC,gBAAgB;gBAC3C,aAAa,EAAE,QAAQ,CAAC,aAAa;gBACrC,YAAY,EAAE,QAAQ,CAAC,YAAY;gBACnC,eAAe,EAAE,QAAQ,CAAC,eAAe;gBACzC,WAAW,EAAE,QAAQ,CAAC,WAAW;gBACjC,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB;gBAC7C,MAAM;gBACN,cAAc,EAAE,OAAO,EAAE,SAAS,IAAI,IAAI;gBAC1C,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI;gBAC7D,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI;gBAC7D,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC;gBAClC,UAAU,EAAE,IAAI,IAAI,EAAE;aACvB;SACF,CAAC,CAAA;QAEJ,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;IAC1B,CAAC;IAED,yFAAyF;IACzF,KAAK,CAAC,gBAAgB,CACpB,EAAM,EACN,WAAmB,EACnB,QAAgB,EAChB,IAAY,EACZ,EAAU;QAEV,MAAM,IAAI,GAAG,MAAM,EAAE;aAClB,cAAc,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC;aACtD,IAAI,CAAC,OAAO,CAAC;aACb,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,WAAW,CAAC,EACpC,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAC9B,GAAG,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,EAC/B,GAAG,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAC9B,CACF,CAAA;QACH,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,CAAA;IAC/C,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,SAAS,CACb,EAAM,EACN,WAAmB,EACnB,OAAe,EACf,MAAc,EACd,aAA4B;QAE5B,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE;aACxB,MAAM,EAAE;aACR,IAAI,CAAC,OAAO,CAAC;aACb,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;aACzE,KAAK,CAAC,CAAC,CAAC,CAAA;QACX,IAAI,CAAC,QAAQ;YAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;QAChD,IAAI,QAAQ,CAAC,eAAe;YAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,8BAA8B,CAAC,CAAA;QAEtF,MAAM,CAAC,UAAU,CAAC,GAAG,MAAM,EAAE;aAC1B,MAAM,CAAC,OAAO,CAAC;aACf,MAAM,CAAC;YACN,EAAE,EAAE,MAAM,EAAE;YACZ,WAAW;YACX,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,SAAS,EAAE,QAAQ,CAAC,SAAS;YAC7B,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,YAAY,EAAE,QAAQ,CAAC,YAAY;YACnC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,MAAM,EAAE,QAAQ;YAChB,KAAK,EAAE,SAAS;YAChB,IAAI,EAAE,SAAS,OAAO,KAAK,MAAM,EAAE;SACpC,CAAC;aACD,SAAS,EAAE,CAAA;QAEd,MAAM,EAAE;aACL,MAAM,CAAC,OAAO,CAAC;aACf,GAAG,CAAC,EAAE,eAAe,EAAE,UAAW,CAAC,EAAE,EAAE,CAAC;aACxC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAEvF,6FAA6F;QAC7F,0CAA0C;QAC1C,MAAM,EAAE;aACL,MAAM,CAAC,OAAO,CAAC;aACf,GAAG,CAAC,EAAE,eAAe,EAAE,UAAW,CAAC,EAAE,EAAE,CAAC;aACxC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,EAAE,UAAW,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAE9F,KAAK,aAAa,CAAA;QAClB,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAW,EAAE,CAAA;IAC9C,CAAC;CACF;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,QAA0B;IAC9C,OAAO,GAAG,QAAQ,CAAC,UAAU,IAAI,MAAM,IAAI,QAAQ,CAAC,QAAQ,CAAC,WAAW,IAAI,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAA;AAC3G,CAAC;AAED,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAA"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type Tx } from '@kernhq/kernel';
|
|
2
|
+
import { inArray } from 'drizzle-orm';
|
|
3
|
+
import type { LedgerKind } from '../../contract/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* The leave balance, and the concurrency that makes it hard.
|
|
6
|
+
*
|
|
7
|
+
* A balance is `sum(leave_ledger.amount_minutes)`. Reading that is easy. Spending it safely is not:
|
|
8
|
+
* two overlapping requests can both read the same balance, both see enough for the last day, and
|
|
9
|
+
* both succeed — leaving somebody minus a day and nobody able to say which request caused it.
|
|
10
|
+
*
|
|
11
|
+
* Two things stop that, and they are deliberately in the database rather than here:
|
|
12
|
+
*
|
|
13
|
+
* 1. `SELECT … FOR UPDATE` on the person's cursor row serialises spending. The second transaction
|
|
14
|
+
* waits, re-reads, and sees the first one's consumption.
|
|
15
|
+
* 2. A partial unique index on `(person, date)` for counted days in a live status means the
|
|
16
|
+
* database refuses to double-book a Tuesday whatever this code believes.
|
|
17
|
+
*
|
|
18
|
+
* Application-level checks alone cannot do either. They are here as well, because a readable error
|
|
19
|
+
* beats a constraint violation — but they are not what makes it correct.
|
|
20
|
+
*/
|
|
21
|
+
export interface BalanceRow {
|
|
22
|
+
personId: string;
|
|
23
|
+
leaveTypeId: string;
|
|
24
|
+
periodYear: number;
|
|
25
|
+
balanceMinutes: number;
|
|
26
|
+
bookedMinutes: number;
|
|
27
|
+
pendingMinutes: number;
|
|
28
|
+
}
|
|
29
|
+
/** A minute count in whatever unit the type displays. Hours to one place, days to two. */
|
|
30
|
+
export declare const toUnit: (minutes: number, unit: string) => number;
|
|
31
|
+
/** Minutes in one working day. A constant here; a policy in Phase 4, per contract. */
|
|
32
|
+
export declare const MINUTES_PER_DAY: number;
|
|
33
|
+
export declare class LedgerService {
|
|
34
|
+
/**
|
|
35
|
+
* Balances for one person, per leave type.
|
|
36
|
+
*
|
|
37
|
+
* Summed from the ledger rather than read from the cursor: the cursor exists to be locked, and a
|
|
38
|
+
* cache that is also the source of truth is a cache that eventually disagrees with it. The sum is
|
|
39
|
+
* one indexed aggregate over a person's own rows, which is small.
|
|
40
|
+
*/
|
|
41
|
+
balances(tx: Tx, workspaceId: string, personId: string, periodYear: number): Promise<{
|
|
42
|
+
personId: string;
|
|
43
|
+
leaveTypeId: string;
|
|
44
|
+
leaveTypeName: string;
|
|
45
|
+
unit: "day" | "half_day" | "hour";
|
|
46
|
+
periodYear: number;
|
|
47
|
+
balanceMinutes: number;
|
|
48
|
+
bookedMinutes: number;
|
|
49
|
+
pendingMinutes: number;
|
|
50
|
+
availableMinutes: number;
|
|
51
|
+
balance: number;
|
|
52
|
+
available: number;
|
|
53
|
+
}[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Take the lock for one person and leave type, creating the cursor row if it is missing.
|
|
56
|
+
*
|
|
57
|
+
* Everything that *spends* balance must call this first, inside the same transaction. It returns
|
|
58
|
+
* the balance as of the moment the lock was acquired, which is the only balance safe to decide on.
|
|
59
|
+
*/
|
|
60
|
+
lockAndRead(tx: Tx, workspaceId: string, personId: string, leaveTypeId: string, periodYear: number): Promise<number>;
|
|
61
|
+
/** Append an entry. The only way anything enters the ledger. */
|
|
62
|
+
append(tx: Tx, workspaceId: string, entry: {
|
|
63
|
+
personId: string;
|
|
64
|
+
leaveTypeId: string;
|
|
65
|
+
kind: LedgerKind;
|
|
66
|
+
amountMinutes: number;
|
|
67
|
+
effectiveOn: string;
|
|
68
|
+
periodYear: number;
|
|
69
|
+
requestId?: string | null;
|
|
70
|
+
reversesEntryId?: string | null;
|
|
71
|
+
reason?: string | null;
|
|
72
|
+
createdBy?: string | null;
|
|
73
|
+
policyHash?: string | null;
|
|
74
|
+
}): Promise<{
|
|
75
|
+
createdAt: Date;
|
|
76
|
+
id: string;
|
|
77
|
+
workspaceId: string;
|
|
78
|
+
personId: string;
|
|
79
|
+
reason: string | null;
|
|
80
|
+
kind: string;
|
|
81
|
+
requestId: string | null;
|
|
82
|
+
policyHash: string | null;
|
|
83
|
+
leaveTypeId: string;
|
|
84
|
+
effectiveOn: string;
|
|
85
|
+
amountMinutes: number;
|
|
86
|
+
periodYear: number;
|
|
87
|
+
reversesEntryId: string | null;
|
|
88
|
+
createdBy: string | null;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Undo an entry by writing its opposite.
|
|
92
|
+
*
|
|
93
|
+
* Never a delete and never an update. A cancelled request has to leave both the consumption and
|
|
94
|
+
* the reversal visible, because "she booked it and then cancelled" and "she never booked it" are
|
|
95
|
+
* different facts and only one of them is true.
|
|
96
|
+
*/
|
|
97
|
+
reverse(tx: Tx, workspaceId: string, entryId: string, reason: string, actorId: string | null, on: string): Promise<{
|
|
98
|
+
createdAt: Date;
|
|
99
|
+
id: string;
|
|
100
|
+
workspaceId: string;
|
|
101
|
+
personId: string;
|
|
102
|
+
reason: string | null;
|
|
103
|
+
kind: string;
|
|
104
|
+
requestId: string | null;
|
|
105
|
+
policyHash: string | null;
|
|
106
|
+
leaveTypeId: string;
|
|
107
|
+
effectiveOn: string;
|
|
108
|
+
amountMinutes: number;
|
|
109
|
+
periodYear: number;
|
|
110
|
+
reversesEntryId: string | null;
|
|
111
|
+
createdBy: string | null;
|
|
112
|
+
}>;
|
|
113
|
+
/** Every entry a request produced, for cancelling it. */
|
|
114
|
+
entriesFor(tx: Tx, workspaceId: string, requestId: string): Promise<{
|
|
115
|
+
id: string;
|
|
116
|
+
workspaceId: string;
|
|
117
|
+
personId: string;
|
|
118
|
+
leaveTypeId: string;
|
|
119
|
+
kind: string;
|
|
120
|
+
amountMinutes: number;
|
|
121
|
+
effectiveOn: string;
|
|
122
|
+
periodYear: number;
|
|
123
|
+
requestId: string | null;
|
|
124
|
+
reversesEntryId: string | null;
|
|
125
|
+
policyHash: string | null;
|
|
126
|
+
reason: string | null;
|
|
127
|
+
createdBy: string | null;
|
|
128
|
+
createdAt: Date;
|
|
129
|
+
}[]>;
|
|
130
|
+
/** Rebuild every cursor from the ledger. Cheap insurance after a bulk import or a bad migration. */
|
|
131
|
+
rebuildCursors(tx: Tx, workspaceId: string, personIds: string[]): Promise<void>;
|
|
132
|
+
}
|
|
133
|
+
export declare const yearOf: (isoDate: string) => number;
|
|
134
|
+
export { inArray };
|
|
135
|
+
//# sourceMappingURL=ledger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../../src/server/services/ledger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAW,OAAO,EAAO,MAAM,aAAa,CAAA;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAGzD;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,0FAA0F;AAC1F,eAAO,MAAM,MAAM,GAAI,SAAS,MAAM,EAAE,MAAM,MAAM,KAAG,MACgD,CAAA;AAEvG,sFAAsF;AACtF,eAAO,MAAM,eAAe,QAAS,CAAA;AAErC,qBAAa,aAAa;IACxB;;;;;;OAMG;IACG,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;;;;cAmDvD,KAAK,GAAG,UAAU,GAAG,MAAM;;;;;;;;;IAYpD;;;;;OAKG;IACG,WAAW,CACf,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC;IA6BlB,gEAAgE;IAC1D,MAAM,CACV,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;QACnB,IAAI,EAAE,UAAU,CAAA;QAChB,aAAa,EAAE,MAAM,CAAA;QACrB,WAAW,EAAE,MAAM,CAAA;QACnB,UAAU,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACtB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC3B;;;;;;;;;;;;;;;;IAyBH;;;;;;OAMG;IACG,OAAO,CACX,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;IAiCZ,yDAAyD;IACnD,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;IAO/D,oGAAoG;IAC9F,cAAc,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE;CAuBtE;AAED,eAAO,MAAM,MAAM,GAAI,SAAS,MAAM,KAAG,MAAqC,CAAA;AAC9E,OAAO,EAAE,OAAO,EAAE,CAAA"}
|
|
@@ -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
|
}
|