@happyvertical/smrt-timesheets 0.55.24 → 0.56.1
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/AGENTS.md +60 -0
- package/README.md +190 -12
- package/agents/period-rollup.md +20 -0
- package/dist/attendance/context.d.ts +80 -0
- package/dist/attendance/context.d.ts.map +1 -0
- package/dist/attendance/correction-models.d.ts +39 -0
- package/dist/attendance/correction-models.d.ts.map +1 -0
- package/dist/attendance/correction-service.d.ts +118 -0
- package/dist/attendance/correction-service.d.ts.map +1 -0
- package/dist/attendance/models.d.ts +57 -0
- package/dist/attendance/models.d.ts.map +1 -1
- package/dist/attendance/segments.d.ts +20 -0
- package/dist/attendance/segments.d.ts.map +1 -0
- package/dist/attendance/service.d.ts +106 -22
- package/dist/attendance/service.d.ts.map +1 -1
- package/dist/attendance/write.d.ts +12 -0
- package/dist/attendance/write.d.ts.map +1 -0
- package/dist/attendance.d.ts +2 -0
- package/dist/attendance.d.ts.map +1 -1
- package/dist/attendance.js +3 -323
- package/dist/chunks/{attendance-Cpo4mIID.js → attendance-Cw75uJnT.js} +3 -3
- package/dist/chunks/{attendance-Cpo4mIID.js.map → attendance-Cw75uJnT.js.map} +1 -1
- package/dist/chunks/attendance-DJNytMTN.js +1246 -0
- package/dist/chunks/attendance-DJNytMTN.js.map +1 -0
- package/dist/chunks/models-tUePZ5x7.js +372 -0
- package/dist/chunks/models-tUePZ5x7.js.map +1 -0
- package/dist/chunks/{rollup-Du6uaqyM.js → rollup-CzCb-DxI.js} +211 -6
- package/dist/chunks/rollup-CzCb-DxI.js.map +1 -0
- package/dist/chunks/service-time-entry-BJVSGx33.js +286 -0
- package/dist/chunks/service-time-entry-BJVSGx33.js.map +1 -0
- package/dist/chunks/time-entry-actions-ChT1lcOp.js +576 -0
- package/dist/chunks/time-entry-actions-ChT1lcOp.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -334
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2020 -168
- package/dist/models/service-snapshots.d.ts.map +1 -1
- package/dist/models/service-time-entry.d.ts +36 -1
- package/dist/models/service-time-entry.d.ts.map +1 -1
- package/dist/recipes.d.ts +124 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/rollup/correction-guard.d.ts +12 -0
- package/dist/rollup/correction-guard.d.ts.map +1 -0
- package/dist/rollup/models.d.ts +31 -1
- package/dist/rollup/models.d.ts.map +1 -1
- package/dist/rollup/service.d.ts +25 -7
- package/dist/rollup/service.d.ts.map +1 -1
- package/dist/rollup/types.d.ts +18 -0
- package/dist/rollup/types.d.ts.map +1 -1
- package/dist/rollup/write.d.ts +2 -0
- package/dist/rollup/write.d.ts.map +1 -1
- package/dist/rollup.d.ts +3 -2
- package/dist/rollup.d.ts.map +1 -1
- package/dist/rollup.js +2 -2
- package/dist/services/action-principal.d.ts +54 -0
- package/dist/services/action-principal.d.ts.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/time-entry-actions.d.ts +83 -0
- package/dist/services/time-entry-actions.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +902 -40
- package/package.json +8 -7
- package/dist/attendance.js.map +0 -1
- package/dist/chunks/models-_ZbpXWnb.js +0 -176
- package/dist/chunks/models-_ZbpXWnb.js.map +0 -1
- package/dist/chunks/rollup-Du6uaqyM.js.map +0 -1
- package/dist/chunks/service-time-entry-BGj39_EC.js +0 -227
- package/dist/chunks/service-time-entry-BGj39_EC.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -15,6 +15,26 @@ Attendance services accept trusted session actors, serialize mutations per
|
|
|
15
15
|
actor, and bind every write to one executor. The new tables use non-null unique
|
|
16
16
|
open slots, not nullable unique keys. A linked entry is draft evidence, and
|
|
17
17
|
rollups must deduplicate `AttendancePunch.serviceTimeEntryId`.
|
|
18
|
+
`AttendanceSegment` (`attendance_segments`) is a running work interval inside a
|
|
19
|
+
punch: queryable `workRef*`/`taskRef*`/`kind` columns, one open per punch
|
|
20
|
+
(`openSlot`), closed by `startBreak`/`punchOut`/`autoClose`, never overlapping
|
|
21
|
+
breaks or each other, immutable once closed. It creates no `ServiceTimeEntry`.
|
|
22
|
+
Closed punches, breaks and segments change only through
|
|
23
|
+
`AttendanceCorrectionService` (`attendance/correction-service.ts`), which sets a
|
|
24
|
+
per-instance capability (`attendance/write.ts`, not exported) that lifts the
|
|
25
|
+
models' closed-interval checks for one save, validates the whole punch layout,
|
|
26
|
+
recomputes durations and appends an `AttendanceCorrection` (append-only ledger,
|
|
27
|
+
no generated operations; only a first acknowledgement by the subject may update
|
|
28
|
+
a row). Never export `write.ts`. Punch `source`/`deviceRef*` and break `kind` are immutable except through corrections; `autoCloseReason` is write-once via `persistAutoClose`. A confirmed `PeriodTimecard` is honoured by the
|
|
29
|
+
opt-in `confirmedTimecardGuard` (`rollup/correction-guard.ts`), which refuses
|
|
30
|
+
while a card is submitted or confirmed. `PeriodRollupService.reopen()` returns
|
|
31
|
+
such a card to `open` and appends a `TimecardReopening` (`timecard_reopenings`,
|
|
32
|
+
append-only, service-only via `rollup/write.ts`); `submit()` is the optional
|
|
33
|
+
open -> submitted step (`requireSubmission` makes it mandatory) (#3950).
|
|
34
|
+
Segment transitions live in `attendance/segments.ts`; compose them with other
|
|
35
|
+
attendance writes via `AttendanceService.withTransaction`. Any new attendance
|
|
36
|
+
table must be added to the `includeObjects` lists in the `*.optional.test.ts`
|
|
37
|
+
files.
|
|
18
38
|
|
|
19
39
|
## Models
|
|
20
40
|
|
|
@@ -28,6 +48,40 @@ rollups must deduplicate `AttendancePunch.serviceTimeEntryId`.
|
|
|
28
48
|
through a `ServiceCommercialResolver`), and corrects. `smrt-projects` supplies
|
|
29
49
|
`SubscriptionServiceCommercialResolver` for #1925 pricing.
|
|
30
50
|
|
|
51
|
+
## Generated write actions (#4001)
|
|
52
|
+
|
|
53
|
+
Generated CRUD writes stay closed. `ServiceTimeEntryCollection` hosts the only
|
|
54
|
+
generated writes, `record` / `submit` / `approve` (`POST
|
|
55
|
+
/servicetimeentries/<action>`, body `{ entryId }` for submit/approve), on REST
|
|
56
|
+
only; MCP and CLI stay read-only until per-operation MCP authorization (#4004).
|
|
57
|
+
`services/time-entry-actions.ts` delegates each to `ServiceEvidenceService`, so
|
|
58
|
+
the trail and snapshots are set in one place. `services/action-principal.ts`
|
|
59
|
+
takes identity from the tenancy ALS, then the smrt-users session ALS, never the
|
|
60
|
+
body; authorizes `servicetimeentries.<action>` with `assertOperationPermission`
|
|
61
|
+
(system context never bypasses; `approve` refuses super-admin bypass); and
|
|
62
|
+
stamps the user's linked profile. Only the participant records/submits their
|
|
63
|
+
own time unless the caller holds `approve`. `approve` prices with the host's
|
|
64
|
+
`setServiceTimeEntryCommercialResolver()` — a factory called per request with
|
|
65
|
+
the collection's options, so a writing resolver shares the request executor;
|
|
66
|
+
without one it is a 409 and writes nothing. The runtime REST transport gates
|
|
67
|
+
a collection-hosted action on the ITEM's `api.include`, SvelteKit on the
|
|
68
|
+
collection's, so both list the actions;
|
|
69
|
+
a consumer closing them overrides both registrations. The collection and both
|
|
70
|
+
snapshot collections carry their own `@smrt()` surface: undecorated, they
|
|
71
|
+
published generated create/update/delete MCP tools. `reject` waits for #3994.
|
|
72
|
+
|
|
73
|
+
## Recipe
|
|
74
|
+
|
|
75
|
+
`src/recipes.ts` declares one `SmrtRecipe`, `timesheets.time-entries`
|
|
76
|
+
(label "Time entries", section `work`), emitted into the built
|
|
77
|
+
`manifest.json` and `smrt-knowledge.json` `recipes`. It shows
|
|
78
|
+
`ServiceTimeEntry` alone, with the generic work reference, evidence, agent
|
|
79
|
+
and approval-trail fields hidden; attendance and rollup models stay out of
|
|
80
|
+
it. `status` and its trail are hidden and locked, and the help never says to
|
|
81
|
+
set them: `save()` accepts a bare transition, so submit and approve must go
|
|
82
|
+
through `ServiceEvidenceService` (the recipe declares no surface for that). Help lives in `src/time-entries.recipe.md` (#3591); keep it and the recipe
|
|
83
|
+
in plain language. See [core recipes](../core/agents/recipes.md).
|
|
84
|
+
|
|
31
85
|
## Invariants
|
|
32
86
|
|
|
33
87
|
- **No domain foreign key on the base.** Work context is the generic
|
|
@@ -122,3 +176,9 @@ Layer 3 lives under `src/rollup/` and the `./rollup` subpath. See
|
|
|
122
176
|
confirmation, attributed adjustments, isolation, and transaction contracts.
|
|
123
177
|
Timecards and adjustments have closed generated surfaces and service-only
|
|
124
178
|
model writes; never bypass the instance capability with generic persistence.
|
|
179
|
+
|
|
180
|
+
Replay hashes the tap AS SENT (action, device `at`, paid, work, and `source`/`device`/`kind` only when present, so older receipts still match) so retries match;
|
|
181
|
+
`replay(taps, { resolveAt, now, refusal })` only changes the stored time and
|
|
182
|
+
reason. Never hash the resolved time. Work actions (`startWork`, `switchWork`,
|
|
183
|
+
`stopWork`, `resumeWork`) run in the tap's transaction via `apply()`. Refusals store
|
|
184
|
+
`code` from `AttendanceConflict.code`; give new conflicts a code.
|
package/README.md
CHANGED
|
@@ -131,10 +131,41 @@ recording facade remains seconds-based; its shared entry view uses
|
|
|
131
131
|
`TimeEntryApprovalQueue` are props-driven: hosts load entries and pass plain
|
|
132
132
|
view objects (`TimeEntry`, `TimeEntryApprovalView`).
|
|
133
133
|
|
|
134
|
+
## Generated write actions
|
|
135
|
+
|
|
136
|
+
Generated CRUD writes are closed. The write path a generated app can call is
|
|
137
|
+
three service-backed REST actions on `ServiceTimeEntryCollection` (#4001):
|
|
138
|
+
|
|
139
|
+
| Route | Body | Permission |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `POST /servicetimeentries/record` | work ref, `source` (`manual`/`timer`), times or `durationSeconds`/`durationHours`, optional `participantProfileId` | `servicetimeentries.record` |
|
|
142
|
+
| `POST /servicetimeentries/submit` | `{ entryId }` | `servicetimeentries.submit` |
|
|
143
|
+
| `POST /servicetimeentries/approve` | `{ entryId }` | `servicetimeentries.approve` (no super-admin bypass) |
|
|
144
|
+
|
|
145
|
+
Each delegates to `ServiceEvidenceService`, so `submittedAt`/`submittedBy`,
|
|
146
|
+
`approvedAt`/`approvedBy`/`approvalPath` (`operator`) and both snapshots are
|
|
147
|
+
set exactly as the service sets them. The tenant and the acting profile come
|
|
148
|
+
from the authenticated session, never the body. Recording or submitting
|
|
149
|
+
someone else's time needs `approve` as well. Install pricing for `approve`
|
|
150
|
+
as a factory, so a resolver that writes (client charges) runs on the request's
|
|
151
|
+
executor and transaction:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { setServiceTimeEntryCommercialResolver } from '@happyvertical/smrt-timesheets';
|
|
155
|
+
|
|
156
|
+
setServiceTimeEntryCommercialResolver(({ options }) =>
|
|
157
|
+
SubscriptionServiceCommercialResolver.create(options, provider),
|
|
158
|
+
);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Without a resolver, `approve` answers 409 `approval_not_configured`. MCP and
|
|
162
|
+
CLI stay read-only; there is no `reject` action yet (#3994).
|
|
163
|
+
|
|
134
164
|
## Closing the generated surface
|
|
135
165
|
|
|
136
166
|
The entry and both snapshots expose generated `list` / `get` on REST, CLI,
|
|
137
|
-
and MCP, with optional tenant scoping
|
|
167
|
+
and MCP, with optional tenant scoping, and the entry's collection the write
|
|
168
|
+
actions above. An application that routes all time
|
|
138
169
|
and money through its own permission-gated services can close those surfaces
|
|
139
170
|
without declaring replacement models:
|
|
140
171
|
|
|
@@ -144,6 +175,8 @@ import '@happyvertical/smrt-timesheets';
|
|
|
144
175
|
|
|
145
176
|
for (const name of [
|
|
146
177
|
'ServiceTimeEntry',
|
|
178
|
+
// Hosts the record/submit/approve actions on generated SvelteKit routes.
|
|
179
|
+
'ServiceTimeEntryCollection',
|
|
147
180
|
'ServiceChargeSnapshot',
|
|
148
181
|
'ServiceCompensationSnapshot',
|
|
149
182
|
]) {
|
|
@@ -269,7 +302,7 @@ and are unchanged.
|
|
|
269
302
|
## Attendance
|
|
270
303
|
|
|
271
304
|
The `@happyvertical/smrt-timesheets/attendance` subpath adds `AttendancePunch`,
|
|
272
|
-
`AttendanceBreak`, and `AttendanceService`. Apply migrations for the new tables
|
|
305
|
+
`AttendanceBreak`, `AttendanceSegment`, and `AttendanceService`. Apply migrations for the new tables
|
|
273
306
|
before enabling the module. Existing entry and snapshot tables are unchanged.
|
|
274
307
|
Attendance supports SQLite and PostgreSQL. DuckDB cannot enforce the required
|
|
275
308
|
`ON UPDATE CASCADE` foreign-key contract used by attendance and existing entry
|
|
@@ -288,7 +321,7 @@ reads without replacing the models or changing their schema:
|
|
|
288
321
|
import { ObjectRegistry } from '@happyvertical/smrt-core';
|
|
289
322
|
import '@happyvertical/smrt-timesheets/attendance';
|
|
290
323
|
|
|
291
|
-
for (const model of ['AttendancePunch', 'AttendanceBreak']) {
|
|
324
|
+
for (const model of ['AttendancePunch', 'AttendanceBreak', 'AttendanceSegment']) {
|
|
292
325
|
ObjectRegistry.registerOverride(`@happyvertical/smrt-timesheets:${model}`, {
|
|
293
326
|
api: false, cli: false, mcp: false,
|
|
294
327
|
});
|
|
@@ -321,6 +354,49 @@ pattern per punch. Persisted ownership/start and closed intervals are immutable.
|
|
|
321
354
|
Direct ORM saves serialize against the parent punch and reject a close while a
|
|
322
355
|
break remains open or ends outside the punch.
|
|
323
356
|
|
|
357
|
+
### Running work segments
|
|
358
|
+
|
|
359
|
+
A shift is one punch; inside it a person works on a sequence of things. An
|
|
360
|
+
`AttendanceSegment` (table `attendance_segments`) is one running interval of work
|
|
361
|
+
inside a punch, with queryable columns `workRefType`/`workRefId`, an optional
|
|
362
|
+
second reference `taskRefType`/`taskRefId` (for example an operation; both or
|
|
363
|
+
neither), a short `kind` lane label, `description`, `startedAt`/`endedAt`,
|
|
364
|
+
`durationSeconds` and `closedReason` (`stop`, `switch`, `break`, `punchOut`).
|
|
365
|
+
Indexes on `(tenantId, workRefType, workRefId)` and `(tenantId, taskRefType,
|
|
366
|
+
taskRefId)` serve job costing; a unique `(tenantId, punchId, openSlot)` index
|
|
367
|
+
allows one open segment per punch. Segments never overlap each other or a break.
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
await attendance.punchIn(t(8));
|
|
371
|
+
await attendance.startWork(t(8), { workRefType: '@acme:WorkOrder', workRefId: 'WO-1',
|
|
372
|
+
taskRefType: '@acme:Operation', taskRefId: 'SHEAR', description: 'Shear' });
|
|
373
|
+
await attendance.switchWork(t(10), { workRefType: '@acme:WorkOrder', workRefId: 'WO-2',
|
|
374
|
+
taskRefType: '@acme:Operation', taskRefId: 'WELD', description: 'Weld' });
|
|
375
|
+
await attendance.startBreak(t(12)); // closes the segment
|
|
376
|
+
await attendance.endBreak(t(13), { resumeWork: true }); // or attendance.resumeWork(t(13))
|
|
377
|
+
await attendance.punchOut(t(16)); // closes the segment
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
`startWork`, `switchWork` (closes the open segment and opens the next at the same
|
|
381
|
+
instant), `stopWork`, `resumeWork` (reopens the work a break interrupted),
|
|
382
|
+
`currentWork()` and `segments(punchId)` are the service surface. `startBreak`,
|
|
383
|
+
`punchOut`, `autoClose` and replayed taps close the open segment. Closed segments
|
|
384
|
+
are immutable; ownership, references and start are immutable always. Closing a
|
|
385
|
+
punch with an open segment, or a segment outside the punch, is refused.
|
|
386
|
+
|
|
387
|
+
Segments are attendance evidence, not service time entries: they do not create
|
|
388
|
+
`ServiceTimeEntry` rows and `punchOut(at, work?)` is unchanged (one draft entry
|
|
389
|
+
for the whole punch; `taskRef*` and `kind` ride in its `metadata`). A consumer that
|
|
390
|
+
costs jobs from segments should not also cost the punch-level entry. Rollups
|
|
391
|
+
still deduplicate `serviceTimeEntryId`.
|
|
392
|
+
|
|
393
|
+
To compose segment work with other attendance mutations (corrections, custom
|
|
394
|
+
replay actions) atomically, use `attendance.withTransaction(async (tx) => ...)`
|
|
395
|
+
and the exported helpers `openSegment`, `listSegments`, `beginSegment`,
|
|
396
|
+
`replaceSegment`, `endSegment`, `continueSegment` and `closeOpenSegment`, which
|
|
397
|
+
take the `AttendanceContext` (`tx.punches`, `tx.breaks`, `tx.segments`, `tx.db`)
|
|
398
|
+
and an open punch.
|
|
399
|
+
|
|
324
400
|
A work reference is optional: attendance alone is not billable evidence. Passing
|
|
325
401
|
one on close, or calling `linkServiceTimeEntry(punch.id, work)` later, creates
|
|
326
402
|
one draft `ServiceTimeEntry` atomically and records `serviceTimeEntryId` on the
|
|
@@ -330,14 +406,30 @@ refused. Approval still uses the entry layer. Rollups must deduplicate this link
|
|
|
330
406
|
not count both attendance and its entry. Zero-net-duration punches cannot create
|
|
331
407
|
service evidence.
|
|
332
408
|
|
|
333
|
-
`autoClose(now,
|
|
334
|
-
|
|
335
|
-
or
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
409
|
+
`autoClose(now, deadline)` closes a due open punch and sets `reviewRequired`.
|
|
410
|
+
`deadline` is either a fixed maximum open duration in seconds (reason
|
|
411
|
+
`maxDuration`) or a per-punch resolver `(punch) => Date | { deadline, reason? }
|
|
412
|
+
| null`, for example the end of the shop day in the shop's time zone or the
|
|
413
|
+
scheduled shift end; `null` leaves the punch open. The open work segment and
|
|
414
|
+
break close at the deadline and the punch records `autoCloseReason` (write-once,
|
|
415
|
+
only through `autoClose`). It returns null before the deadline or when already
|
|
416
|
+
closed, and refuses a resolved deadline before the punch started. If later
|
|
417
|
+
committed attendance would be truncated it refuses the operation for manual
|
|
418
|
+
review. A scheduler calls this per authorized actor; the package does not choose
|
|
419
|
+
overtime, payroll, or shop-time-zone rules.
|
|
420
|
+
|
|
421
|
+
Provenance is optional and consumer-owned (no enum; at most 64 characters, trimmed):
|
|
422
|
+
`punchIn(at, { source, device: { type, id } })` stores `source`, `deviceRefType` and
|
|
423
|
+
`deviceRefId` on the punch (device type and id together, indexed with the tenant), and
|
|
424
|
+
`startBreak(at, paid, { kind })` stores `kind` on the break. They are immutable once
|
|
425
|
+
saved except through `AttendanceCorrectionService` (`addPunch({ source, device })`,
|
|
426
|
+
`saveBreak({ kind })`). All columns default to `''`, so existing rows and calls are unchanged.
|
|
427
|
+
Provenance, `kind` and `autoCloseReason` are additive columns on `attendance_punches` and `attendance_breaks`:
|
|
428
|
+
migrate them as `TEXT NOT NULL DEFAULT ''` (a NULL left on an older row is read as `''`).
|
|
429
|
+
|
|
430
|
+
`replay(taps, options?)` accepts `{ clientId, action, at, paid?, work?, source?, device?, kind? }`, where `action` is
|
|
431
|
+
`punchIn`, `punchOut`, `startBreak`, `endBreak`, or a work action (`startWork`,
|
|
432
|
+
`switchWork` (both require `work`), `stopWork`, `resumeWork`). It sorts each batch by device
|
|
341
433
|
time, then client id. Client ids are durable and scoped to tenant/profile.
|
|
342
434
|
Successful taps set `recordedOffline` and `reviewRequired`. The conflict policy
|
|
343
435
|
is **committed attendance wins**: an event predating committed activity, or
|
|
@@ -349,10 +441,96 @@ are treated as supplied evidence, not server authentication; consumers should
|
|
|
349
441
|
apply device drift policy before replay. Replay receipts must be retained for as
|
|
350
442
|
long as clients can retry their ids.
|
|
351
443
|
|
|
444
|
+
Work actions replay under the same client id and in the same transaction as punch
|
|
445
|
+
and break changes; the outcome carries `segmentId`. Replay options:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
await attendance.replay(taps, {
|
|
449
|
+
// Choose each tap's effective time. Called once per tap without a receipt.
|
|
450
|
+
resolveAt: ({ tap, deviceAt, serverNow, waitedMs, actor }) =>
|
|
451
|
+
waitedMs <= 12 * 3600_000
|
|
452
|
+
? { at: deviceAt, reviewRequired: false }
|
|
453
|
+
: { at: serverNow, reviewRequired: true },
|
|
454
|
+
// Optionally name the stored reason of a refused tap.
|
|
455
|
+
refusal: (tap, error) => ({ code: 'device_busy', message: 'Finish current task first' }),
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
The receipt hashes the tap as sent, so a retry with the same `clientId` still
|
|
460
|
+
matches even though the stored time differs; the outcome then has `resolvedAt`.
|
|
461
|
+
Punches are always flagged `reviewRequired`; `reviewRequired: true` from the
|
|
462
|
+
resolver also flags the resulting work segment (`AttendanceSegment.reviewRequired`,
|
|
463
|
+
a new column). A refused outcome stores `{ error, code }`: `code` defaults to the
|
|
464
|
+
`AttendanceConflict.code` (`predates_committed`, `punch_open`, `no_open_punch`,
|
|
465
|
+
`break_open`, `no_open_break`, `during_break`, `work_open`, `no_open_work`,
|
|
466
|
+
`no_resumable_work`, or `conflict`), so a device screen can say why. Resolved times
|
|
467
|
+
that break chronology are refused like any other tap.
|
|
468
|
+
|
|
469
|
+
### Correcting closed attendance
|
|
470
|
+
|
|
471
|
+
Closed punches, breaks and segments are immutable to every direct write.
|
|
472
|
+
`AttendanceCorrectionService` is the one sanctioned path around that, and it
|
|
473
|
+
leaves a paper trail. Construct it with the **correcting** person as `actor`;
|
|
474
|
+
each request names the `subjectProfileId` whose time it is. As with
|
|
475
|
+
`AttendanceService`, your application authorizes the actor (and decides whether
|
|
476
|
+
they may correct this subject) first.
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
import { AttendanceCorrectionService } from '@happyvertical/smrt-timesheets/attendance';
|
|
480
|
+
import { confirmedTimecardGuard } from '@happyvertical/smrt-timesheets/rollup';
|
|
481
|
+
|
|
482
|
+
const fix = new AttendanceCorrectionService(
|
|
483
|
+
db,
|
|
484
|
+
{ tenantId, profileId: foremanId },
|
|
485
|
+
{ guard: confirmedTimecardGuard }, // optional, see below
|
|
486
|
+
);
|
|
487
|
+
const base = { subjectProfileId: welderId, reason: 'forgot to clock out' };
|
|
488
|
+
|
|
489
|
+
await fix.correctPunch({ ...base, punchId, endedAt: new Date('2026-10-01T15:30:00Z'),
|
|
490
|
+
acknowledgementRequired: true });
|
|
491
|
+
await fix.addPunch({ ...base, reason: 'missed punch', startedAt, endedAt });
|
|
492
|
+
await fix.saveBreak({ ...base, punchId, startedAt, endedAt, paid: false }); // add
|
|
493
|
+
await fix.saveBreak({ ...base, punchId, breakId, endedAt }); // change
|
|
494
|
+
await fix.removeBreak({ ...base, breakId });
|
|
495
|
+
await fix.saveSegment({ ...base, punchId, startedAt, endedAt, work }); // add
|
|
496
|
+
await fix.saveSegment({ ...base, punchId, segmentId, endedAt }); // change
|
|
497
|
+
await fix.removeSegment({ ...base, segmentId });
|
|
498
|
+
|
|
499
|
+
// the welder, constructing the service as themselves:
|
|
500
|
+
await new AttendanceCorrectionService(db, { tenantId, profileId: welderId })
|
|
501
|
+
.acknowledge(correctionId);
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Every method runs in one transaction serialized with the subject's own taps. It
|
|
505
|
+
validates the resulting layout (everything inside its punch, no overlapping
|
|
506
|
+
breaks, segments or other punches, no segment across a break), recomputes
|
|
507
|
+
`durationSeconds` and `unpaidBreakSeconds`, and appends an `AttendanceCorrection`
|
|
508
|
+
row (`attendance_corrections`: `kind`, `subjectProfileId`, `actorProfileId`,
|
|
509
|
+
`punchId`, `targetId`, `beforeState`/`afterState` JSON, a required non-blank
|
|
510
|
+
`reason`, `acknowledgementRequired`, `acknowledgedAt`/`acknowledgedByProfileId`).
|
|
511
|
+
Rows are append-only: the only later change is the subject's single
|
|
512
|
+
`acknowledge()`, and `listForPunch(punchId)` / `pendingAcknowledgements()` read
|
|
513
|
+
the ledger. The ledger has no generated operations, and every ledger,
|
|
514
|
+
closed-break and closed-segment write (insert, acknowledge, delete) is
|
|
515
|
+
service-only: a generic `save()`, `delete()` or collection `create()` is refused. Notes:
|
|
516
|
+
|
|
517
|
+
- Only **closed** punches are corrected; an open one is `AttendanceService`'s.
|
|
518
|
+
Moving a punch edge past a break or segment is refused: correct those first.
|
|
519
|
+
- `correctPunch` clears `reviewRequired` (a person reviewed it). A linked
|
|
520
|
+
*draft* `ServiceTimeEntry` is kept in step; a submitted or approved one
|
|
521
|
+
refuses the correction until it is corrected with `ServiceEvidenceService`.
|
|
522
|
+
- **Confirmed timecards.** A confirmed `PeriodTimecard` never changes, so
|
|
523
|
+
correcting its sources would leave card and attendance disagreeing for good.
|
|
524
|
+
Pass `confirmedTimecardGuard` (from `./rollup`) and a correction overlapping a
|
|
525
|
+
confirmed (or submitted) card is refused before any write; call
|
|
526
|
+
`PeriodRollupService.reopen()` first, correct, then `rollup()` again. Any other lock can be supplied as `guard`.
|
|
527
|
+
- Closed `AttendanceBreak`/`AttendanceSegment` rows can no longer be deleted
|
|
528
|
+
directly; `removeBreak`/`removeSegment` are the path.
|
|
529
|
+
|
|
352
530
|
## Period timecards
|
|
353
531
|
|
|
354
532
|
`./rollup` provides per-person integer-second regular/overtime rollups, immutable
|
|
355
|
-
confirmation, and attributed append-only adjustments. Consumer resolvers own
|
|
533
|
+
confirmation (with an optional submitted step), audited reopening, and attributed append-only adjustments. Consumer resolvers own
|
|
356
534
|
calendar/timezone, holiday and overtime rules. See the [contract and example](agents/period-rollup.md).
|
|
357
535
|
|
|
358
536
|
## Effective-dated labour pricing
|
package/agents/period-rollup.md
CHANGED
|
@@ -133,3 +133,23 @@ embedded writer serialization or a PostgreSQL tenant/person advisory lock.
|
|
|
133
133
|
Outputs are reloaded on the root executor after commit. Rollup reads are an
|
|
134
134
|
application snapshot: subsequent source approvals/corrections appear on the
|
|
135
135
|
next open rollup, and confirmed cards require explicit adjustments.
|
|
136
|
+
|
|
137
|
+
## Lifecycle: submit, confirm, reopen (#3950)
|
|
138
|
+
|
|
139
|
+
`open` (draft) -> `submitted` (optional) -> `confirmed`. `submit(id)` freezes an
|
|
140
|
+
open card for review; `confirm(id)` accepts an open card too unless the service
|
|
141
|
+
is built with `{ requireSubmission: true }`. Submitted and confirmed cards are
|
|
142
|
+
not re-rolled.
|
|
143
|
+
|
|
144
|
+
`reopen(id, { operationId, reason })` returns a submitted or confirmed card to
|
|
145
|
+
`open` (clearing its submission/confirmation attribution) and appends a
|
|
146
|
+
`TimecardReopening`: actor, reason, prior status and a JSON snapshot of the
|
|
147
|
+
previous base, adjustment and effective seconds (`getPrevious()`). It is
|
|
148
|
+
idempotent per `operationId` and refuses a blank reason. `reopenings(id)` lists
|
|
149
|
+
the history. The consumer authorizes the actor; give reopening its own
|
|
150
|
+
permission. Existing adjustments stay and keep counting in `totals()`: after a
|
|
151
|
+
re-roll, offset ones the correction made redundant with a new adjustment.
|
|
152
|
+
`confirmedTimecardGuard` refuses corrections while a card is submitted or
|
|
153
|
+
confirmed and allows them once it is reopened. The new `timecard_reopenings`
|
|
154
|
+
table, and nullable `submitted_at`/`submitted_by_profile_id` columns on
|
|
155
|
+
`period_timecards`, need a migration.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import type { AttendanceBreakCollection, AttendancePunchCollection, AttendanceReplayCollection, AttendanceSegmentCollection } from './models.js';
|
|
3
|
+
/** Resolve these identifiers from the authenticated session, never from tap input. */
|
|
4
|
+
export interface AttendanceActor {
|
|
5
|
+
tenantId: string;
|
|
6
|
+
profileId: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Explicit work attribution for a closed punch's draft evidence entry or for a
|
|
10
|
+
* running work segment. `taskRefType`/`taskRefId` (both or neither) name an
|
|
11
|
+
* optional second reference such as an operation; `kind` is an optional short
|
|
12
|
+
* consumer lane label.
|
|
13
|
+
*/
|
|
14
|
+
export interface AttendanceWork {
|
|
15
|
+
workRefType: string;
|
|
16
|
+
workRefId: string;
|
|
17
|
+
description: string;
|
|
18
|
+
taskRefType?: string;
|
|
19
|
+
taskRefId?: string;
|
|
20
|
+
kind?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Where a punch was captured: a consumer-owned type (for example a class name
|
|
24
|
+
* such as a station or tablet) and that device's id. Both or neither.
|
|
25
|
+
*/
|
|
26
|
+
export interface AttendanceDeviceRef {
|
|
27
|
+
type: string;
|
|
28
|
+
id: string;
|
|
29
|
+
}
|
|
30
|
+
/** Optional provenance recorded when a punch opens. */
|
|
31
|
+
export interface AttendancePunchOptions {
|
|
32
|
+
/** Short consumer-owned label such as `device`, `web`, `import`, `correction`. */
|
|
33
|
+
source?: string;
|
|
34
|
+
/** The device that captured the punch. */
|
|
35
|
+
device?: AttendanceDeviceRef;
|
|
36
|
+
}
|
|
37
|
+
/** Optional detail recorded when a break starts. */
|
|
38
|
+
export interface AttendanceBreakOptions {
|
|
39
|
+
/** Short consumer-owned label such as `meal` or `rest`. */
|
|
40
|
+
kind?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Validate an optional short label (`source`, `kind`, device type); returns the stored form. */
|
|
43
|
+
export declare function attendanceLabel(value: unknown, name: string): string;
|
|
44
|
+
/** Validate punch provenance; returns the stored column values. */
|
|
45
|
+
export declare function attendanceProvenance(options?: AttendancePunchOptions): {
|
|
46
|
+
source: string;
|
|
47
|
+
deviceRefType: string;
|
|
48
|
+
deviceRefId: string;
|
|
49
|
+
};
|
|
50
|
+
/** Expected state conflict, distinct from retryable database failures. */
|
|
51
|
+
export declare class AttendanceConflict extends Error {
|
|
52
|
+
/**
|
|
53
|
+
* Stable machine-readable reason (see {@link AttendanceRefusalCode}); stored
|
|
54
|
+
* on a refused replay outcome so a device screen can say why.
|
|
55
|
+
*/
|
|
56
|
+
readonly code: AttendanceRefusalCode;
|
|
57
|
+
constructor(message: string, code?: AttendanceRefusalCode);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Built-in refusal codes. Consumers may store any other string through the
|
|
61
|
+
* `refusalCode` replay hook.
|
|
62
|
+
*/
|
|
63
|
+
export type AttendanceRefusalCode = 'conflict' | 'invalid' | 'predates_committed' | 'punch_open' | 'no_open_punch' | 'break_open' | 'no_open_break' | 'during_break' | 'work_open' | 'no_open_work' | 'no_resumable_work' | (string & {});
|
|
64
|
+
/**
|
|
65
|
+
* Collections bound to one transaction executor. Handed to
|
|
66
|
+
* `AttendanceService.withTransaction` callbacks and to the segment helpers so
|
|
67
|
+
* corrections and replay can compose several mutations atomically.
|
|
68
|
+
*/
|
|
69
|
+
export type AttendanceContext = {
|
|
70
|
+
db: DatabaseInterface;
|
|
71
|
+
punches: AttendancePunchCollection;
|
|
72
|
+
breaks: AttendanceBreakCollection;
|
|
73
|
+
segments: AttendanceSegmentCollection;
|
|
74
|
+
replays: AttendanceReplayCollection;
|
|
75
|
+
};
|
|
76
|
+
/** Validate a work reference; throws {@link AttendanceConflict}. */
|
|
77
|
+
export declare function validateAttendanceWork(work: AttendanceWork): void;
|
|
78
|
+
/** Canonicalize a tenant/profile id: PostgreSQL requires UUIDs, other adapters take it verbatim. */
|
|
79
|
+
export declare function attendanceIdentity(db: DatabaseInterface, value: string): string;
|
|
80
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/attendance/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EACzB,0BAA0B,EAC1B,2BAA2B,EAC5B,MAAM,aAAa,CAAC;AAErB,sFAAsF;AACtF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AACD;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,uDAAuD;AACvD,MAAM,WAAW,sBAAsB;IACrC,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B;AAED,oDAAoD;AACpD,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAID,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAYpE;AAED,mEAAmE;AACnE,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,CAaA;AAED,0EAA0E;AAC1E,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;gBACzB,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,qBAAkC;CAItE;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAC7B,UAAU,GACV,SAAS,GACT,oBAAoB,GACpB,YAAY,GACZ,eAAe,GACf,YAAY,GACZ,eAAe,GACf,cAAc,GACd,WAAW,GACX,cAAc,GACd,mBAAmB,GACnB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,iBAAiB,CAAC;IACtB,OAAO,EAAE,yBAAyB,CAAC;IACnC,MAAM,EAAE,yBAAyB,CAAC;IAClC,QAAQ,EAAE,2BAA2B,CAAC;IACtC,OAAO,EAAE,0BAA0B,CAAC;CACrC,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAmBjE;AAED,oGAAoG;AACpG,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,MAAM,GACZ,MAAM,CAMR"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SmrtCollection, SmrtObject } from '@happyvertical/smrt-core';
|
|
2
|
+
/** What a correction did. Punch kinds target the punch itself. */
|
|
3
|
+
export declare const ATTENDANCE_CORRECTION_KINDS: readonly ["punch.corrected", "punch.added", "break.added", "break.changed", "break.removed", "segment.added", "segment.changed", "segment.removed"];
|
|
4
|
+
export type AttendanceCorrectionKind = (typeof ATTENDANCE_CORRECTION_KINDS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* One append-only change to closed attendance: who changed whose time, why, and
|
|
7
|
+
* the before/after values as JSON (`null` for an addition's before or a
|
|
8
|
+
* removal's after). Written only by `AttendanceCorrectionService`, in the same
|
|
9
|
+
* transaction as the change. After insert only the acknowledgement may be set,
|
|
10
|
+
* once, by the subject; rows are never deleted.
|
|
11
|
+
*/
|
|
12
|
+
export declare class AttendanceCorrection extends SmrtObject {
|
|
13
|
+
tenantId: string;
|
|
14
|
+
/** Whose time was corrected. */
|
|
15
|
+
subjectProfileId: string;
|
|
16
|
+
/** Who made the correction. */
|
|
17
|
+
actorProfileId: string;
|
|
18
|
+
kind: AttendanceCorrectionKind;
|
|
19
|
+
punchId: string;
|
|
20
|
+
/** The break or segment id for those kinds; the punch id for punch kinds. */
|
|
21
|
+
targetId: string;
|
|
22
|
+
beforeState: string;
|
|
23
|
+
afterState: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
acknowledgementRequired: boolean;
|
|
26
|
+
acknowledgedAt: Date | null;
|
|
27
|
+
acknowledgedByProfileId: string | null;
|
|
28
|
+
/** The values before the change, or null for an addition. */
|
|
29
|
+
getBefore<T = Record<string, unknown>>(): T | null;
|
|
30
|
+
/** The values after the change, or null for a removal. */
|
|
31
|
+
getAfter<T = Record<string, unknown>>(): T | null;
|
|
32
|
+
save(): Promise<this>;
|
|
33
|
+
delete(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/** The correction ledger. */
|
|
36
|
+
export declare class AttendanceCorrectionCollection extends SmrtCollection<AttendanceCorrection> {
|
|
37
|
+
static readonly _itemClass: typeof AttendanceCorrection;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=correction-models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correction-models.d.ts","sourceRoot":"","sources":["../../src/attendance/correction-models.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,cAAc,EACd,UAAU,EAEX,MAAM,0BAA0B,CAAC;AAMlC,kEAAkE;AAClE,eAAO,MAAM,2BAA2B,qJAS9B,CAAC;AACX,MAAM,MAAM,wBAAwB,GAClC,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAc/C;;;;;;GAMG;AACH,qBAkBa,oBAAqB,SAAQ,UAAU;IACtC,QAAQ,SAAM;IAC1B,gCAAgC;IAEhC,gBAAgB,SAAM;IACtB,+BAA+B;IAE/B,cAAc,SAAM;IACK,IAAI,EAAE,wBAAwB,CAAqB;IAC/C,OAAO,SAAM;IAC1C,6EAA6E;IAC7E,QAAQ,SAAM;IACW,WAAW,SAAU;IACrB,UAAU,SAAU;IACpB,MAAM,SAAM;IACrC,uBAAuB,UAAS;IAChC,cAAc,EAAE,IAAI,GAAG,IAAI,CAAQ;IAEnC,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE9C,6DAA6D;IAC7D,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI;IAGlD,0DAA0D;IAC1D,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI;IAIlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAsDrB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAGvC;AAED,6BAA6B;AAC7B,qBAAa,8BAA+B,SAAQ,cAAc,CAAC,oBAAoB,CAAC;IACtF,MAAM,CAAC,QAAQ,CAAC,UAAU,8BAAwB;CACnD"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import { type AttendanceActor, type AttendanceContext, type AttendancePunchOptions, type AttendanceWork } from './context.js';
|
|
3
|
+
import { AttendanceCorrection } from './correction-models.js';
|
|
4
|
+
/** A closed time range a correction touches, for period-lock checks. */
|
|
5
|
+
export interface AttendanceCorrectionInterval {
|
|
6
|
+
startsAt: Date;
|
|
7
|
+
endsAt: Date;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Optional period lock, run on the correction's transaction before any write.
|
|
11
|
+
* Return a message to refuse the correction (for example "overlaps a confirmed
|
|
12
|
+
* timecard"), or null to allow it. `confirmedTimecardGuard` in `./rollup` is the
|
|
13
|
+
* ready-made one.
|
|
14
|
+
*/
|
|
15
|
+
export type AttendanceCorrectionGuard = (tx: AttendanceContext, subject: AttendanceActor, intervals: readonly AttendanceCorrectionInterval[]) => Promise<string | null>;
|
|
16
|
+
export interface AttendanceCorrectionOptions {
|
|
17
|
+
guard?: AttendanceCorrectionGuard;
|
|
18
|
+
}
|
|
19
|
+
/** Fields every correction carries. */
|
|
20
|
+
export interface AttendanceCorrectionRequest {
|
|
21
|
+
/** Whose time is being corrected. */
|
|
22
|
+
subjectProfileId: string;
|
|
23
|
+
/** Required, non-blank. Stored verbatim (trimmed) on the ledger row. */
|
|
24
|
+
reason: string;
|
|
25
|
+
/** When true the subject must `acknowledge()` the correction. */
|
|
26
|
+
acknowledgementRequired?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface CorrectPunchInput extends AttendanceCorrectionRequest {
|
|
29
|
+
punchId: string;
|
|
30
|
+
startedAt?: Date;
|
|
31
|
+
endedAt?: Date;
|
|
32
|
+
}
|
|
33
|
+
export interface AddPunchInput extends AttendanceCorrectionRequest, AttendancePunchOptions {
|
|
34
|
+
startedAt: Date;
|
|
35
|
+
endedAt: Date;
|
|
36
|
+
}
|
|
37
|
+
/** Adds a break when `breakId` is absent; otherwise changes that break. */
|
|
38
|
+
export interface SaveBreakInput extends AttendanceCorrectionRequest {
|
|
39
|
+
punchId: string;
|
|
40
|
+
breakId?: string;
|
|
41
|
+
startedAt?: Date;
|
|
42
|
+
endedAt?: Date;
|
|
43
|
+
paid?: boolean;
|
|
44
|
+
/** Short consumer-owned label (for example `meal`); omit to keep, `''` to clear. */
|
|
45
|
+
kind?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface RemoveBreakInput extends AttendanceCorrectionRequest {
|
|
48
|
+
breakId: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Adds a segment when `segmentId` is absent (`startedAt`, `endedAt` and `work`
|
|
52
|
+
* are then required); otherwise changes that segment, keeping what is omitted.
|
|
53
|
+
*/
|
|
54
|
+
export interface SaveSegmentInput extends AttendanceCorrectionRequest {
|
|
55
|
+
punchId: string;
|
|
56
|
+
segmentId?: string;
|
|
57
|
+
startedAt?: Date;
|
|
58
|
+
endedAt?: Date;
|
|
59
|
+
work?: AttendanceWork;
|
|
60
|
+
}
|
|
61
|
+
export interface RemoveSegmentInput extends AttendanceCorrectionRequest {
|
|
62
|
+
segmentId: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Corrections to closed attendance, with a paper trail.
|
|
66
|
+
*
|
|
67
|
+
* `actor` is who makes the change; each request names the `subjectProfileId`
|
|
68
|
+
* whose time it is. The application authorizes the actor (and decides whether
|
|
69
|
+
* they may correct this subject) before constructing the service, exactly as
|
|
70
|
+
* for `AttendanceService`. This service is the only path that may change a
|
|
71
|
+
* closed punch, break or segment: each method validates the resulting layout
|
|
72
|
+
* (everything inside its punch, no overlapping breaks, segments or punches,
|
|
73
|
+
* no segment across a break), recomputes `durationSeconds` and
|
|
74
|
+
* `unpaidBreakSeconds`, keeps a still-draft linked service entry in step, and
|
|
75
|
+
* appends an `AttendanceCorrection` row, all in one transaction serialized with
|
|
76
|
+
* the subject's own taps. Open punches are not corrected here: use
|
|
77
|
+
* `AttendanceService`.
|
|
78
|
+
*/
|
|
79
|
+
export declare class AttendanceCorrectionService {
|
|
80
|
+
private readonly db;
|
|
81
|
+
private readonly options;
|
|
82
|
+
private readonly actor;
|
|
83
|
+
constructor(db: DatabaseInterface, actor: AttendanceActor, options?: AttendanceCorrectionOptions);
|
|
84
|
+
/** Change a closed punch's start and/or end. A reviewed punch no longer needs review. */
|
|
85
|
+
correctPunch(input: CorrectPunchInput): Promise<AttendanceCorrection>;
|
|
86
|
+
/** Record a missed punch retroactively, as an already-closed interval. */
|
|
87
|
+
addPunch(input: AddPunchInput): Promise<AttendanceCorrection>;
|
|
88
|
+
/** Add a break to a closed punch, or change one (times and/or paid). */
|
|
89
|
+
saveBreak(input: SaveBreakInput): Promise<AttendanceCorrection>;
|
|
90
|
+
/** Remove a break from a closed punch. */
|
|
91
|
+
removeBreak(input: RemoveBreakInput): Promise<AttendanceCorrection>;
|
|
92
|
+
/** Add a work segment to a closed punch, or change one (times and/or work attribution). */
|
|
93
|
+
saveSegment(input: SaveSegmentInput): Promise<AttendanceCorrection>;
|
|
94
|
+
/** Remove a work segment from a closed punch. */
|
|
95
|
+
removeSegment(input: RemoveSegmentInput): Promise<AttendanceCorrection>;
|
|
96
|
+
/**
|
|
97
|
+
* The subject confirms they have seen a correction that required it. Only
|
|
98
|
+
* the subject (this service's actor) may; it can be done once.
|
|
99
|
+
*/
|
|
100
|
+
acknowledge(correctionId: string, at?: Date): Promise<AttendanceCorrection>;
|
|
101
|
+
/** Corrections to one punch, oldest first. */
|
|
102
|
+
listForPunch(punchId: string): Promise<AttendanceCorrection[]>;
|
|
103
|
+
/** Corrections of the actor's own time that still await their acknowledgement. */
|
|
104
|
+
pendingAcknowledgements(): Promise<AttendanceCorrection[]>;
|
|
105
|
+
private attendance;
|
|
106
|
+
private read;
|
|
107
|
+
private run;
|
|
108
|
+
private closedPunch;
|
|
109
|
+
private children;
|
|
110
|
+
private layout;
|
|
111
|
+
private attribute;
|
|
112
|
+
private guard;
|
|
113
|
+
/** The punch's still-draft evidence entry, refusing when it is no longer editable. */
|
|
114
|
+
private editableEntry;
|
|
115
|
+
/** Recompute the punch's durations from its breaks and persist it and its draft entry. */
|
|
116
|
+
private settle;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=correction-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correction-service.d.ts","sourceRoot":"","sources":["../../src/attendance/correction-service.ts"],"names":[],"mappings":"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"}
|