@happyvertical/smrt-timesheets 0.55.24 → 0.56.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/AGENTS.md +26 -0
- package/README.md +156 -11
- 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-B1jCbqSb.js} +3 -3
- package/dist/chunks/{attendance-Cpo4mIID.js.map → attendance-B1jCbqSb.js.map} +1 -1
- package/dist/chunks/attendance-DGVy-iHI.js +1246 -0
- package/dist/chunks/attendance-DGVy-iHI.js.map +1 -0
- package/dist/chunks/models-0-YD5F_o.js +372 -0
- package/dist/chunks/models-0-YD5F_o.js.map +1 -0
- package/dist/chunks/{rollup-Du6uaqyM.js → rollup-CDqzCxI2.js} +211 -6
- package/dist/chunks/rollup-CDqzCxI2.js.map +1 -0
- package/dist/chunks/service-time-entry-t7P-bkOQ.js +227 -0
- package/dist/chunks/service-time-entry-t7P-bkOQ.js.map +1 -0
- package/dist/index.js +5 -5
- package/dist/manifest.json +2965 -1358
- 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/smrt-knowledge.json +700 -34
- package/package.json +6 -6
- 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/dist/smrt-knowledge.json
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-timesheets",
|
|
6
|
-
"packageVersion": "0.
|
|
6
|
+
"packageVersion": "0.56.0",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
13
|
-
"moduleDoc:agents/period-rollup.md": "
|
|
10
|
+
"manifest": "b5ad32209145ad0e72befacafda2eef579b565f2fb2052323522f03f0d1ed9b3",
|
|
11
|
+
"packageJson": "dc9177b3dcd6f28aad203d0e7c0be58abfbf863be6b93ff56486db7c62fea31f",
|
|
12
|
+
"agents": "5ea9c547ff79ab402ab8b1942e1760344bb312dbd9148e4435bb32e14dfecaf9",
|
|
13
|
+
"moduleDoc:agents/period-rollup.md": "7e693235a49e8354a8910f480cde8a7aecdc4f28afaa6d778fd4bfaf67119b46"
|
|
14
14
|
},
|
|
15
15
|
"exports": [
|
|
16
16
|
".",
|
|
@@ -57,10 +57,213 @@
|
|
|
57
57
|
"timesheets"
|
|
58
58
|
],
|
|
59
59
|
"risks": [
|
|
60
|
-
"cross-package-refs:
|
|
60
|
+
"cross-package-refs:15",
|
|
61
61
|
"sensitive-fields-excluded"
|
|
62
62
|
],
|
|
63
63
|
"objects": [
|
|
64
|
+
{
|
|
65
|
+
"name": "AttendanceCorrection",
|
|
66
|
+
"qualifiedName": "@happyvertical/smrt-timesheets:AttendanceCorrection",
|
|
67
|
+
"collection": "attendancecorrections",
|
|
68
|
+
"description": "One append-only change to closed attendance: who changed whose time, why, and the before/after values as JSON (null for an addition's before or a removal's after).",
|
|
69
|
+
"tableName": "attendance_corrections",
|
|
70
|
+
"packageName": "@happyvertical/smrt-timesheets",
|
|
71
|
+
"extends": "SmrtObject",
|
|
72
|
+
"fields": [
|
|
73
|
+
{
|
|
74
|
+
"name": "tenantId",
|
|
75
|
+
"type": "text",
|
|
76
|
+
"required": true,
|
|
77
|
+
"columnType": "UUID"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "subjectProfileId",
|
|
81
|
+
"type": "crossPackageRef",
|
|
82
|
+
"required": false,
|
|
83
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
84
|
+
"columnType": "UUID"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "actorProfileId",
|
|
88
|
+
"type": "crossPackageRef",
|
|
89
|
+
"required": false,
|
|
90
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
91
|
+
"columnType": "UUID"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "kind",
|
|
95
|
+
"type": "text",
|
|
96
|
+
"required": false,
|
|
97
|
+
"columnType": "TEXT"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "punchId",
|
|
101
|
+
"type": "foreignKey",
|
|
102
|
+
"required": false,
|
|
103
|
+
"related": "AttendancePunch",
|
|
104
|
+
"columnType": "UUID"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "targetId",
|
|
108
|
+
"type": "text",
|
|
109
|
+
"required": false,
|
|
110
|
+
"columnType": "TEXT"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "beforeState",
|
|
114
|
+
"type": "text",
|
|
115
|
+
"required": false,
|
|
116
|
+
"columnType": "TEXT"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "afterState",
|
|
120
|
+
"type": "text",
|
|
121
|
+
"required": false,
|
|
122
|
+
"columnType": "TEXT"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "reason",
|
|
126
|
+
"type": "text",
|
|
127
|
+
"required": false,
|
|
128
|
+
"columnType": "TEXT"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "acknowledgementRequired",
|
|
132
|
+
"type": "boolean",
|
|
133
|
+
"required": true,
|
|
134
|
+
"columnType": "BOOLEAN",
|
|
135
|
+
"default": false
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "acknowledgedAt",
|
|
139
|
+
"type": "datetime",
|
|
140
|
+
"required": false,
|
|
141
|
+
"columnType": "TIMESTAMP"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "acknowledgedByProfileId",
|
|
145
|
+
"type": "crossPackageRef",
|
|
146
|
+
"required": false,
|
|
147
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
148
|
+
"columnType": "UUID"
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"relationships": [
|
|
152
|
+
{
|
|
153
|
+
"name": "subjectProfileId",
|
|
154
|
+
"type": "crossPackageRef",
|
|
155
|
+
"required": false,
|
|
156
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
157
|
+
"columnType": "UUID"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "actorProfileId",
|
|
161
|
+
"type": "crossPackageRef",
|
|
162
|
+
"required": false,
|
|
163
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
164
|
+
"columnType": "UUID"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "punchId",
|
|
168
|
+
"type": "foreignKey",
|
|
169
|
+
"required": false,
|
|
170
|
+
"related": "AttendancePunch",
|
|
171
|
+
"columnType": "UUID"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "acknowledgedByProfileId",
|
|
175
|
+
"type": "crossPackageRef",
|
|
176
|
+
"required": false,
|
|
177
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
178
|
+
"columnType": "UUID"
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
"methods": [
|
|
182
|
+
"delete",
|
|
183
|
+
"getAfter",
|
|
184
|
+
"getBefore",
|
|
185
|
+
"save"
|
|
186
|
+
],
|
|
187
|
+
"methodSignatures": [
|
|
188
|
+
{
|
|
189
|
+
"name": "delete",
|
|
190
|
+
"async": true,
|
|
191
|
+
"returns": "Promise<void>"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"name": "getAfter",
|
|
195
|
+
"returns": "T | null"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "getBefore",
|
|
199
|
+
"returns": "T | null"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "save",
|
|
203
|
+
"async": true,
|
|
204
|
+
"returns": "Promise<this>"
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"tenant": {
|
|
208
|
+
"scoped": true,
|
|
209
|
+
"mode": "required",
|
|
210
|
+
"field": "tenantId"
|
|
211
|
+
},
|
|
212
|
+
"conflictColumns": [
|
|
213
|
+
"tenant_id",
|
|
214
|
+
"slug",
|
|
215
|
+
"context"
|
|
216
|
+
],
|
|
217
|
+
"surfaces": [],
|
|
218
|
+
"withheldSurfaces": [
|
|
219
|
+
{
|
|
220
|
+
"kind": "api",
|
|
221
|
+
"operation": "getAfter",
|
|
222
|
+
"code": "not-included",
|
|
223
|
+
"reason": "not listed in api.include",
|
|
224
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceCorrection"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"kind": "api",
|
|
228
|
+
"operation": "getBefore",
|
|
229
|
+
"code": "not-included",
|
|
230
|
+
"reason": "not listed in api.include",
|
|
231
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceCorrection"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"kind": "api",
|
|
235
|
+
"operation": "save",
|
|
236
|
+
"code": "lifecycle-method",
|
|
237
|
+
"reason": "`save` is a framework lifecycle method, not a distinct operation",
|
|
238
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceCorrection"
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
"relationshipFeatures": [
|
|
242
|
+
"crossPackageRef",
|
|
243
|
+
"foreignKey",
|
|
244
|
+
"uuidColumns"
|
|
245
|
+
],
|
|
246
|
+
"tags": [],
|
|
247
|
+
"risks": []
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "AttendanceCorrectionCollection",
|
|
251
|
+
"qualifiedName": "@happyvertical/smrt-timesheets:AttendanceCorrectionCollection",
|
|
252
|
+
"collection": "attendancecorrections",
|
|
253
|
+
"description": "The correction ledger.",
|
|
254
|
+
"tableName": "attendance_corrections",
|
|
255
|
+
"packageName": "@happyvertical/smrt-timesheets",
|
|
256
|
+
"extends": "SmrtCollection",
|
|
257
|
+
"fields": [],
|
|
258
|
+
"relationships": [],
|
|
259
|
+
"methods": [],
|
|
260
|
+
"surfaces": [],
|
|
261
|
+
"relationshipFeatures": [
|
|
262
|
+
"uuidColumns"
|
|
263
|
+
],
|
|
264
|
+
"tags": [],
|
|
265
|
+
"risks": []
|
|
266
|
+
},
|
|
64
267
|
{
|
|
65
268
|
"name": "AttendancePunch",
|
|
66
269
|
"qualifiedName": "@happyvertical/smrt-timesheets:AttendancePunch",
|
|
@@ -116,6 +319,12 @@
|
|
|
116
319
|
"columnType": "BOOLEAN",
|
|
117
320
|
"default": false
|
|
118
321
|
},
|
|
322
|
+
{
|
|
323
|
+
"name": "autoCloseReason",
|
|
324
|
+
"type": "text",
|
|
325
|
+
"required": false,
|
|
326
|
+
"columnType": "TEXT"
|
|
327
|
+
},
|
|
119
328
|
{
|
|
120
329
|
"name": "recordedOffline",
|
|
121
330
|
"type": "boolean",
|
|
@@ -123,6 +332,24 @@
|
|
|
123
332
|
"columnType": "BOOLEAN",
|
|
124
333
|
"default": false
|
|
125
334
|
},
|
|
335
|
+
{
|
|
336
|
+
"name": "source",
|
|
337
|
+
"type": "text",
|
|
338
|
+
"required": false,
|
|
339
|
+
"columnType": "TEXT"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"name": "deviceRefType",
|
|
343
|
+
"type": "text",
|
|
344
|
+
"required": false,
|
|
345
|
+
"columnType": "TEXT"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"name": "deviceRefId",
|
|
349
|
+
"type": "text",
|
|
350
|
+
"required": false,
|
|
351
|
+
"columnType": "TEXT"
|
|
352
|
+
},
|
|
126
353
|
{
|
|
127
354
|
"name": "serviceTimeEntryId",
|
|
128
355
|
"type": "foreignKey",
|
|
@@ -242,17 +469,200 @@
|
|
|
242
469
|
"extends": "SmrtObject",
|
|
243
470
|
"fields": [
|
|
244
471
|
{
|
|
245
|
-
"name": "tenantId",
|
|
472
|
+
"name": "tenantId",
|
|
473
|
+
"type": "text",
|
|
474
|
+
"required": true,
|
|
475
|
+
"columnType": "UUID"
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
"name": "punchId",
|
|
479
|
+
"type": "foreignKey",
|
|
480
|
+
"required": false,
|
|
481
|
+
"related": "AttendancePunch",
|
|
482
|
+
"columnType": "UUID"
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
"name": "startedAt",
|
|
486
|
+
"type": "datetime",
|
|
487
|
+
"required": false,
|
|
488
|
+
"columnType": "TIMESTAMP"
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
"name": "endedAt",
|
|
492
|
+
"type": "datetime",
|
|
493
|
+
"required": false,
|
|
494
|
+
"columnType": "TIMESTAMP"
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
"name": "paid",
|
|
498
|
+
"type": "boolean",
|
|
499
|
+
"required": true,
|
|
500
|
+
"columnType": "BOOLEAN",
|
|
501
|
+
"default": false
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
"name": "kind",
|
|
505
|
+
"type": "text",
|
|
506
|
+
"required": false,
|
|
507
|
+
"columnType": "TEXT"
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
"name": "openSlot",
|
|
511
|
+
"type": "text",
|
|
512
|
+
"required": true,
|
|
513
|
+
"columnType": "TEXT"
|
|
514
|
+
}
|
|
515
|
+
],
|
|
516
|
+
"relationships": [
|
|
517
|
+
{
|
|
518
|
+
"name": "punchId",
|
|
519
|
+
"type": "foreignKey",
|
|
520
|
+
"required": false,
|
|
521
|
+
"related": "AttendancePunch",
|
|
522
|
+
"columnType": "UUID"
|
|
523
|
+
}
|
|
524
|
+
],
|
|
525
|
+
"methods": [
|
|
526
|
+
"delete",
|
|
527
|
+
"save"
|
|
528
|
+
],
|
|
529
|
+
"methodSignatures": [
|
|
530
|
+
{
|
|
531
|
+
"name": "delete",
|
|
532
|
+
"async": true,
|
|
533
|
+
"returns": "Promise<void>"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"name": "save",
|
|
537
|
+
"async": true,
|
|
538
|
+
"returns": "Promise<this>"
|
|
539
|
+
}
|
|
540
|
+
],
|
|
541
|
+
"tenant": {
|
|
542
|
+
"scoped": true,
|
|
543
|
+
"mode": "required",
|
|
544
|
+
"field": "tenantId"
|
|
545
|
+
},
|
|
546
|
+
"conflictColumns": [
|
|
547
|
+
"tenant_id",
|
|
548
|
+
"slug",
|
|
549
|
+
"context"
|
|
550
|
+
],
|
|
551
|
+
"surfaces": [
|
|
552
|
+
{
|
|
553
|
+
"kind": "api",
|
|
554
|
+
"name": "attendancebreaks.list",
|
|
555
|
+
"operation": "list",
|
|
556
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceBreak",
|
|
557
|
+
"path": "/attendancebreaks",
|
|
558
|
+
"method": "GET"
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"kind": "api",
|
|
562
|
+
"name": "attendancebreaks.get",
|
|
563
|
+
"operation": "get",
|
|
564
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceBreak",
|
|
565
|
+
"path": "/attendancebreaks/[id]",
|
|
566
|
+
"method": "GET"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
"kind": "cli",
|
|
570
|
+
"name": "attendancebreak_list",
|
|
571
|
+
"operation": "list",
|
|
572
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceBreak"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
"kind": "cli",
|
|
576
|
+
"name": "attendancebreak_get",
|
|
577
|
+
"operation": "get",
|
|
578
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceBreak"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
"kind": "mcp",
|
|
582
|
+
"name": "attendancebreak_list",
|
|
583
|
+
"operation": "list",
|
|
584
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceBreak"
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
"kind": "mcp",
|
|
588
|
+
"name": "attendancebreak_get",
|
|
589
|
+
"operation": "get",
|
|
590
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceBreak"
|
|
591
|
+
}
|
|
592
|
+
],
|
|
593
|
+
"withheldSurfaces": [
|
|
594
|
+
{
|
|
595
|
+
"kind": "api",
|
|
596
|
+
"operation": "save",
|
|
597
|
+
"code": "lifecycle-method",
|
|
598
|
+
"reason": "`save` is a framework lifecycle method, not a distinct operation",
|
|
599
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceBreak"
|
|
600
|
+
}
|
|
601
|
+
],
|
|
602
|
+
"relationshipFeatures": [
|
|
603
|
+
"foreignKey",
|
|
604
|
+
"uuidColumns"
|
|
605
|
+
],
|
|
606
|
+
"tags": [],
|
|
607
|
+
"risks": []
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
"name": "AttendanceSegment",
|
|
611
|
+
"qualifiedName": "@happyvertical/smrt-timesheets:AttendanceSegment",
|
|
612
|
+
"collection": "attendancesegments",
|
|
613
|
+
"description": "A running unit of work inside one attendance punch: a work reference plus an optional second (task) reference as queryable columns, so job costing can index and join them.",
|
|
614
|
+
"tableName": "attendance_segments",
|
|
615
|
+
"packageName": "@happyvertical/smrt-timesheets",
|
|
616
|
+
"extends": "SmrtObject",
|
|
617
|
+
"fields": [
|
|
618
|
+
{
|
|
619
|
+
"name": "tenantId",
|
|
620
|
+
"type": "text",
|
|
621
|
+
"required": true,
|
|
622
|
+
"columnType": "UUID"
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
"name": "punchId",
|
|
626
|
+
"type": "foreignKey",
|
|
627
|
+
"required": false,
|
|
628
|
+
"related": "AttendancePunch",
|
|
629
|
+
"columnType": "UUID"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
"name": "workRefType",
|
|
633
|
+
"type": "text",
|
|
634
|
+
"required": false,
|
|
635
|
+
"columnType": "TEXT"
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
"name": "workRefId",
|
|
639
|
+
"type": "text",
|
|
640
|
+
"required": false,
|
|
641
|
+
"columnType": "TEXT"
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
"name": "taskRefType",
|
|
246
645
|
"type": "text",
|
|
247
|
-
"required":
|
|
248
|
-
"columnType": "
|
|
646
|
+
"required": false,
|
|
647
|
+
"columnType": "TEXT"
|
|
249
648
|
},
|
|
250
649
|
{
|
|
251
|
-
"name": "
|
|
252
|
-
"type": "
|
|
650
|
+
"name": "taskRefId",
|
|
651
|
+
"type": "text",
|
|
253
652
|
"required": false,
|
|
254
|
-
"
|
|
255
|
-
|
|
653
|
+
"columnType": "TEXT"
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
"name": "kind",
|
|
657
|
+
"type": "text",
|
|
658
|
+
"required": false,
|
|
659
|
+
"columnType": "TEXT"
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
"name": "description",
|
|
663
|
+
"type": "text",
|
|
664
|
+
"required": false,
|
|
665
|
+
"columnType": "TEXT"
|
|
256
666
|
},
|
|
257
667
|
{
|
|
258
668
|
"name": "startedAt",
|
|
@@ -267,7 +677,21 @@
|
|
|
267
677
|
"columnType": "TIMESTAMP"
|
|
268
678
|
},
|
|
269
679
|
{
|
|
270
|
-
"name": "
|
|
680
|
+
"name": "durationSeconds",
|
|
681
|
+
"type": "integer",
|
|
682
|
+
"required": true,
|
|
683
|
+
"columnType": "INTEGER",
|
|
684
|
+
"default": 0
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
"name": "closedReason",
|
|
688
|
+
"type": "text",
|
|
689
|
+
"required": false,
|
|
690
|
+
"columnType": "TEXT",
|
|
691
|
+
"default": ""
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
"name": "reviewRequired",
|
|
271
695
|
"type": "boolean",
|
|
272
696
|
"required": true,
|
|
273
697
|
"columnType": "BOOLEAN",
|
|
@@ -290,9 +714,15 @@
|
|
|
290
714
|
}
|
|
291
715
|
],
|
|
292
716
|
"methods": [
|
|
717
|
+
"delete",
|
|
293
718
|
"save"
|
|
294
719
|
],
|
|
295
720
|
"methodSignatures": [
|
|
721
|
+
{
|
|
722
|
+
"name": "delete",
|
|
723
|
+
"async": true,
|
|
724
|
+
"returns": "Promise<void>"
|
|
725
|
+
},
|
|
296
726
|
{
|
|
297
727
|
"name": "save",
|
|
298
728
|
"async": true,
|
|
@@ -312,43 +742,43 @@
|
|
|
312
742
|
"surfaces": [
|
|
313
743
|
{
|
|
314
744
|
"kind": "api",
|
|
315
|
-
"name": "
|
|
745
|
+
"name": "attendancesegments.list",
|
|
316
746
|
"operation": "list",
|
|
317
|
-
"objectName": "@happyvertical/smrt-timesheets:
|
|
318
|
-
"path": "/
|
|
747
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment",
|
|
748
|
+
"path": "/attendancesegments",
|
|
319
749
|
"method": "GET"
|
|
320
750
|
},
|
|
321
751
|
{
|
|
322
752
|
"kind": "api",
|
|
323
|
-
"name": "
|
|
753
|
+
"name": "attendancesegments.get",
|
|
324
754
|
"operation": "get",
|
|
325
|
-
"objectName": "@happyvertical/smrt-timesheets:
|
|
326
|
-
"path": "/
|
|
755
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment",
|
|
756
|
+
"path": "/attendancesegments/[id]",
|
|
327
757
|
"method": "GET"
|
|
328
758
|
},
|
|
329
759
|
{
|
|
330
760
|
"kind": "cli",
|
|
331
|
-
"name": "
|
|
761
|
+
"name": "attendancesegment_list",
|
|
332
762
|
"operation": "list",
|
|
333
|
-
"objectName": "@happyvertical/smrt-timesheets:
|
|
763
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment"
|
|
334
764
|
},
|
|
335
765
|
{
|
|
336
766
|
"kind": "cli",
|
|
337
|
-
"name": "
|
|
767
|
+
"name": "attendancesegment_get",
|
|
338
768
|
"operation": "get",
|
|
339
|
-
"objectName": "@happyvertical/smrt-timesheets:
|
|
769
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment"
|
|
340
770
|
},
|
|
341
771
|
{
|
|
342
772
|
"kind": "mcp",
|
|
343
|
-
"name": "
|
|
773
|
+
"name": "attendancesegment_list",
|
|
344
774
|
"operation": "list",
|
|
345
|
-
"objectName": "@happyvertical/smrt-timesheets:
|
|
775
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment"
|
|
346
776
|
},
|
|
347
777
|
{
|
|
348
778
|
"kind": "mcp",
|
|
349
|
-
"name": "
|
|
779
|
+
"name": "attendancesegment_get",
|
|
350
780
|
"operation": "get",
|
|
351
|
-
"objectName": "@happyvertical/smrt-timesheets:
|
|
781
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment"
|
|
352
782
|
}
|
|
353
783
|
],
|
|
354
784
|
"withheldSurfaces": [
|
|
@@ -357,7 +787,7 @@
|
|
|
357
787
|
"operation": "save",
|
|
358
788
|
"code": "lifecycle-method",
|
|
359
789
|
"reason": "`save` is a framework lifecycle method, not a distinct operation",
|
|
360
|
-
"objectName": "@happyvertical/smrt-timesheets:
|
|
790
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment"
|
|
361
791
|
}
|
|
362
792
|
],
|
|
363
793
|
"relationshipFeatures": [
|
|
@@ -501,6 +931,24 @@
|
|
|
501
931
|
"tags": [],
|
|
502
932
|
"risks": []
|
|
503
933
|
},
|
|
934
|
+
{
|
|
935
|
+
"name": "AttendanceSegmentCollection",
|
|
936
|
+
"qualifiedName": "@happyvertical/smrt-timesheets:AttendanceSegmentCollection",
|
|
937
|
+
"collection": "attendancesegments",
|
|
938
|
+
"description": "Work segments for job costing and attendance review.",
|
|
939
|
+
"tableName": "attendance_segments",
|
|
940
|
+
"packageName": "@happyvertical/smrt-timesheets",
|
|
941
|
+
"extends": "SmrtCollection",
|
|
942
|
+
"fields": [],
|
|
943
|
+
"relationships": [],
|
|
944
|
+
"methods": [],
|
|
945
|
+
"surfaces": [],
|
|
946
|
+
"relationshipFeatures": [
|
|
947
|
+
"uuidColumns"
|
|
948
|
+
],
|
|
949
|
+
"tags": [],
|
|
950
|
+
"risks": []
|
|
951
|
+
},
|
|
504
952
|
{
|
|
505
953
|
"name": "AttendanceReplayCollection",
|
|
506
954
|
"qualifiedName": "@happyvertical/smrt-timesheets:AttendanceReplayCollection",
|
|
@@ -1415,6 +1863,19 @@
|
|
|
1415
1863
|
"required": false,
|
|
1416
1864
|
"columnType": "TEXT"
|
|
1417
1865
|
},
|
|
1866
|
+
{
|
|
1867
|
+
"name": "submittedAt",
|
|
1868
|
+
"type": "datetime",
|
|
1869
|
+
"required": false,
|
|
1870
|
+
"columnType": "TIMESTAMP"
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
"name": "submittedByProfileId",
|
|
1874
|
+
"type": "crossPackageRef",
|
|
1875
|
+
"required": false,
|
|
1876
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
1877
|
+
"columnType": "UUID"
|
|
1878
|
+
},
|
|
1418
1879
|
{
|
|
1419
1880
|
"name": "confirmedAt",
|
|
1420
1881
|
"type": "datetime",
|
|
@@ -1437,6 +1898,13 @@
|
|
|
1437
1898
|
"related": "@happyvertical/smrt-profiles:Profile",
|
|
1438
1899
|
"columnType": "UUID"
|
|
1439
1900
|
},
|
|
1901
|
+
{
|
|
1902
|
+
"name": "submittedByProfileId",
|
|
1903
|
+
"type": "crossPackageRef",
|
|
1904
|
+
"required": false,
|
|
1905
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
1906
|
+
"columnType": "UUID"
|
|
1907
|
+
},
|
|
1440
1908
|
{
|
|
1441
1909
|
"name": "confirmedByProfileId",
|
|
1442
1910
|
"type": "crossPackageRef",
|
|
@@ -1642,6 +2110,146 @@
|
|
|
1642
2110
|
"tags": [],
|
|
1643
2111
|
"risks": []
|
|
1644
2112
|
},
|
|
2113
|
+
{
|
|
2114
|
+
"name": "TimecardReopening",
|
|
2115
|
+
"qualifiedName": "@happyvertical/smrt-timesheets:TimecardReopening",
|
|
2116
|
+
"collection": "timecardreopenings",
|
|
2117
|
+
"description": "Append-only audit row for one reopening of a submitted or confirmed card: who reopened whose period, why, and the totals it had at that moment (base, adjustments and effective, in integer seconds).",
|
|
2118
|
+
"tableName": "timecard_reopenings",
|
|
2119
|
+
"packageName": "@happyvertical/smrt-timesheets",
|
|
2120
|
+
"extends": "SmrtObject",
|
|
2121
|
+
"fields": [
|
|
2122
|
+
{
|
|
2123
|
+
"name": "tenantId",
|
|
2124
|
+
"type": "text",
|
|
2125
|
+
"required": true,
|
|
2126
|
+
"columnType": "UUID"
|
|
2127
|
+
},
|
|
2128
|
+
{
|
|
2129
|
+
"name": "timecardId",
|
|
2130
|
+
"type": "foreignKey",
|
|
2131
|
+
"required": false,
|
|
2132
|
+
"related": "PeriodTimecard",
|
|
2133
|
+
"columnType": "UUID"
|
|
2134
|
+
},
|
|
2135
|
+
{
|
|
2136
|
+
"name": "profileId",
|
|
2137
|
+
"type": "crossPackageRef",
|
|
2138
|
+
"required": false,
|
|
2139
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
2140
|
+
"columnType": "UUID"
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
"name": "actorProfileId",
|
|
2144
|
+
"type": "crossPackageRef",
|
|
2145
|
+
"required": false,
|
|
2146
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
2147
|
+
"columnType": "UUID"
|
|
2148
|
+
},
|
|
2149
|
+
{
|
|
2150
|
+
"name": "operationId",
|
|
2151
|
+
"type": "text",
|
|
2152
|
+
"required": false,
|
|
2153
|
+
"columnType": "TEXT"
|
|
2154
|
+
},
|
|
2155
|
+
{
|
|
2156
|
+
"name": "fromStatus",
|
|
2157
|
+
"type": "text",
|
|
2158
|
+
"required": false,
|
|
2159
|
+
"columnType": "TEXT"
|
|
2160
|
+
},
|
|
2161
|
+
{
|
|
2162
|
+
"name": "reason",
|
|
2163
|
+
"type": "text",
|
|
2164
|
+
"required": false,
|
|
2165
|
+
"columnType": "TEXT"
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
"name": "previousState",
|
|
2169
|
+
"type": "text",
|
|
2170
|
+
"required": false,
|
|
2171
|
+
"columnType": "TEXT"
|
|
2172
|
+
}
|
|
2173
|
+
],
|
|
2174
|
+
"relationships": [
|
|
2175
|
+
{
|
|
2176
|
+
"name": "timecardId",
|
|
2177
|
+
"type": "foreignKey",
|
|
2178
|
+
"required": false,
|
|
2179
|
+
"related": "PeriodTimecard",
|
|
2180
|
+
"columnType": "UUID"
|
|
2181
|
+
},
|
|
2182
|
+
{
|
|
2183
|
+
"name": "profileId",
|
|
2184
|
+
"type": "crossPackageRef",
|
|
2185
|
+
"required": false,
|
|
2186
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
2187
|
+
"columnType": "UUID"
|
|
2188
|
+
},
|
|
2189
|
+
{
|
|
2190
|
+
"name": "actorProfileId",
|
|
2191
|
+
"type": "crossPackageRef",
|
|
2192
|
+
"required": false,
|
|
2193
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
2194
|
+
"columnType": "UUID"
|
|
2195
|
+
}
|
|
2196
|
+
],
|
|
2197
|
+
"methods": [
|
|
2198
|
+
"delete",
|
|
2199
|
+
"getPrevious",
|
|
2200
|
+
"save"
|
|
2201
|
+
],
|
|
2202
|
+
"methodSignatures": [
|
|
2203
|
+
{
|
|
2204
|
+
"name": "delete",
|
|
2205
|
+
"async": true,
|
|
2206
|
+
"returns": "Promise<void>"
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
"name": "getPrevious",
|
|
2210
|
+
"returns": "T"
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
"name": "save",
|
|
2214
|
+
"async": true,
|
|
2215
|
+
"returns": "Promise<this>"
|
|
2216
|
+
}
|
|
2217
|
+
],
|
|
2218
|
+
"tenant": {
|
|
2219
|
+
"scoped": true,
|
|
2220
|
+
"mode": "required",
|
|
2221
|
+
"field": "tenantId"
|
|
2222
|
+
},
|
|
2223
|
+
"conflictColumns": [
|
|
2224
|
+
"tenant_id",
|
|
2225
|
+
"slug",
|
|
2226
|
+
"context"
|
|
2227
|
+
],
|
|
2228
|
+
"surfaces": [],
|
|
2229
|
+
"withheldSurfaces": [
|
|
2230
|
+
{
|
|
2231
|
+
"kind": "api",
|
|
2232
|
+
"operation": "getPrevious",
|
|
2233
|
+
"code": "not-included",
|
|
2234
|
+
"reason": "not listed in api.include",
|
|
2235
|
+
"objectName": "@happyvertical/smrt-timesheets:TimecardReopening"
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
"kind": "api",
|
|
2239
|
+
"operation": "save",
|
|
2240
|
+
"code": "lifecycle-method",
|
|
2241
|
+
"reason": "`save` is a framework lifecycle method, not a distinct operation",
|
|
2242
|
+
"objectName": "@happyvertical/smrt-timesheets:TimecardReopening"
|
|
2243
|
+
}
|
|
2244
|
+
],
|
|
2245
|
+
"relationshipFeatures": [
|
|
2246
|
+
"crossPackageRef",
|
|
2247
|
+
"foreignKey",
|
|
2248
|
+
"uuidColumns"
|
|
2249
|
+
],
|
|
2250
|
+
"tags": [],
|
|
2251
|
+
"risks": []
|
|
2252
|
+
},
|
|
1645
2253
|
{
|
|
1646
2254
|
"name": "PeriodTimecardCollection",
|
|
1647
2255
|
"qualifiedName": "@happyvertical/smrt-timesheets:PeriodTimecardCollection",
|
|
@@ -1677,6 +2285,24 @@
|
|
|
1677
2285
|
],
|
|
1678
2286
|
"tags": [],
|
|
1679
2287
|
"risks": []
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
"name": "TimecardReopeningCollection",
|
|
2291
|
+
"qualifiedName": "@happyvertical/smrt-timesheets:TimecardReopeningCollection",
|
|
2292
|
+
"collection": "timecardreopenings",
|
|
2293
|
+
"description": "Scoped append-only reopening ledger.",
|
|
2294
|
+
"tableName": "timecard_reopenings",
|
|
2295
|
+
"packageName": "@happyvertical/smrt-timesheets",
|
|
2296
|
+
"extends": "SmrtCollection",
|
|
2297
|
+
"fields": [],
|
|
2298
|
+
"relationships": [],
|
|
2299
|
+
"methods": [],
|
|
2300
|
+
"surfaces": [],
|
|
2301
|
+
"relationshipFeatures": [
|
|
2302
|
+
"uuidColumns"
|
|
2303
|
+
],
|
|
2304
|
+
"tags": [],
|
|
2305
|
+
"risks": []
|
|
1680
2306
|
}
|
|
1681
2307
|
],
|
|
1682
2308
|
"surfaces": [
|
|
@@ -1760,6 +2386,46 @@
|
|
|
1760
2386
|
"operation": "get",
|
|
1761
2387
|
"objectName": "@happyvertical/smrt-timesheets:AttendanceBreak"
|
|
1762
2388
|
},
|
|
2389
|
+
{
|
|
2390
|
+
"kind": "api",
|
|
2391
|
+
"name": "attendancesegments.list",
|
|
2392
|
+
"operation": "list",
|
|
2393
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment",
|
|
2394
|
+
"path": "/attendancesegments",
|
|
2395
|
+
"method": "GET"
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
"kind": "api",
|
|
2399
|
+
"name": "attendancesegments.get",
|
|
2400
|
+
"operation": "get",
|
|
2401
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment",
|
|
2402
|
+
"path": "/attendancesegments/[id]",
|
|
2403
|
+
"method": "GET"
|
|
2404
|
+
},
|
|
2405
|
+
{
|
|
2406
|
+
"kind": "cli",
|
|
2407
|
+
"name": "attendancesegment_list",
|
|
2408
|
+
"operation": "list",
|
|
2409
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment"
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
"kind": "cli",
|
|
2413
|
+
"name": "attendancesegment_get",
|
|
2414
|
+
"operation": "get",
|
|
2415
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment"
|
|
2416
|
+
},
|
|
2417
|
+
{
|
|
2418
|
+
"kind": "mcp",
|
|
2419
|
+
"name": "attendancesegment_list",
|
|
2420
|
+
"operation": "list",
|
|
2421
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment"
|
|
2422
|
+
},
|
|
2423
|
+
{
|
|
2424
|
+
"kind": "mcp",
|
|
2425
|
+
"name": "attendancesegment_get",
|
|
2426
|
+
"operation": "get",
|
|
2427
|
+
"objectName": "@happyvertical/smrt-timesheets:AttendanceSegment"
|
|
2428
|
+
},
|
|
1763
2429
|
{
|
|
1764
2430
|
"kind": "api",
|
|
1765
2431
|
"name": "servicechargesnapshots.list",
|
|
@@ -1923,19 +2589,19 @@
|
|
|
1923
2589
|
],
|
|
1924
2590
|
"prompts": [],
|
|
1925
2591
|
"relationshipsV2": {
|
|
1926
|
-
"foreignKeyFields":
|
|
1927
|
-
"crossPackageRefFields":
|
|
2592
|
+
"foreignKeyFields": 9,
|
|
2593
|
+
"crossPackageRefFields": 15,
|
|
1928
2594
|
"junctionCollections": 0,
|
|
1929
2595
|
"hierarchicalObjects": 0,
|
|
1930
2596
|
"polymorphicAssociations": 0,
|
|
1931
|
-
"uuidColumns":
|
|
2597
|
+
"uuidColumns": 92
|
|
1932
2598
|
},
|
|
1933
|
-
"agentDoc": "# smrt-timesheets\n\nShared time entries: who (a `smrt-profiles` Profile, or an agent) worked how\nlong on what (a generic work reference), whether it was approved, and the\nimmutable charge / compensation snapshots taken at approval. Moved out of\n`smrt-projects` / `smrt-support` in #3288 so shops, builders, and service\ndesks share one model instead of re-implementing it.\n\nThis package contains **layer 1 (entry)**, **layer 2 (attendance)**, and\n**layer 3 (period rollup)**.\n`src/attendance/` owns punches, breaks, atomic transitions and durable offline\nreceipts; `./attendance` is the public subpath. Period rollup belongs in this\npackage too, never projects or support. See the README attendance contract.\nAttendance services accept trusted session actors, serialize mutations per\nactor, and bind every write to one executor. The new tables use non-null unique\nopen slots, not nullable unique keys. A linked entry is draft evidence, and\nrollups must deduplicate `AttendancePunch.serviceTimeEntryId`.\n\n## Models\n\n| Model | Table | Notes |\n|---|---|---|\n| `ServiceTimeEntry` | `service_time_entries` | `workRefType` + `workRefId`, `participantKind` (`human` \\| `agent`), `participantProfileId` / `agentRef`, `source`, `startedAt` / `endedAt` / `durationSeconds`, status machine, `correctionOfId` |\n| `ServiceChargeSnapshot` | `service_charge_snapshots` | Client charge, integer minor units (#2401), one per entry |\n| `ServiceCompensationSnapshot` | `service_compensation_snapshots` | Provider earning, integer minor units, one per entry |\n\n`ServiceEvidenceService` records, submits, approves (writing both snapshots\nthrough a `ServiceCommercialResolver`), and corrects. `smrt-projects` supplies\n`SubscriptionServiceCommercialResolver` for #1925 pricing.\n\n## Invariants\n\n- **No domain foreign key on the base.** Work context is the generic\n `workRefType` (qualified class name) + `workRefId`. A domain adds its own\n context by subclassing over the same table — smrt-support's\n `ServiceTimeEntry` adds `caseId` / `specialistId` and extends\n `frozenFieldNames()` so they freeze on approval.\n- **Entries carry no rate.** Money lives only on the snapshots, `= 0`\n integer initializers (INTEGER columns). Never `= 0.0`. Snapshot `save()`\n rejects an `amount` that is not a safe integer (`VALIDATION_INVALID_VALUE`),\n so SQLite cannot silently store `19.99`; fix a fractional resolver at its\n source rather than rounding here.\n- **Approved evidence never changes.** `save()` enforces\n `SERVICE_TIME_ENTRY_STATUS_TRANSITIONS` and compares the frozen fields with\n the stored row. A correction is a new row (`correctionOfId`), the original\n flips to `corrected`. Snapshots are immutable after their first save.\n- **Old qualified names are aliases.** Each model declares its pre-#3288\n `@happyvertical/smrt-projects:*` name in `previousQualifiedNames` (#3338;\n `packages/core/agents/model-moves.md`). smrt-projects must never list these\n classes in its manifests again (`CONFIG_QUALIFIED_NAME_ALIAS_COLLISION`),\n and no subtype (smrt-support's, or a consumer's closing subclass) may\n redeclare an alias (two claimants). Known gap: a same-named subtype that\n replaces a moved class drops that class's alias, and the replaced class's\n own `@happyvertical/smrt-timesheets:*` name stops resolving too — guidance\n for apps with smrt-support must name `@happyvertical/smrt-support:ServiceTimeEntry`.\n Support's vitest environment keeps both classes registered, so verify this\n against built packages, not package tests. `projects/src/__tests__/legacy-qualified-identity.test.ts` pins\n the projects-only contract.\n- **Table names are a compatibility contract.** `service_time_entries` and the\n two snapshot tables hold existing production rows from smrt-projects /\n smrt-support; renaming any of them, or adding a discriminator, is a data\n migration.\n\n## One table family\n\nClasses may share a table only as one inheritance chain. Core resolves\nsame-named subclasses by constructor ancestry at runtime and qualified manifest\nancestry at build registration; the deepest subtype wins across N levels.\nSibling or unrelated types cannot share the table. `smrt-projects` re-exports\nthe base; `smrt-support` subclasses it directly.\n\nConsumers closing package routes should call\n`ObjectRegistry.registerOverride(qualifiedName, { api: false, mcp: false,\ncli: false, tenancy: { mode: 'required' } })` after importing the model and before\nconstructing transports. This tightens the existing registration without a new\nsubtype. See core's README for startup order, static route generation, and reset\nsemantics. `src/__tests__/consumer-closed-surface.test.ts` also covers existing\nmanifest subclass consumers. The snapshots' explicit `time_entry_id` conflict\nkey is not rewritten when a consumer requires tenancy; widening it remains a\nconsumer schema change.\n\nSubclasses restate their fields: package-isolated manifest scanning does not\nread dependency sources, so an unrestated subclass manifest has no columns.\n\n## Svelte UI\n\n`./svelte` exports `TimeEntryCard`, `TimeEntryList`, `TimeSummary`,\n`DurationDisplay`, and `TimeEntryApprovalQueue` (props-driven, plain view\nobjects, `smrt-ui` primitives, `smrtRawPrimitives: \"strict\"`). smrt-projects\nand smrt-support re-export them from their own `./svelte` subpaths.\nCard/list/summary message keys keep their `projects.*` namespace: tenant\nlanguage overrides are stored by key, and renaming would orphan them. New\nkeys use `timesheets.*`.\n\nDisplay components keep the legacy one-decimal hours format by default. A\nconsumer that retains more precise source evidence supplies `hoursFormatter`;\ncard/list/queue detail snippets carry domain evidence without widening the base\nview. ApprovalQueue's optional action snippet replaces its callback controls so\nthe host can own a native form, request identity, authorization and retry flow.\nDisplay currency codes are open ISO strings and amounts remain integer minor\nunits; never put private rates into a view that should not expose them.\n\n## Validation\n\n```sh\npnpm --filter @happyvertical/smrt-timesheets test\npnpm --filter @happyvertical/smrt-timesheets test:postgres\npnpm --filter @happyvertical/smrt-timesheets typecheck\npnpm --filter @happyvertical/smrt-timesheets build\npnpm --filter @happyvertical/smrt-projects test\npnpm --filter @happyvertical/smrt-support test\n```\n\nSchema-affecting edits must keep `service-time-entry-compat.test.ts` (projects,\nsupport) and `service-time-entry-migration-plan.test.ts` (support) green: a\npre-move table must plan no DDL.\n\n## Period rollup\n\nLayer 3 lives under `src/rollup/` and the `./rollup` subpath. See\n[period-rollup.md](agents/period-rollup.md) for resolver, source clipping,\nconfirmation, attributed adjustments, isolation, and transaction contracts.\nTimecards and adjustments have closed generated surfaces and service-only\nmodel writes; never bypass the instance capability with generic persistence.\n",
|
|
2599
|
+
"agentDoc": "# smrt-timesheets\n\nShared time entries: who (a `smrt-profiles` Profile, or an agent) worked how\nlong on what (a generic work reference), whether it was approved, and the\nimmutable charge / compensation snapshots taken at approval. Moved out of\n`smrt-projects` / `smrt-support` in #3288 so shops, builders, and service\ndesks share one model instead of re-implementing it.\n\nThis package contains **layer 1 (entry)**, **layer 2 (attendance)**, and\n**layer 3 (period rollup)**.\n`src/attendance/` owns punches, breaks, atomic transitions and durable offline\nreceipts; `./attendance` is the public subpath. Period rollup belongs in this\npackage too, never projects or support. See the README attendance contract.\nAttendance services accept trusted session actors, serialize mutations per\nactor, and bind every write to one executor. The new tables use non-null unique\nopen slots, not nullable unique keys. A linked entry is draft evidence, and\nrollups must deduplicate `AttendancePunch.serviceTimeEntryId`.\n`AttendanceSegment` (`attendance_segments`) is a running work interval inside a\npunch: queryable `workRef*`/`taskRef*`/`kind` columns, one open per punch\n(`openSlot`), closed by `startBreak`/`punchOut`/`autoClose`, never overlapping\nbreaks or each other, immutable once closed. It creates no `ServiceTimeEntry`.\nClosed punches, breaks and segments change only through\n`AttendanceCorrectionService` (`attendance/correction-service.ts`), which sets a\nper-instance capability (`attendance/write.ts`, not exported) that lifts the\nmodels' closed-interval checks for one save, validates the whole punch layout,\nrecomputes durations and appends an `AttendanceCorrection` (append-only ledger,\nno generated operations; only a first acknowledgement by the subject may update\na 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\nopt-in `confirmedTimecardGuard` (`rollup/correction-guard.ts`), which refuses\nwhile a card is submitted or confirmed. `PeriodRollupService.reopen()` returns\nsuch a card to `open` and appends a `TimecardReopening` (`timecard_reopenings`,\nappend-only, service-only via `rollup/write.ts`); `submit()` is the optional\nopen -> submitted step (`requireSubmission` makes it mandatory) (#3950).\nSegment transitions live in `attendance/segments.ts`; compose them with other\nattendance writes via `AttendanceService.withTransaction`. Any new attendance\ntable must be added to the `includeObjects` lists in the `*.optional.test.ts`\nfiles.\n\n## Models\n\n| Model | Table | Notes |\n|---|---|---|\n| `ServiceTimeEntry` | `service_time_entries` | `workRefType` + `workRefId`, `participantKind` (`human` \\| `agent`), `participantProfileId` / `agentRef`, `source`, `startedAt` / `endedAt` / `durationSeconds`, status machine, `correctionOfId` |\n| `ServiceChargeSnapshot` | `service_charge_snapshots` | Client charge, integer minor units (#2401), one per entry |\n| `ServiceCompensationSnapshot` | `service_compensation_snapshots` | Provider earning, integer minor units, one per entry |\n\n`ServiceEvidenceService` records, submits, approves (writing both snapshots\nthrough a `ServiceCommercialResolver`), and corrects. `smrt-projects` supplies\n`SubscriptionServiceCommercialResolver` for #1925 pricing.\n\n## Invariants\n\n- **No domain foreign key on the base.** Work context is the generic\n `workRefType` (qualified class name) + `workRefId`. A domain adds its own\n context by subclassing over the same table — smrt-support's\n `ServiceTimeEntry` adds `caseId` / `specialistId` and extends\n `frozenFieldNames()` so they freeze on approval.\n- **Entries carry no rate.** Money lives only on the snapshots, `= 0`\n integer initializers (INTEGER columns). Never `= 0.0`. Snapshot `save()`\n rejects an `amount` that is not a safe integer (`VALIDATION_INVALID_VALUE`),\n so SQLite cannot silently store `19.99`; fix a fractional resolver at its\n source rather than rounding here.\n- **Approved evidence never changes.** `save()` enforces\n `SERVICE_TIME_ENTRY_STATUS_TRANSITIONS` and compares the frozen fields with\n the stored row. A correction is a new row (`correctionOfId`), the original\n flips to `corrected`. Snapshots are immutable after their first save.\n- **Old qualified names are aliases.** Each model declares its pre-#3288\n `@happyvertical/smrt-projects:*` name in `previousQualifiedNames` (#3338;\n `packages/core/agents/model-moves.md`). smrt-projects must never list these\n classes in its manifests again (`CONFIG_QUALIFIED_NAME_ALIAS_COLLISION`),\n and no subtype (smrt-support's, or a consumer's closing subclass) may\n redeclare an alias (two claimants). Known gap: a same-named subtype that\n replaces a moved class drops that class's alias, and the replaced class's\n own `@happyvertical/smrt-timesheets:*` name stops resolving too — guidance\n for apps with smrt-support must name `@happyvertical/smrt-support:ServiceTimeEntry`.\n Support's vitest environment keeps both classes registered, so verify this\n against built packages, not package tests. `projects/src/__tests__/legacy-qualified-identity.test.ts` pins\n the projects-only contract.\n- **Table names are a compatibility contract.** `service_time_entries` and the\n two snapshot tables hold existing production rows from smrt-projects /\n smrt-support; renaming any of them, or adding a discriminator, is a data\n migration.\n\n## One table family\n\nClasses may share a table only as one inheritance chain. Core resolves\nsame-named subclasses by constructor ancestry at runtime and qualified manifest\nancestry at build registration; the deepest subtype wins across N levels.\nSibling or unrelated types cannot share the table. `smrt-projects` re-exports\nthe base; `smrt-support` subclasses it directly.\n\nConsumers closing package routes should call\n`ObjectRegistry.registerOverride(qualifiedName, { api: false, mcp: false,\ncli: false, tenancy: { mode: 'required' } })` after importing the model and before\nconstructing transports. This tightens the existing registration without a new\nsubtype. See core's README for startup order, static route generation, and reset\nsemantics. `src/__tests__/consumer-closed-surface.test.ts` also covers existing\nmanifest subclass consumers. The snapshots' explicit `time_entry_id` conflict\nkey is not rewritten when a consumer requires tenancy; widening it remains a\nconsumer schema change.\n\nSubclasses restate their fields: package-isolated manifest scanning does not\nread dependency sources, so an unrestated subclass manifest has no columns.\n\n## Svelte UI\n\n`./svelte` exports `TimeEntryCard`, `TimeEntryList`, `TimeSummary`,\n`DurationDisplay`, and `TimeEntryApprovalQueue` (props-driven, plain view\nobjects, `smrt-ui` primitives, `smrtRawPrimitives: \"strict\"`). smrt-projects\nand smrt-support re-export them from their own `./svelte` subpaths.\nCard/list/summary message keys keep their `projects.*` namespace: tenant\nlanguage overrides are stored by key, and renaming would orphan them. New\nkeys use `timesheets.*`.\n\nDisplay components keep the legacy one-decimal hours format by default. A\nconsumer that retains more precise source evidence supplies `hoursFormatter`;\ncard/list/queue detail snippets carry domain evidence without widening the base\nview. ApprovalQueue's optional action snippet replaces its callback controls so\nthe host can own a native form, request identity, authorization and retry flow.\nDisplay currency codes are open ISO strings and amounts remain integer minor\nunits; never put private rates into a view that should not expose them.\n\n## Validation\n\n```sh\npnpm --filter @happyvertical/smrt-timesheets test\npnpm --filter @happyvertical/smrt-timesheets test:postgres\npnpm --filter @happyvertical/smrt-timesheets typecheck\npnpm --filter @happyvertical/smrt-timesheets build\npnpm --filter @happyvertical/smrt-projects test\npnpm --filter @happyvertical/smrt-support test\n```\n\nSchema-affecting edits must keep `service-time-entry-compat.test.ts` (projects,\nsupport) and `service-time-entry-migration-plan.test.ts` (support) green: a\npre-move table must plan no DDL.\n\n## Period rollup\n\nLayer 3 lives under `src/rollup/` and the `./rollup` subpath. See\n[period-rollup.md](agents/period-rollup.md) for resolver, source clipping,\nconfirmation, attributed adjustments, isolation, and transaction contracts.\nTimecards and adjustments have closed generated surfaces and service-only\nmodel writes; never bypass the instance capability with generic persistence.\n\nReplay 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;\n`replay(taps, { resolveAt, now, refusal })` only changes the stored time and\nreason. Never hash the resolved time. Work actions (`startWork`, `switchWork`,\n`stopWork`, `resumeWork`) run in the tap's transaction via `apply()`. Refusals store\n`code` from `AttendanceConflict.code`; give new conflicts a code.\n",
|
|
1934
2600
|
"moduleDocs": [
|
|
1935
2601
|
{
|
|
1936
2602
|
"path": "agents/period-rollup.md",
|
|
1937
2603
|
"module": "period-rollup",
|
|
1938
|
-
"content": "# Period timecards\n\n`@happyvertical/smrt-timesheets/rollup` exports `PeriodRollupService`,\n`PeriodTimecard`, `TimecardAdjustment`, their collections, and resolver types.\nThe package root also exports them. SQLite and PostgreSQL are supported.\nCreate application tables through normal migrations; neither service provisions\nschema. The attendance option additionally requires the attendance tables.\n\nConstruct a service only after authenticating and authorizing its trusted\n`{ tenantId, profileId }` scope. A conflicting ambient tenant is rejected.\n`options.actorProfileId` records a separately authorized supervisor; it defaults\nto the subject profile for self confirmation/correction. Never accept this\ntrusted scope or attribution unchecked from request input. Profile authorization\nis consumer-owned; the service filters every source/card/adjustment read by its\ntrusted subject and tenant. Generic API, CLI and MCP surfaces are closed by\ndefault; consumer registry overrides may further restrict them (#3318).\n\n## Resolver contract\n\n`periodFor(at, actor)` supplies half-open UTC `startsAt`/`endsAt`, an IANA\n`timezone`, a nonempty policy `version`, and a JSON `rules` snapshot.\nThe consumer owns period length, calendar start day, timezone and DST, overtime\nthresholds, holidays and classification. The requested instant must be inside\nthe window. Overlapping windows for existing cards are rejected: introduce a new\ncalendar at a boundary after existing windows, and use adjustments for already\nconfirmed periods.\n\n`classify(period, sources, actor)` returns nonnegative safe integer\n`regularSeconds` and `overtimeSeconds`, whose sum must equal source seconds.\nInputs are detached so resolver mutation cannot rewrite evidence. Resolver\nexceptions, malformed windows, invalid timezone, fractional/unsafe totals and\nnonconserving classification abort the transaction. No Alberta or other\njurisdiction rule is assumed, and no pay rate or money is stored.\n\nA configurable UTC calendar example (illustrative policy, not payroll advice):\n\n```ts\nimport { PeriodRollupService, type PeriodRulesResolver } from\n '@happyvertical/smrt-timesheets/rollup';\n\nconst settings = {\n lengthDays: 14, startDay: 1, timezone: 'UTC', // 0 = Sunday\n anchor: '2026-01-05T00:00:00Z', // aligned Monday; anchors fortnight phase\n thresholdSeconds: 80 * 3600,\n holidays: ['2026-12-25'],\n};\nconst resolver: PeriodRulesResolver = {\n async periodFor(at) {\n const anchor = new Date(settings.anchor);\n if (anchor.getUTCDay() !== settings.startDay || settings.timezone !== 'UTC')\n throw new Error('Use a timezone-aware calendar resolver for local periods');\n const length = settings.lengthDays * 86400000;\n const start = anchor.getTime() +\n Math.floor((at.getTime() - anchor.getTime()) / length) * length;\n return {\n startsAt: new Date(start), endsAt: new Date(start + length),\n timezone: settings.timezone, version: 'shop-policy-v1', rules: settings,\n };\n },\n async classify(period, sources) {\n const rules = period.rules as typeof settings;\n // This example assigns a contribution to its clipped start's UTC date.\n // Local daily rules should split contributions at their own local midnight.\n let regular = 0;\n let holiday = 0;\n for (const source of sources) {\n if (rules.holidays.includes(source.startsAt.slice(0, 10))) holiday += source.seconds;\n else regular += source.seconds;\n }\n return {\n regularSeconds: Math.min(regular, rules.thresholdSeconds),\n overtimeSeconds: holiday + Math.max(0, regular - rules.thresholdSeconds),\n };\n },\n};\nconst rollups = new PeriodRollupService(db, authorizedSubject, resolver,\n { attendance: true, actorProfileId: authenticatedSupervisorProfileId });\nconst card = await rollups.rollup(new Date());\nawait rollups.confirm(card.id!);\nawait rollups.adjust(card.id!, {\n operationId: requestId, regularSeconds: 900, overtimeSeconds: 0,\n reason: 'Approved correction after confirmation',\n});\nconst effectiveSeconds = await rollups.totals(card.id!);\n```\n\nA timezone-aware resolver must compute each local boundary separately: a local\nday crossing DST is not necessarily 86400 seconds. Resolvers can use an app's\nexisting calendar library; no new timezone dependency or implicit default is\nintroduced here.\n\n## Evidence and immutability\n\nDecimal-hours-only entries have no measured integer seconds. The seconds-only\nrollup rejects these sources explicitly when their approval instant falls in the\nrequested period; it never rounds them or counts null as\nzero. Use an hours-aware consumer policy for that source representation.\n\nOnly approved human `ServiceTimeEntry` rows for the subject count; corrected\nancestors do not. Correction chains must remain within that subject/tenant and\npoint to corrected parents. Multiple approved descendants of one root are\nrejected as ambiguous rather than double-counted. Entries with both `startedAt`\nand `endedAt` allocate their declared seconds\nproportionally across the period using exact integer prefix division, preserving\nseconds over adjacent periods. Unbounded entries count at `startedAt`, or, when\nabsent, `approvedAt`; an end-only timestamp does not create an interval. Missing\nboth attribution timestamps is an error. This is time attribution, not a\nclaim that a manual entry has a continuous wall-clock duration.\n\nWith `attendance: true`, closed punches without `reviewRequired` count, excluding\nunpaid breaks and retaining paid breaks. Open/review-required punches do not\ncount. Linked attendance is authoritative: its evidence entry and any approved\ncorrection descendants are excluded, including when the punch is still under\nreview or outside the requested period. Unlinked approved work entries and\nattendance both count; consumers must link overlapping evidence explicitly.\nSource snapshots preserve each paid attendance segment and entry identity.\n\nRepeated rollup updates one open card per tenant/person/start; a confirmed card\ncannot be recomputed. Confirmation is attributed and idempotent. Model save,\ncollection create/update/upsert and object/collection delete cannot bypass the\nservice. Model mutation capabilities are per-instance and not exported.\nDirect SQL is a trusted administrative boundary, not an exposed application API.\nProfile/card foreign keys restrict deletion to preserve history.\n\nAdjustments contain signed integer regular/overtime deltas, reason, operation\nkey and actor attribution. A retry reuses its row only for the exact same\npayload and actor. Effective totals may never become negative or unsafe.\nAdjustments never alter the confirmed row or source snapshot. Undo an adjustment\nwith a separately attributed compensating adjustment, never deletion.\n\nAll service mutations use a root transaction, transaction-bound collections,\nembedded writer serialization or a PostgreSQL tenant/person advisory lock.\nOutputs are reloaded on the root executor after commit. Rollup reads are an\napplication snapshot: subsequent source approvals/corrections appear on the\nnext open rollup, and confirmed cards require explicit adjustments.\n"
|
|
2604
|
+
"content": "# Period timecards\n\n`@happyvertical/smrt-timesheets/rollup` exports `PeriodRollupService`,\n`PeriodTimecard`, `TimecardAdjustment`, their collections, and resolver types.\nThe package root also exports them. SQLite and PostgreSQL are supported.\nCreate application tables through normal migrations; neither service provisions\nschema. The attendance option additionally requires the attendance tables.\n\nConstruct a service only after authenticating and authorizing its trusted\n`{ tenantId, profileId }` scope. A conflicting ambient tenant is rejected.\n`options.actorProfileId` records a separately authorized supervisor; it defaults\nto the subject profile for self confirmation/correction. Never accept this\ntrusted scope or attribution unchecked from request input. Profile authorization\nis consumer-owned; the service filters every source/card/adjustment read by its\ntrusted subject and tenant. Generic API, CLI and MCP surfaces are closed by\ndefault; consumer registry overrides may further restrict them (#3318).\n\n## Resolver contract\n\n`periodFor(at, actor)` supplies half-open UTC `startsAt`/`endsAt`, an IANA\n`timezone`, a nonempty policy `version`, and a JSON `rules` snapshot.\nThe consumer owns period length, calendar start day, timezone and DST, overtime\nthresholds, holidays and classification. The requested instant must be inside\nthe window. Overlapping windows for existing cards are rejected: introduce a new\ncalendar at a boundary after existing windows, and use adjustments for already\nconfirmed periods.\n\n`classify(period, sources, actor)` returns nonnegative safe integer\n`regularSeconds` and `overtimeSeconds`, whose sum must equal source seconds.\nInputs are detached so resolver mutation cannot rewrite evidence. Resolver\nexceptions, malformed windows, invalid timezone, fractional/unsafe totals and\nnonconserving classification abort the transaction. No Alberta or other\njurisdiction rule is assumed, and no pay rate or money is stored.\n\nA configurable UTC calendar example (illustrative policy, not payroll advice):\n\n```ts\nimport { PeriodRollupService, type PeriodRulesResolver } from\n '@happyvertical/smrt-timesheets/rollup';\n\nconst settings = {\n lengthDays: 14, startDay: 1, timezone: 'UTC', // 0 = Sunday\n anchor: '2026-01-05T00:00:00Z', // aligned Monday; anchors fortnight phase\n thresholdSeconds: 80 * 3600,\n holidays: ['2026-12-25'],\n};\nconst resolver: PeriodRulesResolver = {\n async periodFor(at) {\n const anchor = new Date(settings.anchor);\n if (anchor.getUTCDay() !== settings.startDay || settings.timezone !== 'UTC')\n throw new Error('Use a timezone-aware calendar resolver for local periods');\n const length = settings.lengthDays * 86400000;\n const start = anchor.getTime() +\n Math.floor((at.getTime() - anchor.getTime()) / length) * length;\n return {\n startsAt: new Date(start), endsAt: new Date(start + length),\n timezone: settings.timezone, version: 'shop-policy-v1', rules: settings,\n };\n },\n async classify(period, sources) {\n const rules = period.rules as typeof settings;\n // This example assigns a contribution to its clipped start's UTC date.\n // Local daily rules should split contributions at their own local midnight.\n let regular = 0;\n let holiday = 0;\n for (const source of sources) {\n if (rules.holidays.includes(source.startsAt.slice(0, 10))) holiday += source.seconds;\n else regular += source.seconds;\n }\n return {\n regularSeconds: Math.min(regular, rules.thresholdSeconds),\n overtimeSeconds: holiday + Math.max(0, regular - rules.thresholdSeconds),\n };\n },\n};\nconst rollups = new PeriodRollupService(db, authorizedSubject, resolver,\n { attendance: true, actorProfileId: authenticatedSupervisorProfileId });\nconst card = await rollups.rollup(new Date());\nawait rollups.confirm(card.id!);\nawait rollups.adjust(card.id!, {\n operationId: requestId, regularSeconds: 900, overtimeSeconds: 0,\n reason: 'Approved correction after confirmation',\n});\nconst effectiveSeconds = await rollups.totals(card.id!);\n```\n\nA timezone-aware resolver must compute each local boundary separately: a local\nday crossing DST is not necessarily 86400 seconds. Resolvers can use an app's\nexisting calendar library; no new timezone dependency or implicit default is\nintroduced here.\n\n## Evidence and immutability\n\nDecimal-hours-only entries have no measured integer seconds. The seconds-only\nrollup rejects these sources explicitly when their approval instant falls in the\nrequested period; it never rounds them or counts null as\nzero. Use an hours-aware consumer policy for that source representation.\n\nOnly approved human `ServiceTimeEntry` rows for the subject count; corrected\nancestors do not. Correction chains must remain within that subject/tenant and\npoint to corrected parents. Multiple approved descendants of one root are\nrejected as ambiguous rather than double-counted. Entries with both `startedAt`\nand `endedAt` allocate their declared seconds\nproportionally across the period using exact integer prefix division, preserving\nseconds over adjacent periods. Unbounded entries count at `startedAt`, or, when\nabsent, `approvedAt`; an end-only timestamp does not create an interval. Missing\nboth attribution timestamps is an error. This is time attribution, not a\nclaim that a manual entry has a continuous wall-clock duration.\n\nWith `attendance: true`, closed punches without `reviewRequired` count, excluding\nunpaid breaks and retaining paid breaks. Open/review-required punches do not\ncount. Linked attendance is authoritative: its evidence entry and any approved\ncorrection descendants are excluded, including when the punch is still under\nreview or outside the requested period. Unlinked approved work entries and\nattendance both count; consumers must link overlapping evidence explicitly.\nSource snapshots preserve each paid attendance segment and entry identity.\n\nRepeated rollup updates one open card per tenant/person/start; a confirmed card\ncannot be recomputed. Confirmation is attributed and idempotent. Model save,\ncollection create/update/upsert and object/collection delete cannot bypass the\nservice. Model mutation capabilities are per-instance and not exported.\nDirect SQL is a trusted administrative boundary, not an exposed application API.\nProfile/card foreign keys restrict deletion to preserve history.\n\nAdjustments contain signed integer regular/overtime deltas, reason, operation\nkey and actor attribution. A retry reuses its row only for the exact same\npayload and actor. Effective totals may never become negative or unsafe.\nAdjustments never alter the confirmed row or source snapshot. Undo an adjustment\nwith a separately attributed compensating adjustment, never deletion.\n\nAll service mutations use a root transaction, transaction-bound collections,\nembedded writer serialization or a PostgreSQL tenant/person advisory lock.\nOutputs are reloaded on the root executor after commit. Rollup reads are an\napplication snapshot: subsequent source approvals/corrections appear on the\nnext open rollup, and confirmed cards require explicit adjustments.\n\n## Lifecycle: submit, confirm, reopen (#3950)\n\n`open` (draft) -> `submitted` (optional) -> `confirmed`. `submit(id)` freezes an\nopen card for review; `confirm(id)` accepts an open card too unless the service\nis built with `{ requireSubmission: true }`. Submitted and confirmed cards are\nnot re-rolled.\n\n`reopen(id, { operationId, reason })` returns a submitted or confirmed card to\n`open` (clearing its submission/confirmation attribution) and appends a\n`TimecardReopening`: actor, reason, prior status and a JSON snapshot of the\nprevious base, adjustment and effective seconds (`getPrevious()`). It is\nidempotent per `operationId` and refuses a blank reason. `reopenings(id)` lists\nthe history. The consumer authorizes the actor; give reopening its own\npermission. Existing adjustments stay and keep counting in `totals()`: after a\nre-roll, offset ones the correction made redundant with a new adjustment.\n`confirmedTimecardGuard` refuses corrections while a card is submitted or\nconfirmed and allows them once it is reopened. The new `timecard_reopenings`\ntable, and nullable `submitted_at`/`submitted_by_profile_id` columns on\n`period_timecards`, need a migration.\n"
|
|
1939
2605
|
}
|
|
1940
2606
|
],
|
|
1941
2607
|
"browser": {
|