@happyvertical/smrt-timesheets 0.55.21 → 0.55.23
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/README.md +9 -0
- package/agents/labour-valuation.md +73 -0
- package/dist/attendance.js +2 -2
- package/dist/chunks/{attendance-OKyc1O4x.js → attendance-BVeStEBv.js} +3 -3
- package/dist/chunks/{attendance-OKyc1O4x.js.map → attendance-BVeStEBv.js.map} +1 -1
- package/dist/chunks/{models-B0NOh8I3.js → models-zU8Um7c2.js} +2 -2
- package/dist/chunks/{models-B0NOh8I3.js.map → models-zU8Um7c2.js.map} +1 -1
- package/dist/chunks/{rollup-2--s0AyP.js → rollup-U1IO_CMA.js} +3 -3
- package/dist/chunks/{rollup-2--s0AyP.js.map → rollup-U1IO_CMA.js.map} +1 -1
- package/dist/chunks/{service-time-entry-BY8UcnFk.js → service-time-entry-CBv-FF-W.js} +2 -2
- package/dist/chunks/{service-time-entry-BY8UcnFk.js.map → service-time-entry-CBv-FF-W.js.map} +1 -1
- package/dist/index.js +285 -8
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/rollup.js +1 -1
- package/dist/services/dated-labour-commercial-resolver.d.ts.map +1 -1
- package/dist/services/dated-percentage-labour-commercial-resolver.d.ts +36 -0
- package/dist/services/dated-percentage-labour-commercial-resolver.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/labour-math.d.ts +17 -0
- package/dist/services/labour-math.d.ts.map +1 -0
- package/dist/services/labour-valuation-reconciliation.d.ts +59 -0
- package/dist/services/labour-valuation-reconciliation.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +3 -3
- package/dist/svelte/components/TimeEntryApprovalQueue.svelte +1 -1
- package/dist/svelte/components/TimeEntryCard.svelte +4 -4
- package/dist/svelte/components/TimeEntryList.svelte +3 -3
- package/dist/svelte/components/TimeSummary.svelte +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -401,3 +401,12 @@ reconstructing a resolver after an interrupted approval, the host must restore
|
|
|
401
401
|
**the originally selected authorized date and rate terms**, not reselect from a
|
|
402
402
|
newly edited schedule; this helper introduces no persistence schema or schedule
|
|
403
403
|
lookup service. Fully completed approval retries retain their existing snapshots.
|
|
404
|
+
|
|
405
|
+
### Percentage burden and analytical labour valuation
|
|
406
|
+
|
|
407
|
+
`DatedPercentageLabourCommercialResolver` adds opt-in exact percentage-derived
|
|
408
|
+
burden without changing the additive dated resolver. `reconcileLabourValuation`
|
|
409
|
+
compares canonical approved loaded costs with an explicitly selected analytical
|
|
410
|
+
aggregate; it never reprices or posts approval facts. Both retain private
|
|
411
|
+
compensation boundaries. See [the exact contract and validation matrix](agents/labour-valuation.md)
|
|
412
|
+
for input units, immutable provenance, supported modes and host responsibilities.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Exact percentage labour and analytical reconciliation
|
|
2
|
+
|
|
3
|
+
Tracking: SMRT #3813; consumer tradeschief.com #283. Existing additive
|
|
4
|
+
`DatedLabourCommercialResolver` inputs and `dated-labour-v1` snapshots are
|
|
5
|
+
unchanged. No schema, dependency or migration changes.
|
|
6
|
+
|
|
7
|
+
`DatedPercentageLabourCommercialResolver` accepts the same dated host-authorized
|
|
8
|
+
schedule context, with `burdenPercent: { id, version, percent }` replacing
|
|
9
|
+
`burden`. Percent uses percentage points (`20` means twenty percent), not hourly
|
|
10
|
+
minor units or fractional `0.20`. Wage/customer rates and percent inputs support
|
|
11
|
+
up to 18 fractional digits of nonnegative plain decimal text. Each decimal
|
|
12
|
+
input is bounded to 40 characters and a value no greater than
|
|
13
|
+
`Number.MAX_SAFE_INTEGER`; these input/resource limits do not limit derived
|
|
14
|
+
product precision. Final components and aggregate amounts must be safe integer
|
|
15
|
+
minor units. Derived
|
|
16
|
+
hourly burden remains an exact rational, including products beyond 18 places;
|
|
17
|
+
no intermediate rounding or re-parsing through the hourly-rate precision limit.
|
|
18
|
+
Only final component amounts round half up. Provider amount is wage; private
|
|
19
|
+
terms retain loaded cost, percentage identity/version and exact hourly basis.
|
|
20
|
+
The compensation version is `dated-percentage-labour-v1`. Client terms retain
|
|
21
|
+
the original public allowlist and do not reveal the chosen compensation mode.
|
|
22
|
+
|
|
23
|
+
The host selects immutable tenant/project policies, currency, input precision,
|
|
24
|
+
and approved wage/percentage versions, and authorizes the worker. Supply the
|
|
25
|
+
same retained input for an incomplete approval retry. Later configuration cannot
|
|
26
|
+
reconstruct or amend a pending snapshot. Only supported modes may be offered;
|
|
27
|
+
missing required policy is a host validation error, never a migration default.
|
|
28
|
+
|
|
29
|
+
`reconcileLabourValuation` is a pure private report calculation, not a posting
|
|
30
|
+
API. Explicit mode `half-up-exact-loaded-aggregate` sums exact hours times wage
|
|
31
|
+
plus exact hours times burden across supplied approved snapshots, then rounds
|
|
32
|
+
that combined aggregate once. `canonicalCostMinor` separately sums the original
|
|
33
|
+
independently rounded component amounts. `differenceMinor` is analytical minus
|
|
34
|
+
canonical. Regrouping may change analytical rounding, never the canonical facts.
|
|
35
|
+
|
|
36
|
+
The caller supplies explicit tenant/currency/group id and revision, plus 1–10000
|
|
37
|
+
private evidence records. Each binds entry/snapshot/worker identity, host policy
|
|
38
|
+
id/version/digest, and a frozen compensation snapshot (including its native
|
|
39
|
+
id, tenantId and timeEntryId). Map native `termsVersion`/`rateSnapshot` to
|
|
40
|
+
`version`/parsed `terms` at the host boundary. Missing or unsupported calculation
|
|
41
|
+
terms, mixed currencies/tenants/groups, duplicate entries/snapshots, conflicting
|
|
42
|
+
policy digests/rate revisions, altered retained arithmetic and unsafe amounts
|
|
43
|
+
are rejected. Negative/reversal evidence is unsupported; a signed difference is
|
|
44
|
+
allowed. Input order does not affect results or sorted evidence membership.
|
|
45
|
+
|
|
46
|
+
IDs, group membership and supplied policy digests are consistency checks, not
|
|
47
|
+
proof of authenticity or authorization. The host loads authorized immutable
|
|
48
|
+
snapshots, verifies worker ownership and excludes superseded/corrected entries.
|
|
49
|
+
The API cannot discover omitted evidence or authenticate an invented snapshot.
|
|
50
|
+
It has no database, policy lookup, financial mutation or public projection.
|
|
51
|
+
|
|
52
|
+
## Behavior-to-test matrix
|
|
53
|
+
|
|
54
|
+
All rows below require current executable evidence before review and publication.
|
|
55
|
+
Package unit command: `pnpm --filter @happyvertical/smrt-timesheets test`.
|
|
56
|
+
Native command: `pnpm --filter @happyvertical/smrt-timesheets test:postgres`.
|
|
57
|
+
|
|
58
|
+
| Behavior/invariant | Reachable trigger; positive / negative | Actor/context | Executor/transaction | Runtime/dialect | External edge | Level / validation |
|
|
59
|
+
|---|---|---|---|---|---|---|
|
|
60
|
+
| Exact percentage components | Approval: quarter hours, zero, >18-place product / malformed, ambiguous, overflow | Authorized host worker | Pure preflight | Node; dialect N/A (pure) | Distinct percent vs amount | Public unit / package unit |
|
|
61
|
+
| Dated selection and isolation | Construct/approve: adjacent period / date gap, foreign tenant/worker | Host tenant × worker | Pure preflight | Node; dialect N/A (pure) | Existing dated API preserved | Public unit / package unit |
|
|
62
|
+
| Private immutable provenance | Partial approval then retry: retained terms / caller amendment | Owner vs client projection | Existing ServiceEvidenceService executor | SQLite + PostgreSQL | Opt-in version; old additive regression | Shared integration / package unit + native |
|
|
63
|
+
| Atomic host mutation | Approved entry then exception / zero persisted effects | Tenant host | All collections bind host transaction | SQLite + PostgreSQL | Database rollback | Shared integration / package unit + native |
|
|
64
|
+
| Canonical vs analytical | Two fractional entries: sum rounded vs rounded sum / invalid amount, unsupported version/mode | Authorized private reader | N/A pure, no writes | Node; dialect N/A (pure) | Frozen snapshot input | Public unit / package unit |
|
|
65
|
+
| Evidence integrity | Reordering/grouping: stable canonical and permutation / duplicates, foreign identity, drift | Tenant + group + supplied policy | N/A pure, no writes | Node; dialect N/A (pure) | Host authenticates inputs | Public unit / package unit |
|
|
66
|
+
| Release compatibility | Package import/build: exported types/resolvers / inaccessible exports | Public consumer | N/A no persistence | Supported package runtime | Published artifact | Build/typecheck/verify:pack plus coordinator public consumer proof |
|
|
67
|
+
|
|
68
|
+
This is an additive feature, not a claimed regression fix; base-failure comparison
|
|
69
|
+
is N/A. Existing additive assertions remain unchanged. Run package
|
|
70
|
+
build/typecheck, downstream projects/support tests and applicable documented root
|
|
71
|
+
checks before exact-head review. Two independent financial/public-contract
|
|
72
|
+
reviews are required before shipping. Exact-head results belong in the pull
|
|
73
|
+
request and its validation/review evidence, not this durable contract.
|
package/dist/attendance.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as ServiceTimeEntryCollection } from "./chunks/service-time-entry-
|
|
2
|
-
import { a as AttendanceReplay, i as AttendancePunchCollection, n as AttendanceBreakCollection, o as AttendanceReplayCollection, r as AttendancePunch, t as AttendanceBreak } from "./chunks/models-
|
|
1
|
+
import { i as ServiceTimeEntryCollection } from "./chunks/service-time-entry-CBv-FF-W.js";
|
|
2
|
+
import { a as AttendanceReplay, i as AttendancePunchCollection, n as AttendanceBreakCollection, o as AttendanceReplayCollection, r as AttendancePunch, t as AttendanceBreak } from "./chunks/models-zU8Um7c2.js";
|
|
3
3
|
import { isEmbeddedDatabase, isPostgresDatabase, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
|
|
4
4
|
import { getTenantId, withTenant } from "@happyvertical/smrt-tenancy";
|
|
5
5
|
//#region src/attendance/service.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as AttendancePunchCollection, n as AttendanceBreakCollection } from "./models-
|
|
2
|
-
import { i as integer, n as clipSource, r as instant } from "./rollup-
|
|
1
|
+
import { i as AttendancePunchCollection, n as AttendanceBreakCollection } from "./models-zU8Um7c2.js";
|
|
2
|
+
import { i as integer, n as clipSource, r as instant } from "./rollup-U1IO_CMA.js";
|
|
3
3
|
//#region src/rollup/attendance.ts
|
|
4
4
|
async function attendanceSources(db, actor, period) {
|
|
5
5
|
const punches = await AttendancePunchCollection.create({ db });
|
|
@@ -57,4 +57,4 @@ async function attendanceSources(db, actor, period) {
|
|
|
57
57
|
//#endregion
|
|
58
58
|
export { attendanceSources };
|
|
59
59
|
|
|
60
|
-
//# sourceMappingURL=attendance-
|
|
60
|
+
//# sourceMappingURL=attendance-BVeStEBv.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attendance-
|
|
1
|
+
{"version":3,"file":"attendance-BVeStEBv.js","names":[],"sources":["../../src/rollup/attendance.ts"],"sourcesContent":["import type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n AttendanceBreakCollection,\n AttendancePunchCollection,\n} from '../attendance/models.js';\nimport { clipSource, instant, integer } from './calculation.js';\nimport type { TimecardActor, TimecardPeriod, TimecardSource } from './types.js';\n\n/** Optional attendance reader. Called on the rollup's transaction executor. */\nexport async function attendanceSources(\n db: DatabaseInterface,\n actor: TimecardActor,\n period: TimecardPeriod,\n): Promise<{ sources: TimecardSource[]; linkedEntryIds: Set<string> }> {\n const punches = await AttendancePunchCollection.create({ db });\n const breaks = await AttendanceBreakCollection.create({ db });\n const sources: TimecardSource[] = [];\n const linkedEntryIds = new Set<string>();\n for (const punch of await punches.list({ where: { ...actor } })) {\n // A linked punch is authoritative even when pending review: never count its\n // evidence entry behind the review gate, or outside the punch's period.\n if (punch.serviceTimeEntryId) linkedEntryIds.add(punch.serviceTimeEntryId);\n if (!punch.endedAt || punch.reviewRequired) continue;\n const start = instant(punch.startedAt);\n const end = instant(punch.endedAt);\n if (end < start) throw new Error('Invalid attendance interval.');\n let cursor = start;\n let unpaidMs = 0;\n const spans: [number, number][] = [];\n for (const pause of await breaks.list({\n where: { tenantId: actor.tenantId, punchId: punch.id },\n orderBy: 'started_at ASC',\n })) {\n if (!pause.endedAt)\n throw new Error('Closed attendance has an open break.');\n const left = instant(pause.startedAt);\n const right = instant(pause.endedAt);\n if (left < cursor || right < left || right > end)\n throw new Error('Invalid or overlapping attendance breaks.');\n if (!pause.paid) {\n spans.push([cursor, left]);\n unpaidMs += right - left;\n } else {\n spans.push([cursor, right]);\n }\n cursor = right;\n }\n spans.push([cursor, end]);\n const net = Math.round((end - start - unpaidMs) / 1000);\n if (integer(punch.durationSeconds) !== net)\n throw new Error('Attendance duration does not match its breaks.');\n // Round elapsed paid prefixes once, preserving conservation across breaks.\n let paidMs = 0;\n for (const [left, right] of spans) {\n const seconds =\n Math.round((paidMs + right - left) / 1000) - Math.round(paidMs / 1000);\n paidMs += right - left;\n const source = clipSource(\n {\n kind: 'attendance',\n id: String(punch.id),\n startsAt: new Date(left).toISOString(),\n endsAt: new Date(right).toISOString(),\n seconds,\n },\n period,\n );\n if (source?.seconds) sources.push(source);\n }\n }\n return { sources, linkedEntryIds };\n}\n"],"mappings":";;;AASA,eAAsB,kBACpB,IACA,OACA,QACqE;CACrE,MAAM,UAAU,MAAM,0BAA0B,OAAO,EAAE,GAAG,CAAC;CAC7D,MAAM,SAAS,MAAM,0BAA0B,OAAO,EAAE,GAAG,CAAC;CAC5D,MAAM,UAA4B,CAAC;CACnC,MAAM,iCAAiB,IAAI,IAAY;CACvC,KAAA,MAAW,SAAS,MAAM,QAAQ,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC,GAAG;EAG/D,IAAI,MAAM,oBAAoB,eAAe,IAAI,MAAM,kBAAkB;EACzE,IAAI,CAAC,MAAM,WAAW,MAAM,gBAAgB;EAC5C,MAAM,QAAQ,QAAQ,MAAM,SAAS;EACrC,MAAM,MAAM,QAAQ,MAAM,OAAO;EACjC,IAAI,MAAM,OAAO,MAAM,IAAI,MAAM,8BAA8B;EAC/D,IAAI,SAAS;EACb,IAAI,WAAW;EACf,MAAM,QAA4B,CAAC;EACnC,KAAA,MAAW,SAAS,MAAM,OAAO,KAAK;GACpC,OAAO;IAAE,UAAU,MAAM;IAAU,SAAS,MAAM;GAAG;GACrD,SAAS;EACX,CAAC,GAAG;GACF,IAAI,CAAC,MAAM,SACT,MAAM,IAAI,MAAM,sCAAsC;GACxD,MAAM,OAAO,QAAQ,MAAM,SAAS;GACpC,MAAM,QAAQ,QAAQ,MAAM,OAAO;GACnC,IAAI,OAAO,UAAU,QAAQ,QAAQ,QAAQ,KAC3C,MAAM,IAAI,MAAM,2CAA2C;GAC7D,IAAI,CAAC,MAAM,MAAM;IACf,MAAM,KAAK,CAAC,QAAQ,IAAI,CAAC;IACzB,YAAY,QAAQ;GACtB,OACE,MAAM,KAAK,CAAC,QAAQ,KAAK,CAAC;GAE5B,SAAS;EACX;EACA,MAAM,KAAK,CAAC,QAAQ,GAAG,CAAC;EACxB,MAAM,MAAM,KAAK,OAAO,MAAM,QAAQ,YAAY,GAAI;EACtD,IAAI,QAAQ,MAAM,eAAe,MAAM,KACrC,MAAM,IAAI,MAAM,gDAAgD;EAElE,IAAI,SAAS;EACb,KAAA,MAAW,CAAC,MAAM,UAAU,OAAO;GACjC,MAAM,UACJ,KAAK,OAAO,SAAS,QAAQ,QAAQ,GAAI,IAAI,KAAK,MAAM,SAAS,GAAI;GACvE,UAAU,QAAQ;GAClB,MAAM,SAAS,WACb;IACE,MAAM;IACN,IAAI,OAAO,MAAM,EAAE;IACnB,UAAU,IAAI,KAAK,IAAI,CAAA,CAAE,YAAY;IACrC,QAAQ,IAAI,KAAK,KAAK,CAAA,CAAE,YAAY;IACpC;GACF,GACA,MACF;GACA,IAAI,QAAQ,SAAS,QAAQ,KAAK,MAAM;EAC1C;CACF;CACA,OAAO;EAAE;EAAS;CAAe;AACnC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as parseTimesheetJson } from "./service-time-entry-
|
|
1
|
+
import { o as parseTimesheetJson } from "./service-time-entry-CBv-FF-W.js";
|
|
2
2
|
import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, isEmbeddedDatabase, isPostgresDatabase, smrt, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
|
|
3
3
|
import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
|
|
4
4
|
//#region src/attendance/models.ts
|
|
@@ -173,4 +173,4 @@ var AttendanceReplayCollection = class extends SmrtCollection {
|
|
|
173
173
|
//#endregion
|
|
174
174
|
export { AttendanceReplay as a, AttendancePunchCollection as i, AttendanceBreakCollection as n, AttendanceReplayCollection as o, AttendancePunch as r, AttendanceBreak as t };
|
|
175
175
|
|
|
176
|
-
//# sourceMappingURL=models-
|
|
176
|
+
//# sourceMappingURL=models-zU8Um7c2.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models-B0NOh8I3.js","names":[],"sources":["../../src/attendance/models.ts"],"sourcesContent":["import {\n crossPackageRef,\n field,\n foreignKey,\n isEmbeddedDatabase,\n isPostgresDatabase,\n SmrtCollection,\n SmrtObject,\n smrt,\n withEmbeddedWriteTransaction,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport { parseTimesheetJson } from '../models/json.js';\n\n// Punch closes and break writes share one atomic parent boundary, including direct ORM saves.\nasync function saveWithPunchLock<T extends SmrtObject>(\n model: T,\n punchId: string | null | undefined,\n tenant: string,\n save: () => Promise<T>,\n): Promise<T> {\n return withEmbeddedWriteTransaction(\n model.db,\n isEmbeddedDatabase(model.db),\n (db) =>\n model.withDatabase(db, async () => {\n if (punchId && isPostgresDatabase(db))\n await db.query(\n 'SELECT id FROM attendance_punches WHERE id = $1 AND tenant_id = $2 FOR UPDATE',\n punchId,\n tenant,\n );\n return save();\n }),\n true,\n );\n}\n\n/** A tenant-scoped interval of attendance, independent of billable work. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'attendance_punches',\n indexes: [\n {\n name: 'attendance_punches_open_owner_key',\n columns: ['tenantId', 'profileId', 'openSlot'],\n unique: true,\n },\n ],\n api: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n})\nexport class AttendancePunch extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId() tenantId = '';\n @crossPackageRef('@happyvertical/smrt-profiles:Profile') profileId = '';\n startedAt: Date = new Date();\n endedAt: Date | null = null;\n durationSeconds = 0;\n unpaidBreakSeconds = 0;\n reviewRequired = false;\n recordedOffline = false;\n // Resolve the active table family, including smrt-support's same-named subtype.\n @foreignKey('ServiceTimeEntry', { nullable: true }) serviceTimeEntryId:\n | string\n | null = null;\n /** Unique while open; closed rows retain independent non-null slots on every dialect. */\n @field({ required: true }) openSlot = '';\n\n override async save(): Promise<this> {\n return saveWithPunchLock(this, this.id, this.tenantId, () =>\n this.saveConsistent(),\n );\n }\n\n private async saveConsistent(): Promise<this> {\n if (!this.tenantId || !this.profileId)\n throw new Error('Attendance requires tenantId and profileId.');\n if (\n !Number.isFinite(this.startedAt?.getTime()) ||\n (this.endedAt &&\n (!Number.isFinite(this.endedAt.getTime()) ||\n this.endedAt < this.startedAt))\n )\n throw new Error('Invalid attendance interval.');\n if (\n ![this.durationSeconds, this.unpaidBreakSeconds].every(\n (value) => Number.isSafeInteger(value) && value >= 0,\n )\n )\n throw new Error(\n 'Attendance durations must be nonnegative safe integer seconds.',\n );\n const prior = this.id\n ? await this.db.get(this.tableName, { id: this.id })\n : null;\n if (\n prior &&\n (prior.tenant_id !== this.tenantId ||\n prior.profile_id !== this.profileId ||\n new Date(prior.started_at as string).getTime() !==\n this.startedAt.getTime())\n )\n throw new Error('Attendance ownership and start are immutable.');\n if (\n prior?.ended_at &&\n (new Date(prior.ended_at as string).getTime() !==\n this.endedAt?.getTime() ||\n Number(prior.duration_seconds) !== this.durationSeconds ||\n Number(prior.unpaid_break_seconds) !== this.unpaidBreakSeconds)\n )\n throw new Error('Closed attendance evidence is immutable.');\n if (this.id && this.endedAt) {\n const breaks = await this.db.list('attendance_breaks', {\n tenant_id: this.tenantId,\n punch_id: this.id,\n });\n if (\n breaks.some(\n (item) =>\n !item.ended_at ||\n new Date(item.started_at as string).getTime() <\n this.startedAt.getTime() ||\n new Date(item.ended_at as string).getTime() >\n this.endedAt!.getTime(),\n )\n )\n throw new Error('Break must be inside its tenant-owned punch.');\n }\n this.id ??= crypto.randomUUID();\n this.openSlot = this.endedAt ? JSON.stringify(['closed', this.id]) : 'open';\n return super.save();\n }\n}\n\n/** A paid or unpaid break inside one attendance punch. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'attendance_breaks',\n indexes: [\n {\n name: 'attendance_breaks_open_punch_key',\n columns: ['tenantId', 'punchId', 'openSlot'],\n unique: true,\n },\n ],\n api: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n})\nexport class AttendanceBreak extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId() tenantId = '';\n @foreignKey(AttendancePunch) punchId = '';\n startedAt: Date = new Date();\n endedAt: Date | null = null;\n paid = false;\n @field({ required: true }) openSlot = '';\n\n override async save(): Promise<this> {\n return saveWithPunchLock(this, this.punchId, this.tenantId, () =>\n this.saveConsistent(),\n );\n }\n\n private async saveConsistent(): Promise<this> {\n if (!this.tenantId || !this.punchId || typeof this.paid !== 'boolean')\n throw new Error('Invalid attendance break.');\n if (\n !Number.isFinite(this.startedAt?.getTime()) ||\n (this.endedAt &&\n (!Number.isFinite(this.endedAt.getTime()) ||\n this.endedAt < this.startedAt))\n )\n throw new Error('Invalid attendance break interval.');\n const parent = await this.db.get('attendance_punches', {\n id: this.punchId,\n tenant_id: this.tenantId,\n });\n if (\n !parent ||\n this.startedAt.getTime() <\n new Date(parent.started_at as string).getTime() ||\n (parent.ended_at &&\n (!this.endedAt ||\n this.endedAt.getTime() >\n new Date(parent.ended_at as string).getTime()))\n )\n throw new Error('Break must be inside its tenant-owned punch.');\n const prior = this.id\n ? await this.db.get(this.tableName, { id: this.id })\n : null;\n if (\n prior &&\n (prior.tenant_id !== this.tenantId ||\n prior.punch_id !== this.punchId ||\n new Date(prior.started_at as string).getTime() !==\n this.startedAt.getTime() ||\n Boolean(prior.paid) !== this.paid)\n )\n throw new Error('Break ownership, start and pay policy are immutable.');\n if (\n prior?.ended_at &&\n new Date(prior.ended_at as string).getTime() !== this.endedAt?.getTime()\n )\n throw new Error('Closed breaks are immutable.');\n this.id ??= crypto.randomUUID();\n this.openSlot = this.endedAt ? JSON.stringify(['closed', this.id]) : 'open';\n return super.save();\n }\n}\n\n/** Durable success or conflict; conflicts do not mutate attendance. */\nexport interface AttendanceReplayOutcome {\n clientId: string;\n punchId?: string;\n error?: string;\n}\n\nfunction validReplayOutcome(value: unknown): value is AttendanceReplayOutcome {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const result = value as Record<string, unknown>;\n return (\n typeof result.clientId === 'string' &&\n result.clientId.length > 0 &&\n ((typeof result.punchId === 'string' &&\n result.punchId.length > 0 &&\n result.error === undefined) ||\n (typeof result.error === 'string' &&\n result.error.length > 0 &&\n result.punchId === undefined))\n );\n}\n\n/** Durable offline-tap outcome; never exposed through generated operations. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'attendance_replays',\n sensitive: true,\n api: { include: [] },\n cli: { include: [] },\n mcp: { include: [] },\n})\nexport class AttendanceReplay extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId() tenantId = '';\n @field({ required: true, unique: true }) replayKey = '';\n @field({ type: 'text' }) content = '';\n @field({ type: 'text' }) outcome = '{}';\n\n /** Read a durable receipt, failing closed rather than re-executing a corrupt tap. */\n getOutcome(): AttendanceReplayOutcome {\n const value = parseTimesheetJson<unknown>(this.outcome, null);\n if (!validReplayOutcome(value))\n throw new Error('Invalid stored attendance replay outcome.');\n return value;\n }\n\n /** Store the JSON representation of a validated success or refusal. */\n setOutcome(value: AttendanceReplayOutcome): void {\n if (!validReplayOutcome(value))\n throw new Error('Invalid attendance replay outcome.');\n this.outcome = JSON.stringify(value);\n }\n\n override async save(): Promise<this> {\n this.getOutcome();\n return super.save();\n }\n}\n\n/** Punches for attendance and period rollup. */\nexport class AttendancePunchCollection extends SmrtCollection<AttendancePunch> {\n static readonly _itemClass = AttendancePunch;\n}\n/** Break intervals for net duration and period clipping. */\nexport class AttendanceBreakCollection extends SmrtCollection<AttendanceBreak> {\n static readonly _itemClass = AttendanceBreak;\n}\n/** Internal durable replay ledger. */\nexport class AttendanceReplayCollection extends SmrtCollection<AttendanceReplay> {\n static readonly _itemClass = AttendanceReplay;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"models-zU8Um7c2.js","names":[],"sources":["../../src/attendance/models.ts"],"sourcesContent":["import {\n crossPackageRef,\n field,\n foreignKey,\n isEmbeddedDatabase,\n isPostgresDatabase,\n SmrtCollection,\n SmrtObject,\n smrt,\n withEmbeddedWriteTransaction,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport { parseTimesheetJson } from '../models/json.js';\n\n// Punch closes and break writes share one atomic parent boundary, including direct ORM saves.\nasync function saveWithPunchLock<T extends SmrtObject>(\n model: T,\n punchId: string | null | undefined,\n tenant: string,\n save: () => Promise<T>,\n): Promise<T> {\n return withEmbeddedWriteTransaction(\n model.db,\n isEmbeddedDatabase(model.db),\n (db) =>\n model.withDatabase(db, async () => {\n if (punchId && isPostgresDatabase(db))\n await db.query(\n 'SELECT id FROM attendance_punches WHERE id = $1 AND tenant_id = $2 FOR UPDATE',\n punchId,\n tenant,\n );\n return save();\n }),\n true,\n );\n}\n\n/** A tenant-scoped interval of attendance, independent of billable work. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'attendance_punches',\n indexes: [\n {\n name: 'attendance_punches_open_owner_key',\n columns: ['tenantId', 'profileId', 'openSlot'],\n unique: true,\n },\n ],\n api: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n})\nexport class AttendancePunch extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId() tenantId = '';\n @crossPackageRef('@happyvertical/smrt-profiles:Profile') profileId = '';\n startedAt: Date = new Date();\n endedAt: Date | null = null;\n durationSeconds = 0;\n unpaidBreakSeconds = 0;\n reviewRequired = false;\n recordedOffline = false;\n // Resolve the active table family, including smrt-support's same-named subtype.\n @foreignKey('ServiceTimeEntry', { nullable: true }) serviceTimeEntryId:\n | string\n | null = null;\n /** Unique while open; closed rows retain independent non-null slots on every dialect. */\n @field({ required: true }) openSlot = '';\n\n override async save(): Promise<this> {\n return saveWithPunchLock(this, this.id, this.tenantId, () =>\n this.saveConsistent(),\n );\n }\n\n private async saveConsistent(): Promise<this> {\n if (!this.tenantId || !this.profileId)\n throw new Error('Attendance requires tenantId and profileId.');\n if (\n !Number.isFinite(this.startedAt?.getTime()) ||\n (this.endedAt &&\n (!Number.isFinite(this.endedAt.getTime()) ||\n this.endedAt < this.startedAt))\n )\n throw new Error('Invalid attendance interval.');\n if (\n ![this.durationSeconds, this.unpaidBreakSeconds].every(\n (value) => Number.isSafeInteger(value) && value >= 0,\n )\n )\n throw new Error(\n 'Attendance durations must be nonnegative safe integer seconds.',\n );\n const prior = this.id\n ? await this.db.get(this.tableName, { id: this.id })\n : null;\n if (\n prior &&\n (prior.tenant_id !== this.tenantId ||\n prior.profile_id !== this.profileId ||\n new Date(prior.started_at as string).getTime() !==\n this.startedAt.getTime())\n )\n throw new Error('Attendance ownership and start are immutable.');\n if (\n prior?.ended_at &&\n (new Date(prior.ended_at as string).getTime() !==\n this.endedAt?.getTime() ||\n Number(prior.duration_seconds) !== this.durationSeconds ||\n Number(prior.unpaid_break_seconds) !== this.unpaidBreakSeconds)\n )\n throw new Error('Closed attendance evidence is immutable.');\n if (this.id && this.endedAt) {\n const breaks = await this.db.list('attendance_breaks', {\n tenant_id: this.tenantId,\n punch_id: this.id,\n });\n if (\n breaks.some(\n (item) =>\n !item.ended_at ||\n new Date(item.started_at as string).getTime() <\n this.startedAt.getTime() ||\n new Date(item.ended_at as string).getTime() >\n this.endedAt!.getTime(),\n )\n )\n throw new Error('Break must be inside its tenant-owned punch.');\n }\n this.id ??= crypto.randomUUID();\n this.openSlot = this.endedAt ? JSON.stringify(['closed', this.id]) : 'open';\n return super.save();\n }\n}\n\n/** A paid or unpaid break inside one attendance punch. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'attendance_breaks',\n indexes: [\n {\n name: 'attendance_breaks_open_punch_key',\n columns: ['tenantId', 'punchId', 'openSlot'],\n unique: true,\n },\n ],\n api: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n})\nexport class AttendanceBreak extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId() tenantId = '';\n @foreignKey(AttendancePunch) punchId = '';\n startedAt: Date = new Date();\n endedAt: Date | null = null;\n paid = false;\n @field({ required: true }) openSlot = '';\n\n override async save(): Promise<this> {\n return saveWithPunchLock(this, this.punchId, this.tenantId, () =>\n this.saveConsistent(),\n );\n }\n\n private async saveConsistent(): Promise<this> {\n if (!this.tenantId || !this.punchId || typeof this.paid !== 'boolean')\n throw new Error('Invalid attendance break.');\n if (\n !Number.isFinite(this.startedAt?.getTime()) ||\n (this.endedAt &&\n (!Number.isFinite(this.endedAt.getTime()) ||\n this.endedAt < this.startedAt))\n )\n throw new Error('Invalid attendance break interval.');\n const parent = await this.db.get('attendance_punches', {\n id: this.punchId,\n tenant_id: this.tenantId,\n });\n if (\n !parent ||\n this.startedAt.getTime() <\n new Date(parent.started_at as string).getTime() ||\n (parent.ended_at &&\n (!this.endedAt ||\n this.endedAt.getTime() >\n new Date(parent.ended_at as string).getTime()))\n )\n throw new Error('Break must be inside its tenant-owned punch.');\n const prior = this.id\n ? await this.db.get(this.tableName, { id: this.id })\n : null;\n if (\n prior &&\n (prior.tenant_id !== this.tenantId ||\n prior.punch_id !== this.punchId ||\n new Date(prior.started_at as string).getTime() !==\n this.startedAt.getTime() ||\n Boolean(prior.paid) !== this.paid)\n )\n throw new Error('Break ownership, start and pay policy are immutable.');\n if (\n prior?.ended_at &&\n new Date(prior.ended_at as string).getTime() !== this.endedAt?.getTime()\n )\n throw new Error('Closed breaks are immutable.');\n this.id ??= crypto.randomUUID();\n this.openSlot = this.endedAt ? JSON.stringify(['closed', this.id]) : 'open';\n return super.save();\n }\n}\n\n/** Durable success or conflict; conflicts do not mutate attendance. */\nexport interface AttendanceReplayOutcome {\n clientId: string;\n punchId?: string;\n error?: string;\n}\n\nfunction validReplayOutcome(value: unknown): value is AttendanceReplayOutcome {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const result = value as Record<string, unknown>;\n return (\n typeof result.clientId === 'string' &&\n result.clientId.length > 0 &&\n ((typeof result.punchId === 'string' &&\n result.punchId.length > 0 &&\n result.error === undefined) ||\n (typeof result.error === 'string' &&\n result.error.length > 0 &&\n result.punchId === undefined))\n );\n}\n\n/** Durable offline-tap outcome; never exposed through generated operations. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'attendance_replays',\n sensitive: true,\n api: { include: [] },\n cli: { include: [] },\n mcp: { include: [] },\n})\nexport class AttendanceReplay extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId() tenantId = '';\n @field({ required: true, unique: true }) replayKey = '';\n @field({ type: 'text' }) content = '';\n @field({ type: 'text' }) outcome = '{}';\n\n /** Read a durable receipt, failing closed rather than re-executing a corrupt tap. */\n getOutcome(): AttendanceReplayOutcome {\n const value = parseTimesheetJson<unknown>(this.outcome, null);\n if (!validReplayOutcome(value))\n throw new Error('Invalid stored attendance replay outcome.');\n return value;\n }\n\n /** Store the JSON representation of a validated success or refusal. */\n setOutcome(value: AttendanceReplayOutcome): void {\n if (!validReplayOutcome(value))\n throw new Error('Invalid attendance replay outcome.');\n this.outcome = JSON.stringify(value);\n }\n\n override async save(): Promise<this> {\n this.getOutcome();\n return super.save();\n }\n}\n\n/** Punches for attendance and period rollup. */\nexport class AttendancePunchCollection extends SmrtCollection<AttendancePunch> {\n static readonly _itemClass = AttendancePunch;\n}\n/** Break intervals for net duration and period clipping. */\nexport class AttendanceBreakCollection extends SmrtCollection<AttendanceBreak> {\n static readonly _itemClass = AttendanceBreak;\n}\n/** Internal durable replay ledger. */\nexport class AttendanceReplayCollection extends SmrtCollection<AttendanceReplay> {\n static readonly _itemClass = AttendanceReplay;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as ServiceTimeEntryCollection } from "./service-time-entry-
|
|
1
|
+
import { i as ServiceTimeEntryCollection } from "./service-time-entry-CBv-FF-W.js";
|
|
2
2
|
import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, isEmbeddedDatabase, isPostgresDatabase, smrt, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
|
|
3
3
|
import { TenantScoped, getTenantId, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -272,7 +272,7 @@ var PeriodRollupService = class {
|
|
|
272
272
|
let sources = [];
|
|
273
273
|
let linkedEntryIds = /* @__PURE__ */ new Set();
|
|
274
274
|
if (this.options.attendance) {
|
|
275
|
-
const { attendanceSources } = await import("./attendance-
|
|
275
|
+
const { attendanceSources } = await import("./attendance-BVeStEBv.js");
|
|
276
276
|
({sources, linkedEntryIds} = await attendanceSources(db, this.actor, period));
|
|
277
277
|
}
|
|
278
278
|
const evidence = await (await ServiceTimeEntryCollection.create({ db })).list({ where: {
|
|
@@ -432,4 +432,4 @@ var PeriodRollupService = class {
|
|
|
432
432
|
//#endregion
|
|
433
433
|
export { PeriodTimecard as a, TimecardAdjustmentCollection as c, integer as i, clipSource as n, PeriodTimecardCollection as o, instant as r, TimecardAdjustment as s, PeriodRollupService as t };
|
|
434
434
|
|
|
435
|
-
//# sourceMappingURL=rollup-
|
|
435
|
+
//# sourceMappingURL=rollup-U1IO_CMA.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rollup-2--s0AyP.js","names":["id"],"sources":["../../src/rollup/write.ts","../../src/rollup/models.ts","../../src/rollup/calculation.ts","../../src/rollup/service.ts"],"sourcesContent":["import type { SmrtObject } from '@happyvertical/smrt-core';\n\n// Per-instance capability, deliberately absent from the package export surface.\nconst permitted = new WeakSet<SmrtObject>();\nexport function assertRollupWrite(object: SmrtObject): void {\n if (!permitted.has(object))\n throw new Error(\n 'Timecards and adjustments are writable only through PeriodRollupService.',\n );\n}\nexport async function persistRollup<T extends SmrtObject>(\n object: T,\n): Promise<T> {\n permitted.add(object);\n try {\n await object.save();\n return object;\n } finally {\n permitted.delete(object);\n }\n}\n","import {\n crossPackageRef,\n field,\n foreignKey,\n SmrtCollection,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { TimecardSource } from './types.js';\nimport { assertRollupWrite } from './write.js';\n\n/** One person's immutable-on-confirmation period snapshot, in integer seconds. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'period_timecards',\n sensitive: true,\n api: { include: [] },\n cli: { include: [] },\n mcp: { include: [] },\n})\nexport class PeriodTimecard extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId() tenantId = '';\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', {\n onDelete: 'RESTRICT',\n })\n profileId = '';\n startsAt: Date = new Date(0);\n endsAt: Date = new Date(0);\n timezone = '';\n rulesVersion = '';\n @field({ type: 'text' }) rulesSnapshot = '{}';\n @field({ type: 'text' }) sourceSnapshot = '[]';\n regularSeconds = 0;\n overtimeSeconds = 0;\n totalSeconds = 0;\n @field({ type: 'text' }) status: 'open' | 'confirmed' = 'open';\n confirmedAt: Date | null = null;\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', {\n nullable: true,\n onDelete: 'RESTRICT',\n })\n confirmedByProfileId: string | null = null;\n\n /** Return a detached source snapshot; corrupt persisted JSON fails closed. */\n getSources(): TimecardSource[] {\n const result: unknown = JSON.parse(this.sourceSnapshot);\n if (!Array.isArray(result))\n throw new Error('Invalid timecard source snapshot.');\n return result as TimecardSource[];\n }\n /** Return the consumer policy frozen with this rollup. */\n getRules(): Record<string, unknown> {\n const result: unknown = JSON.parse(this.rulesSnapshot);\n if (!result || typeof result !== 'object' || Array.isArray(result))\n throw new Error('Invalid timecard rules snapshot.');\n return result as Record<string, unknown>;\n }\n override async save(): Promise<this> {\n assertRollupWrite(this);\n const prior = this.id\n ? await this.db.get(this.tableName, { id: this.id })\n : null;\n if (prior?.status === 'confirmed')\n throw new Error(\n 'Confirmed timecards are immutable; append an adjustment.',\n );\n if (\n prior &&\n (prior.tenant_id !== this.tenantId ||\n prior.profile_id !== this.profileId ||\n new Date(prior.starts_at as string).getTime() !==\n this.startsAt.getTime())\n )\n throw new Error('Timecard identity is immutable.');\n return (await super.save()) as this;\n }\n override async delete(): Promise<void> {\n throw new Error('Timecard history cannot be deleted.');\n }\n}\n\n/** Attributed append-only signed seconds; the confirmed base card never changes. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'timecard_adjustments',\n sensitive: true,\n api: { include: [] },\n cli: { include: [] },\n mcp: { include: [] },\n})\nexport class TimecardAdjustment extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId() tenantId = '';\n @foreignKey(PeriodTimecard, { onDelete: 'RESTRICT' }) timecardId = '';\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', {\n onDelete: 'RESTRICT',\n })\n profileId = '';\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', {\n onDelete: 'RESTRICT',\n })\n actorProfileId = '';\n operationId = '';\n regularSeconds = 0;\n overtimeSeconds = 0;\n reason = '';\n override async save(): Promise<this> {\n assertRollupWrite(this);\n if (this.id && (await this.db.get(this.tableName, { id: this.id })))\n throw new Error('Timecard adjustments are append-only.');\n this.requireInsertOnSave();\n return (await super.save()) as this;\n }\n override async delete(): Promise<void> {\n throw new Error('Timecard adjustments cannot be deleted.');\n }\n}\n/** Scoped persistence collection; mutations must use PeriodRollupService. */\nexport class PeriodTimecardCollection extends SmrtCollection<PeriodTimecard> {\n static readonly _itemClass = PeriodTimecard;\n}\n/** Scoped append-only adjustment collection. */\nexport class TimecardAdjustmentCollection extends SmrtCollection<TimecardAdjustment> {\n static readonly _itemClass = TimecardAdjustment;\n}\n","import type { TimecardPeriod, TimecardSource } from './types.js';\n\nexport function integer(value: number, signed = false): number {\n if (!Number.isSafeInteger(value) || (!signed && value < 0))\n throw new Error('Timecard seconds must be safe integers and nonnegative.');\n return value;\n}\nexport function instant(value: Date): number {\n if (!(value instanceof Date) || !Number.isFinite(value.getTime()))\n throw new Error('Timecard timestamps must be valid dates.');\n return value.getTime();\n}\nexport function validatePeriod(\n value: TimecardPeriod,\n at: Date,\n): TimecardPeriod {\n if (\n !value ||\n instant(value.startsAt) > instant(at) ||\n instant(value.endsAt) <= instant(at) ||\n instant(value.startsAt) >= instant(value.endsAt)\n )\n throw new Error('Resolver period must contain the requested instant.');\n if (!value.timezone || !value.version?.trim())\n throw new Error('Resolver must supply a timezone and policy version.');\n new Intl.DateTimeFormat('en', { timeZone: value.timezone });\n if (\n !value.rules ||\n typeof value.rules !== 'object' ||\n Array.isArray(value.rules)\n )\n throw new Error('Resolver rules must be a JSON object.');\n // JSON round-trip detaches consumer-owned state; fail rather than silently lose non-JSON values.\n const json = JSON.stringify(value.rules, (_key, item) => {\n if (\n typeof item === 'function' ||\n typeof item === 'undefined' ||\n typeof item === 'symbol' ||\n (typeof item === 'number' && !Number.isFinite(item))\n )\n throw new Error('Resolver rules must be JSON serializable.');\n return item;\n });\n const rules: unknown = JSON.parse(json);\n if (!rules || typeof rules !== 'object' || Array.isArray(rules))\n throw new Error('Resolver rules must serialize to a JSON object.');\n return {\n startsAt: new Date(value.startsAt),\n endsAt: new Date(value.endsAt),\n timezone: value.timezone,\n version: value.version,\n rules: rules as Record<string, unknown>,\n };\n}\n/** Prefix allocation preserves integer seconds across adjacent half-open periods. */\nexport function clipSource(\n source: TimecardSource,\n period: TimecardPeriod,\n): TimecardSource | null {\n const start = instant(new Date(source.startsAt));\n const end = instant(new Date(source.endsAt));\n integer(source.seconds);\n if (end < start) throw new Error('Invalid source interval.');\n const left = Math.max(start, period.startsAt.getTime());\n const right = Math.min(end, period.endsAt.getTime());\n if (start === end)\n return start >= period.startsAt.getTime() && start < period.endsAt.getTime()\n ? { ...source }\n : null;\n if (left >= right) return null;\n const span = integer(end - start);\n const prefix = (elapsed: number) =>\n Number((BigInt(source.seconds) * BigInt(integer(elapsed))) / BigInt(span));\n const seconds = prefix(right - start) - prefix(left - start);\n return {\n ...source,\n startsAt: new Date(left).toISOString(),\n endsAt: new Date(right).toISOString(),\n seconds: integer(seconds),\n };\n}\n","import { createHash } from 'node:crypto';\nimport {\n isEmbeddedDatabase,\n isPostgresDatabase,\n withEmbeddedWriteTransaction,\n} from '@happyvertical/smrt-core';\nimport { getTenantId, withTenant } from '@happyvertical/smrt-tenancy';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ServiceTimeEntryCollection } from '../models/service-time-entry.js';\nimport { clipSource, instant, integer, validatePeriod } from './calculation.js';\nimport {\n PeriodTimecard,\n PeriodTimecardCollection,\n TimecardAdjustment,\n TimecardAdjustmentCollection,\n} from './models.js';\nimport type {\n PeriodRulesResolver,\n TimecardActor,\n TimecardAdjustmentInput,\n TimecardSource,\n} from './types.js';\nimport { persistRollup } from './write.js';\n\nfunction identity(parts: string[]): string {\n const bytes = createHash('sha256').update(JSON.stringify(parts)).digest();\n bytes[6] = (bytes[6] & 15) | 80;\n bytes[8] = (bytes[8] & 63) | 128;\n const hex = bytes.subarray(0, 16).toString('hex');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\nfunction adjustedSeconds(\n base: number,\n rows: TimecardAdjustment[],\n field: 'regularSeconds' | 'overtimeSeconds',\n): number {\n const total = rows.reduce(\n (sum, row) => sum + BigInt(integer(row[field], true)),\n BigInt(integer(base)),\n );\n return integer(Number(total));\n}\n\n/**\n * Server-side, trusted tenant/person capability. Consumers authorize this scope\n * before construction. Reads and writes reject conflicting ambient tenancy.\n * SQLite and PostgreSQL are supported; policy and payroll remain consumer-owned.\n */\nexport class PeriodRollupService {\n constructor(\n private readonly db: DatabaseInterface,\n private readonly actor: TimecardActor,\n private readonly resolver: PeriodRulesResolver,\n private readonly options: {\n attendance?: boolean;\n actorProfileId?: string;\n } = {},\n ) {\n if (!actor.tenantId?.trim() || !actor.profileId?.trim())\n throw new Error('Timecards require a trusted tenant/profile actor.');\n this.actor = Object.freeze({\n tenantId: this.identity(actor.tenantId),\n profileId: this.identity(actor.profileId),\n });\n if (options.actorProfileId !== undefined && !options.actorProfileId.trim())\n throw new Error('Actor attribution must be a nonempty profile id.');\n this.options = {\n attendance: options.attendance,\n actorProfileId: this.identity(\n options.actorProfileId ?? this.actor.profileId,\n ),\n };\n }\n\n private identity(value: string): string {\n if (!isPostgresDatabase(this.db)) return value;\n const hex = value.replace(/[{}-]/g, '').toLowerCase();\n if (!/^[0-9a-f]{32}$/.test(hex))\n throw new Error('PostgreSQL timecard identities must be UUIDs.');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n }\n private scope<T>(run: () => Promise<T>): Promise<T> {\n const active = getTenantId();\n if (active && this.identity(active) !== this.actor.tenantId)\n throw new Error('Timecard actor differs from active tenant.');\n return withTenant({ tenantId: this.actor.tenantId }, run);\n }\n private transact<T>(run: (db: DatabaseInterface) => Promise<T>): Promise<T> {\n return this.scope(() =>\n withEmbeddedWriteTransaction(\n this.db,\n isEmbeddedDatabase(this.db),\n async (db) => {\n if (isPostgresDatabase(db))\n await db.query(\n 'SELECT pg_advisory_xact_lock(hashtextextended($1, 0))',\n JSON.stringify([\n 'smrt.rollup',\n this.actor.tenantId,\n this.actor.profileId,\n ]),\n );\n if (this.options.attendance && isPostgresDatabase(db))\n await db.query(\n 'SELECT pg_advisory_xact_lock(hashtextextended($1, 0))',\n JSON.stringify([\n 'smrt.attendance',\n this.actor.tenantId,\n this.actor.profileId,\n ]),\n );\n return run(db);\n },\n true,\n ),\n );\n }\n private async card(\n db: DatabaseInterface,\n id: string,\n ): Promise<PeriodTimecard> {\n const collection = await PeriodTimecardCollection.create({ db });\n const [card] = await collection.list({\n where: { ...this.actor, id },\n limit: 1,\n });\n if (!card) throw new Error('Timecard not found in actor scope.');\n return card;\n }\n /** Scoped read on the root executor, including after transaction completion. */\n async get(id: string): Promise<PeriodTimecard> {\n return this.scope(() => this.card(this.db, id));\n }\n /** Rebuild one open card from a consistent transaction-bound source snapshot. */\n async rollup(at: Date): Promise<PeriodTimecard> {\n at = new Date(instant(at));\n const id = await this.transact(async (db) => {\n const period = validatePeriod(\n await this.resolver.periodFor(new Date(at), this.actor),\n at,\n );\n const id = identity([\n 'smrt.period',\n this.actor.tenantId,\n this.actor.profileId,\n period.startsAt.toISOString(),\n ]);\n const collection = await PeriodTimecardCollection.create({ db });\n const [prior] = await collection.list({\n where: { ...this.actor, id },\n limit: 1,\n });\n if (prior?.status === 'confirmed')\n throw new Error(\n 'Confirmed timecards are immutable; append an adjustment.',\n );\n // A policy change must not create overlapping cards for the same person.\n for (const other of await collection.list({ where: { ...this.actor } })) {\n if (\n other.id !== id &&\n other.startsAt < period.endsAt &&\n other.endsAt > period.startsAt\n )\n throw new Error('Resolver period overlaps an existing timecard.');\n }\n let sources: TimecardSource[] = [];\n let linkedEntryIds = new Set<string>();\n if (this.options.attendance) {\n const { attendanceSources } = await import('./attendance.js');\n ({ sources, linkedEntryIds } = await attendanceSources(\n db,\n this.actor,\n period,\n ));\n }\n const entries = await ServiceTimeEntryCollection.create({ db });\n const evidence = await entries.list({\n where: {\n tenantId: this.actor.tenantId,\n participantProfileId: this.actor.profileId,\n participantKind: 'human',\n },\n });\n const byId = new Map(evidence.map((entry) => [String(entry.id), entry]));\n const approvedRoots = new Set<string>();\n for (const entry of evidence) {\n if (entry.status !== 'approved') continue;\n let linked = linkedEntryIds.has(String(entry.id));\n let ancestor = entry;\n const seen = new Set([String(entry.id)]);\n while (ancestor.correctionOfId) {\n const parentId = ancestor.correctionOfId;\n if (seen.has(parentId))\n throw new Error('Cyclic evidence correction chain.');\n seen.add(parentId);\n const parent = byId.get(parentId);\n if (!parent || parent.status !== 'corrected')\n throw new Error(\n 'Correction requires corrected evidence in the same actor scope.',\n );\n linked ||= linkedEntryIds.has(parentId);\n ancestor = parent;\n }\n const rootId = String(ancestor.id);\n if (approvedRoots.has(rootId))\n throw new Error('Ambiguous approved evidence correction chain.');\n approvedRoots.add(rootId);\n if (linked) continue;\n // Duration-only manual evidence belongs to its approval instant. A\n // bounded interval allocates its actual duration by elapsed fraction.\n const start = entry.startedAt ?? entry.approvedAt;\n if (!start)\n throw new Error(\n 'Approved evidence needs a start or approval timestamp.',\n );\n // Hours-only sources are unbounded and attributed at approval. An\n // unsupported quantity in another period must not block this one.\n if (\n entry.durationSeconds === null &&\n (instant(start) < period.startsAt.getTime() ||\n instant(start) >= period.endsAt.getTime())\n )\n continue;\n const end = entry.startedAt && entry.endedAt ? entry.endedAt : start;\n const source = clipSource(\n {\n kind: 'entry',\n id: String(entry.id),\n startsAt: new Date(instant(start)).toISOString(),\n endsAt: new Date(instant(end)).toISOString(),\n seconds: integer(entry.requireDurationSeconds()),\n },\n period,\n );\n if (source?.seconds) sources.push(source);\n }\n sources.sort(\n (a, b) =>\n a.startsAt.localeCompare(b.startsAt) ||\n a.kind.localeCompare(b.kind) ||\n a.id.localeCompare(b.id),\n );\n const totalSeconds = sources.reduce(\n (sum, source) => integer(sum + source.seconds),\n 0,\n );\n // Resolver receives detached snapshots so it cannot alter persisted evidence.\n const classified = await this.resolver.classify(\n structuredClone(period),\n structuredClone(sources),\n this.actor,\n );\n const regularSeconds = integer(classified?.regularSeconds);\n const overtimeSeconds = integer(classified?.overtimeSeconds);\n if (integer(regularSeconds + overtimeSeconds) !== totalSeconds)\n throw new Error('Resolver classification must conserve total seconds.');\n const card =\n prior ??\n (await new PeriodTimecard({ db, id, _skipLoad: true }).initialize());\n if (!prior) card.requireInsertOnSave();\n Object.assign(card, this.actor, {\n startsAt: period.startsAt,\n endsAt: period.endsAt,\n timezone: period.timezone,\n rulesVersion: period.version,\n rulesSnapshot: JSON.stringify(period.rules),\n sourceSnapshot: JSON.stringify(sources),\n regularSeconds,\n overtimeSeconds,\n totalSeconds,\n });\n await persistRollup(card);\n return id;\n });\n return this.get(id);\n }\n /** Confirm the current snapshot; repeated confirmation is idempotent. */\n async confirm(id: string): Promise<PeriodTimecard> {\n await this.transact(async (db) => {\n const card = await this.card(db, id);\n if (card.status === 'confirmed') return;\n card.status = 'confirmed';\n card.confirmedAt = new Date();\n card.confirmedByProfileId = this.options.actorProfileId!;\n await persistRollup(card);\n });\n return this.get(id);\n }\n /** Append a correction. An operation key retry must have exactly the same payload. */\n async adjust(\n id: string,\n input: TimecardAdjustmentInput,\n ): Promise<TimecardAdjustment> {\n id = this.identity(id);\n input = {\n operationId: input.operationId,\n reason: input.reason,\n regularSeconds: input.regularSeconds,\n overtimeSeconds: input.overtimeSeconds,\n };\n integer(input.regularSeconds, true);\n integer(input.overtimeSeconds, true);\n if (!input.operationId?.trim() || !input.reason?.trim())\n throw new Error('Adjustment requires an operationId and reason.');\n const adjustmentId = identity([\n 'smrt.adjustment',\n this.actor.tenantId,\n this.actor.profileId,\n id,\n input.operationId,\n ]);\n await this.transact(async (db) => {\n const card = await this.card(db, id);\n if (card.status !== 'confirmed')\n throw new Error('Adjustments require a confirmed timecard.');\n const collection = await TimecardAdjustmentCollection.create({ db });\n const rows = await collection.list({\n where: { ...this.actor, timecardId: id },\n });\n const prior = rows.find((row) => row.id === adjustmentId);\n if (prior) {\n if (\n prior.regularSeconds !== input.regularSeconds ||\n prior.overtimeSeconds !== input.overtimeSeconds ||\n prior.reason !== input.reason ||\n prior.actorProfileId !== this.options.actorProfileId\n )\n throw new Error(\n 'Adjustment operationId reused with a different payload.',\n );\n return;\n }\n const regular = adjustedSeconds(\n card.regularSeconds,\n rows,\n 'regularSeconds',\n );\n const overtime = adjustedSeconds(\n card.overtimeSeconds,\n rows,\n 'overtimeSeconds',\n );\n integer(\n integer(regular + input.regularSeconds) +\n integer(overtime + input.overtimeSeconds),\n );\n const adjustment = await new TimecardAdjustment({\n db,\n id: adjustmentId,\n _skipLoad: true,\n }).initialize();\n Object.assign(adjustment, this.actor, {\n timecardId: id,\n actorProfileId: this.options.actorProfileId!,\n ...input,\n });\n await persistRollup(adjustment);\n });\n return this.scope(async () => {\n const collection = await TimecardAdjustmentCollection.create({\n db: this.db,\n });\n const [row] = await collection.list({\n where: { ...this.actor, id: adjustmentId },\n limit: 1,\n });\n if (!row) throw new Error('Committed adjustment disappeared.');\n return row;\n });\n }\n /** Effective totals combine the frozen base with append-only corrections. */\n async totals(id: string): Promise<{\n regularSeconds: number;\n overtimeSeconds: number;\n totalSeconds: number;\n }> {\n return this.transact(async (db) => {\n const card = await this.card(db, id);\n const collection = await TimecardAdjustmentCollection.create({ db });\n const rows = await collection.list({\n where: { ...this.actor, timecardId: id },\n });\n const regularSeconds = adjustedSeconds(\n card.regularSeconds,\n rows,\n 'regularSeconds',\n );\n const overtimeSeconds = adjustedSeconds(\n card.overtimeSeconds,\n rows,\n 'overtimeSeconds',\n );\n return {\n regularSeconds,\n overtimeSeconds,\n totalSeconds: integer(regularSeconds + overtimeSeconds),\n };\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rollup-U1IO_CMA.js","names":["id"],"sources":["../../src/rollup/write.ts","../../src/rollup/models.ts","../../src/rollup/calculation.ts","../../src/rollup/service.ts"],"sourcesContent":["import type { SmrtObject } from '@happyvertical/smrt-core';\n\n// Per-instance capability, deliberately absent from the package export surface.\nconst permitted = new WeakSet<SmrtObject>();\nexport function assertRollupWrite(object: SmrtObject): void {\n if (!permitted.has(object))\n throw new Error(\n 'Timecards and adjustments are writable only through PeriodRollupService.',\n );\n}\nexport async function persistRollup<T extends SmrtObject>(\n object: T,\n): Promise<T> {\n permitted.add(object);\n try {\n await object.save();\n return object;\n } finally {\n permitted.delete(object);\n }\n}\n","import {\n crossPackageRef,\n field,\n foreignKey,\n SmrtCollection,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { TimecardSource } from './types.js';\nimport { assertRollupWrite } from './write.js';\n\n/** One person's immutable-on-confirmation period snapshot, in integer seconds. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'period_timecards',\n sensitive: true,\n api: { include: [] },\n cli: { include: [] },\n mcp: { include: [] },\n})\nexport class PeriodTimecard extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId() tenantId = '';\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', {\n onDelete: 'RESTRICT',\n })\n profileId = '';\n startsAt: Date = new Date(0);\n endsAt: Date = new Date(0);\n timezone = '';\n rulesVersion = '';\n @field({ type: 'text' }) rulesSnapshot = '{}';\n @field({ type: 'text' }) sourceSnapshot = '[]';\n regularSeconds = 0;\n overtimeSeconds = 0;\n totalSeconds = 0;\n @field({ type: 'text' }) status: 'open' | 'confirmed' = 'open';\n confirmedAt: Date | null = null;\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', {\n nullable: true,\n onDelete: 'RESTRICT',\n })\n confirmedByProfileId: string | null = null;\n\n /** Return a detached source snapshot; corrupt persisted JSON fails closed. */\n getSources(): TimecardSource[] {\n const result: unknown = JSON.parse(this.sourceSnapshot);\n if (!Array.isArray(result))\n throw new Error('Invalid timecard source snapshot.');\n return result as TimecardSource[];\n }\n /** Return the consumer policy frozen with this rollup. */\n getRules(): Record<string, unknown> {\n const result: unknown = JSON.parse(this.rulesSnapshot);\n if (!result || typeof result !== 'object' || Array.isArray(result))\n throw new Error('Invalid timecard rules snapshot.');\n return result as Record<string, unknown>;\n }\n override async save(): Promise<this> {\n assertRollupWrite(this);\n const prior = this.id\n ? await this.db.get(this.tableName, { id: this.id })\n : null;\n if (prior?.status === 'confirmed')\n throw new Error(\n 'Confirmed timecards are immutable; append an adjustment.',\n );\n if (\n prior &&\n (prior.tenant_id !== this.tenantId ||\n prior.profile_id !== this.profileId ||\n new Date(prior.starts_at as string).getTime() !==\n this.startsAt.getTime())\n )\n throw new Error('Timecard identity is immutable.');\n return (await super.save()) as this;\n }\n override async delete(): Promise<void> {\n throw new Error('Timecard history cannot be deleted.');\n }\n}\n\n/** Attributed append-only signed seconds; the confirmed base card never changes. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'timecard_adjustments',\n sensitive: true,\n api: { include: [] },\n cli: { include: [] },\n mcp: { include: [] },\n})\nexport class TimecardAdjustment extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId() tenantId = '';\n @foreignKey(PeriodTimecard, { onDelete: 'RESTRICT' }) timecardId = '';\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', {\n onDelete: 'RESTRICT',\n })\n profileId = '';\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', {\n onDelete: 'RESTRICT',\n })\n actorProfileId = '';\n operationId = '';\n regularSeconds = 0;\n overtimeSeconds = 0;\n reason = '';\n override async save(): Promise<this> {\n assertRollupWrite(this);\n if (this.id && (await this.db.get(this.tableName, { id: this.id })))\n throw new Error('Timecard adjustments are append-only.');\n this.requireInsertOnSave();\n return (await super.save()) as this;\n }\n override async delete(): Promise<void> {\n throw new Error('Timecard adjustments cannot be deleted.');\n }\n}\n/** Scoped persistence collection; mutations must use PeriodRollupService. */\nexport class PeriodTimecardCollection extends SmrtCollection<PeriodTimecard> {\n static readonly _itemClass = PeriodTimecard;\n}\n/** Scoped append-only adjustment collection. */\nexport class TimecardAdjustmentCollection extends SmrtCollection<TimecardAdjustment> {\n static readonly _itemClass = TimecardAdjustment;\n}\n","import type { TimecardPeriod, TimecardSource } from './types.js';\n\nexport function integer(value: number, signed = false): number {\n if (!Number.isSafeInteger(value) || (!signed && value < 0))\n throw new Error('Timecard seconds must be safe integers and nonnegative.');\n return value;\n}\nexport function instant(value: Date): number {\n if (!(value instanceof Date) || !Number.isFinite(value.getTime()))\n throw new Error('Timecard timestamps must be valid dates.');\n return value.getTime();\n}\nexport function validatePeriod(\n value: TimecardPeriod,\n at: Date,\n): TimecardPeriod {\n if (\n !value ||\n instant(value.startsAt) > instant(at) ||\n instant(value.endsAt) <= instant(at) ||\n instant(value.startsAt) >= instant(value.endsAt)\n )\n throw new Error('Resolver period must contain the requested instant.');\n if (!value.timezone || !value.version?.trim())\n throw new Error('Resolver must supply a timezone and policy version.');\n new Intl.DateTimeFormat('en', { timeZone: value.timezone });\n if (\n !value.rules ||\n typeof value.rules !== 'object' ||\n Array.isArray(value.rules)\n )\n throw new Error('Resolver rules must be a JSON object.');\n // JSON round-trip detaches consumer-owned state; fail rather than silently lose non-JSON values.\n const json = JSON.stringify(value.rules, (_key, item) => {\n if (\n typeof item === 'function' ||\n typeof item === 'undefined' ||\n typeof item === 'symbol' ||\n (typeof item === 'number' && !Number.isFinite(item))\n )\n throw new Error('Resolver rules must be JSON serializable.');\n return item;\n });\n const rules: unknown = JSON.parse(json);\n if (!rules || typeof rules !== 'object' || Array.isArray(rules))\n throw new Error('Resolver rules must serialize to a JSON object.');\n return {\n startsAt: new Date(value.startsAt),\n endsAt: new Date(value.endsAt),\n timezone: value.timezone,\n version: value.version,\n rules: rules as Record<string, unknown>,\n };\n}\n/** Prefix allocation preserves integer seconds across adjacent half-open periods. */\nexport function clipSource(\n source: TimecardSource,\n period: TimecardPeriod,\n): TimecardSource | null {\n const start = instant(new Date(source.startsAt));\n const end = instant(new Date(source.endsAt));\n integer(source.seconds);\n if (end < start) throw new Error('Invalid source interval.');\n const left = Math.max(start, period.startsAt.getTime());\n const right = Math.min(end, period.endsAt.getTime());\n if (start === end)\n return start >= period.startsAt.getTime() && start < period.endsAt.getTime()\n ? { ...source }\n : null;\n if (left >= right) return null;\n const span = integer(end - start);\n const prefix = (elapsed: number) =>\n Number((BigInt(source.seconds) * BigInt(integer(elapsed))) / BigInt(span));\n const seconds = prefix(right - start) - prefix(left - start);\n return {\n ...source,\n startsAt: new Date(left).toISOString(),\n endsAt: new Date(right).toISOString(),\n seconds: integer(seconds),\n };\n}\n","import { createHash } from 'node:crypto';\nimport {\n isEmbeddedDatabase,\n isPostgresDatabase,\n withEmbeddedWriteTransaction,\n} from '@happyvertical/smrt-core';\nimport { getTenantId, withTenant } from '@happyvertical/smrt-tenancy';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ServiceTimeEntryCollection } from '../models/service-time-entry.js';\nimport { clipSource, instant, integer, validatePeriod } from './calculation.js';\nimport {\n PeriodTimecard,\n PeriodTimecardCollection,\n TimecardAdjustment,\n TimecardAdjustmentCollection,\n} from './models.js';\nimport type {\n PeriodRulesResolver,\n TimecardActor,\n TimecardAdjustmentInput,\n TimecardSource,\n} from './types.js';\nimport { persistRollup } from './write.js';\n\nfunction identity(parts: string[]): string {\n const bytes = createHash('sha256').update(JSON.stringify(parts)).digest();\n bytes[6] = (bytes[6] & 15) | 80;\n bytes[8] = (bytes[8] & 63) | 128;\n const hex = bytes.subarray(0, 16).toString('hex');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\nfunction adjustedSeconds(\n base: number,\n rows: TimecardAdjustment[],\n field: 'regularSeconds' | 'overtimeSeconds',\n): number {\n const total = rows.reduce(\n (sum, row) => sum + BigInt(integer(row[field], true)),\n BigInt(integer(base)),\n );\n return integer(Number(total));\n}\n\n/**\n * Server-side, trusted tenant/person capability. Consumers authorize this scope\n * before construction. Reads and writes reject conflicting ambient tenancy.\n * SQLite and PostgreSQL are supported; policy and payroll remain consumer-owned.\n */\nexport class PeriodRollupService {\n constructor(\n private readonly db: DatabaseInterface,\n private readonly actor: TimecardActor,\n private readonly resolver: PeriodRulesResolver,\n private readonly options: {\n attendance?: boolean;\n actorProfileId?: string;\n } = {},\n ) {\n if (!actor.tenantId?.trim() || !actor.profileId?.trim())\n throw new Error('Timecards require a trusted tenant/profile actor.');\n this.actor = Object.freeze({\n tenantId: this.identity(actor.tenantId),\n profileId: this.identity(actor.profileId),\n });\n if (options.actorProfileId !== undefined && !options.actorProfileId.trim())\n throw new Error('Actor attribution must be a nonempty profile id.');\n this.options = {\n attendance: options.attendance,\n actorProfileId: this.identity(\n options.actorProfileId ?? this.actor.profileId,\n ),\n };\n }\n\n private identity(value: string): string {\n if (!isPostgresDatabase(this.db)) return value;\n const hex = value.replace(/[{}-]/g, '').toLowerCase();\n if (!/^[0-9a-f]{32}$/.test(hex))\n throw new Error('PostgreSQL timecard identities must be UUIDs.');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n }\n private scope<T>(run: () => Promise<T>): Promise<T> {\n const active = getTenantId();\n if (active && this.identity(active) !== this.actor.tenantId)\n throw new Error('Timecard actor differs from active tenant.');\n return withTenant({ tenantId: this.actor.tenantId }, run);\n }\n private transact<T>(run: (db: DatabaseInterface) => Promise<T>): Promise<T> {\n return this.scope(() =>\n withEmbeddedWriteTransaction(\n this.db,\n isEmbeddedDatabase(this.db),\n async (db) => {\n if (isPostgresDatabase(db))\n await db.query(\n 'SELECT pg_advisory_xact_lock(hashtextextended($1, 0))',\n JSON.stringify([\n 'smrt.rollup',\n this.actor.tenantId,\n this.actor.profileId,\n ]),\n );\n if (this.options.attendance && isPostgresDatabase(db))\n await db.query(\n 'SELECT pg_advisory_xact_lock(hashtextextended($1, 0))',\n JSON.stringify([\n 'smrt.attendance',\n this.actor.tenantId,\n this.actor.profileId,\n ]),\n );\n return run(db);\n },\n true,\n ),\n );\n }\n private async card(\n db: DatabaseInterface,\n id: string,\n ): Promise<PeriodTimecard> {\n const collection = await PeriodTimecardCollection.create({ db });\n const [card] = await collection.list({\n where: { ...this.actor, id },\n limit: 1,\n });\n if (!card) throw new Error('Timecard not found in actor scope.');\n return card;\n }\n /** Scoped read on the root executor, including after transaction completion. */\n async get(id: string): Promise<PeriodTimecard> {\n return this.scope(() => this.card(this.db, id));\n }\n /** Rebuild one open card from a consistent transaction-bound source snapshot. */\n async rollup(at: Date): Promise<PeriodTimecard> {\n at = new Date(instant(at));\n const id = await this.transact(async (db) => {\n const period = validatePeriod(\n await this.resolver.periodFor(new Date(at), this.actor),\n at,\n );\n const id = identity([\n 'smrt.period',\n this.actor.tenantId,\n this.actor.profileId,\n period.startsAt.toISOString(),\n ]);\n const collection = await PeriodTimecardCollection.create({ db });\n const [prior] = await collection.list({\n where: { ...this.actor, id },\n limit: 1,\n });\n if (prior?.status === 'confirmed')\n throw new Error(\n 'Confirmed timecards are immutable; append an adjustment.',\n );\n // A policy change must not create overlapping cards for the same person.\n for (const other of await collection.list({ where: { ...this.actor } })) {\n if (\n other.id !== id &&\n other.startsAt < period.endsAt &&\n other.endsAt > period.startsAt\n )\n throw new Error('Resolver period overlaps an existing timecard.');\n }\n let sources: TimecardSource[] = [];\n let linkedEntryIds = new Set<string>();\n if (this.options.attendance) {\n const { attendanceSources } = await import('./attendance.js');\n ({ sources, linkedEntryIds } = await attendanceSources(\n db,\n this.actor,\n period,\n ));\n }\n const entries = await ServiceTimeEntryCollection.create({ db });\n const evidence = await entries.list({\n where: {\n tenantId: this.actor.tenantId,\n participantProfileId: this.actor.profileId,\n participantKind: 'human',\n },\n });\n const byId = new Map(evidence.map((entry) => [String(entry.id), entry]));\n const approvedRoots = new Set<string>();\n for (const entry of evidence) {\n if (entry.status !== 'approved') continue;\n let linked = linkedEntryIds.has(String(entry.id));\n let ancestor = entry;\n const seen = new Set([String(entry.id)]);\n while (ancestor.correctionOfId) {\n const parentId = ancestor.correctionOfId;\n if (seen.has(parentId))\n throw new Error('Cyclic evidence correction chain.');\n seen.add(parentId);\n const parent = byId.get(parentId);\n if (!parent || parent.status !== 'corrected')\n throw new Error(\n 'Correction requires corrected evidence in the same actor scope.',\n );\n linked ||= linkedEntryIds.has(parentId);\n ancestor = parent;\n }\n const rootId = String(ancestor.id);\n if (approvedRoots.has(rootId))\n throw new Error('Ambiguous approved evidence correction chain.');\n approvedRoots.add(rootId);\n if (linked) continue;\n // Duration-only manual evidence belongs to its approval instant. A\n // bounded interval allocates its actual duration by elapsed fraction.\n const start = entry.startedAt ?? entry.approvedAt;\n if (!start)\n throw new Error(\n 'Approved evidence needs a start or approval timestamp.',\n );\n // Hours-only sources are unbounded and attributed at approval. An\n // unsupported quantity in another period must not block this one.\n if (\n entry.durationSeconds === null &&\n (instant(start) < period.startsAt.getTime() ||\n instant(start) >= period.endsAt.getTime())\n )\n continue;\n const end = entry.startedAt && entry.endedAt ? entry.endedAt : start;\n const source = clipSource(\n {\n kind: 'entry',\n id: String(entry.id),\n startsAt: new Date(instant(start)).toISOString(),\n endsAt: new Date(instant(end)).toISOString(),\n seconds: integer(entry.requireDurationSeconds()),\n },\n period,\n );\n if (source?.seconds) sources.push(source);\n }\n sources.sort(\n (a, b) =>\n a.startsAt.localeCompare(b.startsAt) ||\n a.kind.localeCompare(b.kind) ||\n a.id.localeCompare(b.id),\n );\n const totalSeconds = sources.reduce(\n (sum, source) => integer(sum + source.seconds),\n 0,\n );\n // Resolver receives detached snapshots so it cannot alter persisted evidence.\n const classified = await this.resolver.classify(\n structuredClone(period),\n structuredClone(sources),\n this.actor,\n );\n const regularSeconds = integer(classified?.regularSeconds);\n const overtimeSeconds = integer(classified?.overtimeSeconds);\n if (integer(regularSeconds + overtimeSeconds) !== totalSeconds)\n throw new Error('Resolver classification must conserve total seconds.');\n const card =\n prior ??\n (await new PeriodTimecard({ db, id, _skipLoad: true }).initialize());\n if (!prior) card.requireInsertOnSave();\n Object.assign(card, this.actor, {\n startsAt: period.startsAt,\n endsAt: period.endsAt,\n timezone: period.timezone,\n rulesVersion: period.version,\n rulesSnapshot: JSON.stringify(period.rules),\n sourceSnapshot: JSON.stringify(sources),\n regularSeconds,\n overtimeSeconds,\n totalSeconds,\n });\n await persistRollup(card);\n return id;\n });\n return this.get(id);\n }\n /** Confirm the current snapshot; repeated confirmation is idempotent. */\n async confirm(id: string): Promise<PeriodTimecard> {\n await this.transact(async (db) => {\n const card = await this.card(db, id);\n if (card.status === 'confirmed') return;\n card.status = 'confirmed';\n card.confirmedAt = new Date();\n card.confirmedByProfileId = this.options.actorProfileId!;\n await persistRollup(card);\n });\n return this.get(id);\n }\n /** Append a correction. An operation key retry must have exactly the same payload. */\n async adjust(\n id: string,\n input: TimecardAdjustmentInput,\n ): Promise<TimecardAdjustment> {\n id = this.identity(id);\n input = {\n operationId: input.operationId,\n reason: input.reason,\n regularSeconds: input.regularSeconds,\n overtimeSeconds: input.overtimeSeconds,\n };\n integer(input.regularSeconds, true);\n integer(input.overtimeSeconds, true);\n if (!input.operationId?.trim() || !input.reason?.trim())\n throw new Error('Adjustment requires an operationId and reason.');\n const adjustmentId = identity([\n 'smrt.adjustment',\n this.actor.tenantId,\n this.actor.profileId,\n id,\n input.operationId,\n ]);\n await this.transact(async (db) => {\n const card = await this.card(db, id);\n if (card.status !== 'confirmed')\n throw new Error('Adjustments require a confirmed timecard.');\n const collection = await TimecardAdjustmentCollection.create({ db });\n const rows = await collection.list({\n where: { ...this.actor, timecardId: id },\n });\n const prior = rows.find((row) => row.id === adjustmentId);\n if (prior) {\n if (\n prior.regularSeconds !== input.regularSeconds ||\n prior.overtimeSeconds !== input.overtimeSeconds ||\n prior.reason !== input.reason ||\n prior.actorProfileId !== this.options.actorProfileId\n )\n throw new Error(\n 'Adjustment operationId reused with a different payload.',\n );\n return;\n }\n const regular = adjustedSeconds(\n card.regularSeconds,\n rows,\n 'regularSeconds',\n );\n const overtime = adjustedSeconds(\n card.overtimeSeconds,\n rows,\n 'overtimeSeconds',\n );\n integer(\n integer(regular + input.regularSeconds) +\n integer(overtime + input.overtimeSeconds),\n );\n const adjustment = await new TimecardAdjustment({\n db,\n id: adjustmentId,\n _skipLoad: true,\n }).initialize();\n Object.assign(adjustment, this.actor, {\n timecardId: id,\n actorProfileId: this.options.actorProfileId!,\n ...input,\n });\n await persistRollup(adjustment);\n });\n return this.scope(async () => {\n const collection = await TimecardAdjustmentCollection.create({\n db: this.db,\n });\n const [row] = await collection.list({\n where: { ...this.actor, id: adjustmentId },\n limit: 1,\n });\n if (!row) throw new Error('Committed adjustment disappeared.');\n return row;\n });\n }\n /** Effective totals combine the frozen base with append-only corrections. */\n async totals(id: string): Promise<{\n regularSeconds: number;\n overtimeSeconds: number;\n totalSeconds: number;\n }> {\n return this.transact(async (db) => {\n const card = await this.card(db, id);\n const collection = await TimecardAdjustmentCollection.create({ db });\n const rows = await collection.list({\n where: { ...this.actor, timecardId: id },\n });\n const regularSeconds = adjustedSeconds(\n card.regularSeconds,\n rows,\n 'regularSeconds',\n );\n const overtimeSeconds = adjustedSeconds(\n card.overtimeSeconds,\n rows,\n 'overtimeSeconds',\n );\n return {\n regularSeconds,\n overtimeSeconds,\n totalSeconds: integer(regularSeconds + overtimeSeconds),\n };\n });\n }\n}\n"],"mappings":"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"}
|