@happyvertical/smrt-timesheets 0.57.46 → 0.57.47
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 +4 -0
- package/dist/attendance.js +2 -2
- package/dist/chunks/{attendance-PGuRvmev.js → attendance-Cff4qsKO.js} +3 -3
- package/dist/chunks/{attendance-PGuRvmev.js.map → attendance-Cff4qsKO.js.map} +1 -1
- package/dist/chunks/{attendance-C6eqaZs_.js → attendance-Dx6vtRp_.js} +3 -3
- package/dist/chunks/{attendance-C6eqaZs_.js.map → attendance-Dx6vtRp_.js.map} +1 -1
- package/dist/chunks/{models-9a8vBk3J.js → models-BPvzhbBk.js} +2 -2
- package/dist/chunks/{models-9a8vBk3J.js.map → models-BPvzhbBk.js.map} +1 -1
- package/dist/chunks/{rollup-6Xv4ULa_.js → rollup-MLc_2oKj.js} +3 -3
- package/dist/chunks/{rollup-6Xv4ULa_.js.map → rollup-MLc_2oKj.js.map} +1 -1
- package/dist/chunks/service-time-entry-vfA38BNi.js +286 -0
- package/dist/chunks/service-time-entry-vfA38BNi.js.map +1 -0
- package/dist/chunks/{time-entry-actions-C1h4eq46.js → time-entry-actions-DaFXkjFC.js} +2 -2
- package/dist/chunks/{time-entry-actions-C1h4eq46.js.map → time-entry-actions-DaFXkjFC.js.map} +1 -1
- package/dist/index.js +5 -5
- package/dist/manifest.json +58 -1
- package/dist/rollup.js +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/package.json +8 -8
- package/dist/chunks/service-time-entry-DHhGUUqL.js +0 -286
- package/dist/chunks/service-time-entry-DHhGUUqL.js.map +0 -1
package/dist/chunks/{time-entry-actions-C1h4eq46.js.map → time-entry-actions-DaFXkjFC.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-entry-actions-C1h4eq46.js","names":[],"sources":["../../src/models/service-snapshots.ts","../../src/services/action-principal.ts","../../src/services/service-evidence-service.ts","../../src/services/time-entry-actions.ts"],"sourcesContent":["import {\n field,\n foreignKey,\n SmrtCollection,\n SmrtObject,\n type SmrtSaveOptions,\n smrt,\n ValidationError,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n\n/**\n * Refuse an amount that is not a JavaScript-safe integer number of minor\n * units. SQLite stores `19.99` in an INTEGER column without complaint and\n * PostgreSQL rejects it only at the driver, so the model boundary is the one\n * place both engines (and every writer: collections, `ServiceEvidenceService`,\n * resolvers) get the same answer. Sign is not constrained here.\n */\nexport function assertSnapshotMinorUnits(model: string, amount: unknown): void {\n if (typeof amount !== 'number' || !Number.isSafeInteger(amount)) {\n throw new ValidationError(\n `${model}.amount must be a safe integer number of minor units (cents) — ` +\n `got ${String(amount)}. $19.99 is 1999.`,\n 'VALIDATION_INVALID_VALUE',\n {\n fieldName: 'amount',\n value: amount,\n expectedType: 'safe integer minor units',\n },\n );\n }\n}\n\nconst chargeSnapshot = new WeakMap<ServiceChargeSnapshot, string>();\nconst compensationSnapshot = new WeakMap<ServiceCompensationSnapshot, string>();\n\nasync function immutableCommercialSnapshot(\n object: SmrtObject,\n fields: string[],\n): Promise<string | null> {\n if (!object.id) return null;\n try {\n const row = await object.db.get(object.tableName, { id: object.id });\n if (!row) return null;\n return JSON.stringify(\n Object.fromEntries(\n fields.map((key) => [\n key,\n row[key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)] ?? null,\n ]),\n ),\n );\n } catch {\n return null;\n }\n}\n\nfunction commercialSnapshot(\n object: Record<string, unknown>,\n fields: string[],\n): string {\n return JSON.stringify(\n Object.fromEntries(fields.map((key) => [key, object[key] ?? null])),\n );\n}\n\nconst CHARGE_FIELDS = [\n 'timeEntryId',\n 'amount',\n 'currency',\n 'pricingVersion',\n 'strategy',\n 'rateSnapshot',\n 'sourceChargeRef',\n];\nconst COMPENSATION_FIELDS = [\n 'timeEntryId',\n 'amount',\n 'currency',\n 'termsVersion',\n 'rateSnapshot',\n];\n\n/**\n * Immutable client-charge snapshot for one approved time entry. Money is\n * integer minor units, consistent with smrt-commerce (#2401).\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'service_charge_snapshots',\n // Moved from smrt-projects in #3288; stored references keep resolving.\n previousQualifiedNames: [\n '@happyvertical/smrt-projects:ServiceChargeSnapshot',\n ],\n conflictColumns: ['time_entry_id'],\n api: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n})\nexport class ServiceChargeSnapshot extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId({ nullable: true }) tenantId: string | null = null;\n @foreignKey('ServiceTimeEntry', { required: true }) timeEntryId: string = '';\n /**\n * Client charge in **integer minor units** of {@link currency} — `$19.99` is\n * `1999`. The `= 0` initializer is load-bearing: SMRT maps an integer literal\n * to INTEGER and a decimal literal to DECIMAL (#2401).\n *\n * Copied verbatim from the upstream `ClientCharge.amount`, so the two carry\n * the same unit by construction; a split would land cents in a major-units\n * column with no error anywhere.\n */\n amount: number = 0;\n @field({ type: 'text' }) currency: string = 'USD';\n @field({ type: 'text' }) pricingVersion: string = '';\n @field({ type: 'text' }) strategy: string = '';\n @field({ type: 'text' }) rateSnapshot: string = '{}';\n @field({ type: 'text' }) sourceChargeRef: string = '';\n createdAt: Date = new Date();\n\n override async initialize(): Promise<this> {\n await super.initialize();\n if (await this.isSaved())\n chargeSnapshot.set(\n this,\n commercialSnapshot(\n this as unknown as Record<string, unknown>,\n CHARGE_FIELDS,\n ),\n );\n return this;\n }\n\n override async save(options?: SmrtSaveOptions): Promise<this> {\n assertSnapshotMinorUnits('ServiceChargeSnapshot', this.amount);\n const current = commercialSnapshot(\n this as unknown as Record<string, unknown>,\n CHARGE_FIELDS,\n );\n const prior =\n (await immutableCommercialSnapshot(this, CHARGE_FIELDS)) ??\n chargeSnapshot.get(this) ??\n null;\n if (prior && prior !== current)\n throw new Error(\n 'ServiceChargeSnapshot is immutable; append an upstream adjustment instead.',\n );\n const result = (await super.save(options)) as this;\n chargeSnapshot.set(this, current);\n return result;\n }\n}\n\n/**\n * Immutable provider-compensation snapshot for one approved time entry,\n * kept separate from the client charge so the margin is `charge - compensation`.\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'service_compensation_snapshots',\n // Moved from smrt-projects in #3288; stored references keep resolving.\n previousQualifiedNames: [\n '@happyvertical/smrt-projects:ServiceCompensationSnapshot',\n ],\n conflictColumns: ['time_entry_id'],\n api: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n})\nexport class ServiceCompensationSnapshot extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId({ nullable: true }) tenantId: string | null = null;\n @foreignKey('ServiceTimeEntry', { required: true }) timeEntryId: string = '';\n /**\n * Provider earning in **integer minor units** of {@link currency} (#2401).\n *\n * Converts with {@link ServiceChargeSnapshot.amount}: the delivery margin is\n * `charge - compensation`, so a mixed pair would make that subtraction\n * meaningless.\n */\n amount: number = 0;\n @field({ type: 'text' }) currency: string = 'USD';\n @field({ type: 'text' }) termsVersion: string = '';\n @field({ type: 'text' }) rateSnapshot: string = '{}';\n createdAt: Date = new Date();\n\n override async initialize(): Promise<this> {\n await super.initialize();\n if (await this.isSaved())\n compensationSnapshot.set(\n this,\n commercialSnapshot(\n this as unknown as Record<string, unknown>,\n COMPENSATION_FIELDS,\n ),\n );\n return this;\n }\n\n override async save(options?: SmrtSaveOptions): Promise<this> {\n assertSnapshotMinorUnits('ServiceCompensationSnapshot', this.amount);\n const current = commercialSnapshot(\n this as unknown as Record<string, unknown>,\n COMPENSATION_FIELDS,\n );\n const prior =\n (await immutableCommercialSnapshot(this, COMPENSATION_FIELDS)) ??\n compensationSnapshot.get(this) ??\n null;\n if (prior && prior !== current)\n throw new Error(\n 'ServiceCompensationSnapshot is immutable; create explicit settlement evidence instead.',\n );\n const result = (await super.save(options)) as this;\n compensationSnapshot.set(this, current);\n return result;\n }\n}\n\n/** Collection over `service_charge_snapshots`. */\n// Registered collection class: without its own surface it published\n// generated create/update/delete MCP tools, and a forged snapshot would make\n// approval skip pricing (#4001). Reads only, like the model.\n@smrt({ mcp: { include: ['list', 'get'] }, cli: false })\nexport class ServiceChargeSnapshotCollection extends SmrtCollection<ServiceChargeSnapshot> { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n static readonly _itemClass = ServiceChargeSnapshot;\n}\n/** Collection over `service_compensation_snapshots`. */\n// Registered collection class: without its own surface it published\n// generated create/update/delete MCP tools, and a forged snapshot would make\n// approval skip pricing (#4001). Reads only, like the model.\n@smrt({ mcp: { include: ['list', 'get'] }, cli: false })\nexport class ServiceCompensationSnapshotCollection extends SmrtCollection<ServiceCompensationSnapshot> { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n static readonly _itemClass = ServiceCompensationSnapshot;\n}\n","/**\n * The acting principal for the generated service-backed time entry actions\n * (#4001).\n *\n * Identity always comes from the ambient request context, never from the\n * request body: the tenancy ALS (`withTenant`, the SvelteKit/Express\n * adapters) first, then the authenticated smrt-users session-permission ALS\n * (`createSessionHandler()` without `enterTenantContext`). This is the\n * smrt-fields / smrt-preferences precedent. Authorization goes through\n * smrt-users' `assertOperationPermission()` against the principal's published\n * permission snapshot, so ceilings, super-admin and catalog presence behave\n * exactly as they do for every other guarded operation.\n */\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { getCurrentTenant, withTenant } from '@happyvertical/smrt-tenancy';\nimport {\n assertOperationPermission,\n getCurrentSessionPermissionContext,\n type OperationPermissionCollectionInput,\n OperationPermissionError,\n UserCollection,\n} from '@happyvertical/smrt-users';\n\n/** A refused action with a 4xx status the generated transports serialize. */\nexport class TimesheetActionError extends Error {\n readonly status: number;\n readonly code: string;\n readonly publicMessage: string;\n\n constructor(status: number, code: string, message: string) {\n super(message);\n this.name = 'TimesheetActionError';\n this.status = status;\n this.code = code;\n this.publicMessage = message;\n }\n}\n\n/** Who is acting, resolved from the ambient context. */\nexport interface TimesheetActionPrincipal {\n tenantId: string;\n userId: string;\n permissions: ReadonlySet<string>;\n /** smrt-profiles Profile linked to the user; stamped into audit fields. */\n profileId: string;\n /** True when the tenancy ALS already carries this principal. */\n fromTenancy: boolean;\n}\n\ninterface AmbientPrincipal {\n tenantId: string | null;\n userId: string | null;\n permissions: ReadonlySet<string>;\n profileId: string | null;\n fromTenancy: boolean;\n}\n\nfunction profileIdOf(user: unknown): string | null {\n if (!user || typeof user !== 'object') return null;\n const value = (user as { profileId?: unknown }).profileId;\n return typeof value === 'string' && value ? value : null;\n}\n\nfunction ambientPrincipal(): AmbientPrincipal {\n const tenant = getCurrentTenant();\n const session = getCurrentSessionPermissionContext();\n if (tenant?.tenantId) {\n // Generated SvelteKit routes enter `{ tenantId, userId, permissions }`\n // without the user object; the same person's session still carries it.\n const sessionProfile =\n session?.userId && session.userId === tenant.userId\n ? profileIdOf(session.user)\n : null;\n return {\n tenantId: tenant.tenantId,\n userId: tenant.userId ?? null,\n permissions: tenant.permissions,\n profileId: profileIdOf(tenant.user) ?? sessionProfile,\n fromTenancy: true,\n };\n }\n return {\n tenantId: session?.tenantId ?? null,\n userId: session?.userId ?? null,\n permissions: session?.permissionSet ?? new Set<string>(),\n profileId: profileIdOf(session?.user),\n fromTenancy: false,\n };\n}\n\nasync function loadProfileId(\n userId: string,\n options: SmrtClassOptions,\n): Promise<string | null> {\n try {\n const users = await UserCollection.create({\n db: options.db ?? options.persistence,\n } as SmrtClassOptions);\n return profileIdOf(await users.get({ id: userId }));\n } catch {\n return null;\n }\n}\n\n/** True when the ambient principal holds `<collection>.<action>`. */\nexport async function principalHolds(\n collection: OperationPermissionCollectionInput,\n action: string,\n options: SmrtClassOptions,\n): Promise<boolean> {\n const ambient = ambientPrincipal();\n try {\n await assertOperationPermission({\n collection,\n action,\n db: options.db ?? options.persistence,\n tenantId: ambient.tenantId,\n userId: ambient.userId,\n permissionSet: ambient.permissions,\n allowSystemContextBypass: false,\n allowSuperAdminBypass: false,\n });\n return true;\n } catch (error) {\n if (error instanceof OperationPermissionError) return false;\n throw error;\n }\n}\n\n/**\n * Authorize `<collection>.<action>` for the ambient principal and resolve the\n * profile it acts as. Fails closed (403) without an authenticated,\n * tenant-bound principal, without the permission, or without a linked\n * profile — an action always records who took it.\n *\n * System context never bypasses: trusted server code without a person calls\n * `ServiceEvidenceService` directly. `allowSuperAdminBypass: false` is for\n * money and separation-of-duties operations that need an explicit grant.\n */\nexport async function authorizeTimesheetAction(\n collection: OperationPermissionCollectionInput,\n action: string,\n options: SmrtClassOptions,\n guard: { allowSuperAdminBypass: boolean },\n): Promise<TimesheetActionPrincipal> {\n const ambient = ambientPrincipal();\n try {\n await assertOperationPermission({\n collection,\n action,\n db: options.db ?? options.persistence,\n tenantId: ambient.tenantId,\n userId: ambient.userId,\n permissionSet: ambient.permissions,\n allowSystemContextBypass: false,\n allowSuperAdminBypass: guard.allowSuperAdminBypass,\n });\n } catch (error) {\n if (error instanceof OperationPermissionError) {\n throw new TimesheetActionError(403, 'permission_denied', error.message);\n }\n throw error;\n }\n if (!ambient.tenantId || !ambient.userId) {\n throw new TimesheetActionError(\n 403,\n 'permission_denied',\n 'Time entry actions need an authenticated tenant context.',\n );\n }\n const profileId =\n ambient.profileId ?? (await loadProfileId(ambient.userId, options));\n if (!profileId) {\n throw new TimesheetActionError(\n 403,\n 'actor_profile_required',\n 'Time entry actions need a user with a linked profile.',\n );\n }\n return {\n tenantId: ambient.tenantId,\n userId: ambient.userId,\n permissions: ambient.permissions,\n profileId,\n fromTenancy: ambient.fromTenancy,\n };\n}\n\n/**\n * Run `fn` with the tenancy ALS carrying `principal`, so the tenancy\n * interceptor scopes every read and write. A session-only principal is\n * entered with the same tenant, user and permissions; nothing is widened.\n */\nexport async function withActionPrincipal<T>(\n principal: TimesheetActionPrincipal,\n fn: () => Promise<T>,\n): Promise<T> {\n if (principal.fromTenancy) return fn();\n return withTenant(\n {\n tenantId: principal.tenantId,\n userId: principal.userId,\n permissions: new Set(principal.permissions),\n },\n fn,\n );\n}\n","import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport {\n ServiceChargeSnapshotCollection,\n ServiceCompensationSnapshotCollection,\n} from '../models/service-snapshots.js';\nimport type { ServiceTimeEntry } from '../models/service-time-entry.js';\nimport {\n SERVICE_DURATION_HOURS_EVIDENCE,\n ServiceTimeEntryCollection,\n validateDurationHours,\n} from '../models/service-time-entry.js';\nimport type {\n ServiceEvidence,\n ServiceParticipantKind,\n ServiceTimeEntrySource,\n} from '../types.js';\n\n/**\n * Input for {@link ServiceEvidenceService.record}. The work reference is the\n * qualified class name and id of whatever the time was spent on.\n */\nexport interface RecordServiceTimeInput {\n tenantId?: string | null;\n workRefType?: string | null;\n workRefId?: string | null;\n participantKind: ServiceParticipantKind;\n participantProfileId?: string | null;\n agentRef?: string;\n source: ServiceTimeEntrySource;\n description: string;\n startedAt?: Date | null;\n endedAt?: Date | null;\n durationSeconds?: number | null;\n /** Exact positive decimal text; exclusive with seconds and timestamps. */\n durationHours?: string;\n evidence?: ServiceEvidence[];\n metadata?: Record<string, unknown>;\n}\n\nexport interface CommercialSnapshot {\n /**\n * Integer minor units of `currency` — `$19.99` is `1999` (#2401).\n *\n * Written straight into `ServiceChargeSnapshot.amount` /\n * `ServiceCompensationSnapshot.amount`, INTEGER columns whose `save()`\n * refuses anything but a safe integer on every engine. A fractional\n * major-unit value (`19.99`) is rejected; a whole one (`150` for $150.00)\n * is not detectable and is stored 100× too small, so return minor units.\n */\n amount: number;\n currency?: string;\n version: string;\n strategy?: string;\n terms: Record<string, unknown>;\n sourceRef?: string;\n}\n\nexport interface ServiceCommercialResolver {\n /** Pure preflight: reject unsupported evidence before approval or snapshot writes. */\n validateEntry?(entry: ServiceTimeEntry): void | Promise<void>;\n priceClient(entry: ServiceTimeEntry): Promise<CommercialSnapshot>;\n compensateProvider(entry: ServiceTimeEntry): Promise<CommercialSnapshot>;\n}\n\n/**\n * Records, submits, approves, and corrects time entries. Approval writes one\n * immutable client-charge and one provider-compensation snapshot per entry;\n * a correction is a new entry pointing at the approved one, which flips to\n * `corrected` with its snapshots intact.\n */\nexport class ServiceEvidenceService {\n constructor(\n private readonly entries: ServiceTimeEntryCollection,\n private readonly charges: ServiceChargeSnapshotCollection,\n private readonly compensation: ServiceCompensationSnapshotCollection,\n private readonly commercial?: ServiceCommercialResolver,\n ) {}\n\n static async create(\n options: SmrtClassOptions = {},\n commercial?: ServiceCommercialResolver,\n ): Promise<ServiceEvidenceService> {\n const [entries, charges, compensation] = await Promise.all([\n ServiceTimeEntryCollection.create(options),\n ServiceChargeSnapshotCollection.create(options),\n ServiceCompensationSnapshotCollection.create(options),\n ]);\n return new ServiceEvidenceService(\n entries,\n charges,\n compensation,\n commercial,\n );\n }\n\n async record(input: RecordServiceTimeInput): Promise<ServiceTimeEntry> {\n if (!['timer', 'manual', 'import', 'agent'].includes(input.source))\n throw new Error('Unsupported service time source.');\n if (Boolean(input.workRefType) !== Boolean(input.workRefId))\n throw new Error('workRefType and workRefId must be provided together.');\n if (!(input.workRefType && input.workRefId))\n throw new Error('Service time requires a work reference.');\n if (input.participantKind === 'human' && !input.participantProfileId)\n throw new Error('Human service time requires participantProfileId.');\n if (input.participantKind === 'agent' && !input.agentRef)\n throw new Error('Agent service time requires agentRef.');\n if (input.source === 'timer' && (!input.startedAt || !input.endedAt))\n throw new Error('Timer service time requires startedAt and endedAt.');\n if (input.startedAt && input.endedAt && input.startedAt >= input.endedAt)\n throw new Error('Service time startedAt must be before endedAt.');\n const { durationHours, ...fields } = input;\n if (\n input.evidence?.some(\n (item) => item?.kind === SERVICE_DURATION_HOURS_EVIDENCE,\n )\n )\n throw new Error(\n 'Use durationHours to supply reserved decimal-hours evidence.',\n );\n const hours =\n durationHours === undefined ? null : validateDurationHours(durationHours);\n if (\n hours !== null &&\n (input.durationSeconds != null || input.startedAt || input.endedAt)\n )\n throw new Error(\n 'Decimal-hours-only service time cannot also supply seconds or timestamps.',\n );\n if (hours === null && input.durationSeconds === null)\n throw new Error('Null durationSeconds requires durationHours.');\n const duration =\n hours !== null\n ? null\n : (input.durationSeconds ??\n (input.startedAt && input.endedAt\n ? Math.round(\n (input.endedAt.getTime() - input.startedAt.getTime()) / 1000,\n )\n : 0));\n if (duration !== null && (!Number.isSafeInteger(duration) || duration <= 0))\n throw new Error(\n 'Service time durationSeconds must be a positive integer.',\n );\n const entry = await this.entries.create({\n ...fields,\n durationSeconds: duration,\n evidence: JSON.stringify([\n ...(input.evidence ?? []),\n ...(hours === null\n ? []\n : [{ kind: SERVICE_DURATION_HOURS_EVIDENCE, hours }]),\n ]),\n metadata: JSON.stringify(input.metadata ?? {}),\n status: 'draft',\n submittedAt: null,\n submittedByProfileId: null,\n approvedAt: null,\n approvedByProfileId: null,\n approvalPath: '',\n rejectedAt: null,\n rejectedByProfileId: null,\n rejectionReason: '',\n correctionOfId: null,\n });\n await entry.save();\n return entry;\n }\n\n async submit(\n entry: ServiceTimeEntry,\n profileId?: string,\n ): Promise<ServiceTimeEntry> {\n if (entry.status !== 'draft' && entry.status !== 'rejected')\n throw new Error('Only draft or rejected service time may be submitted.');\n entry.status = 'submitted';\n entry.submittedAt = new Date();\n entry.submittedByProfileId = profileId ?? null;\n return entry.save();\n }\n\n async approve(\n entry: ServiceTimeEntry,\n options: { actorProfileId?: string; approvalPath: string },\n ): Promise<ServiceTimeEntry> {\n if (entry.status !== 'submitted' && entry.status !== 'approved')\n throw new Error(\n 'Only submitted service time or an incomplete approval may be approved.',\n );\n if (!this.commercial)\n throw new Error(\n 'A ServiceCommercialResolver is required to approve service time.',\n );\n const timeEntryId = entry.id;\n if (!timeEntryId)\n throw new Error('ServiceTimeEntry must be saved before approval.');\n const existingCharge = (\n await this.charges.list({ where: { timeEntryId }, limit: 1 })\n )[0];\n const existingCompensation = (\n await this.compensation.list({ where: { timeEntryId }, limit: 1 })\n )[0];\n if (entry.status === 'approved' && existingCharge && existingCompensation)\n return entry;\n\n await this.commercial.validateEntry?.(entry);\n\n // Commit approval before either commercial resolver runs. The subscription\n // resolver creates an approved ClientCharge as part of priceClient(), so a\n // failed approval must never leave billable spend behind.\n if (entry.status === 'submitted') {\n entry.status = 'approved';\n entry.approvedAt = new Date();\n entry.approvedByProfileId = options.actorProfileId ?? null;\n entry.approvalPath = options.approvalPath;\n try {\n await entry.save();\n } catch (error) {\n const revisionConflict =\n error instanceof Error &&\n 'code' in error &&\n error.code === 'RUNTIME_REVISION_CONFLICT';\n if (!revisionConflict) throw error;\n const concurrent = await this.entries.get(timeEntryId);\n if (concurrent?.status !== 'approved') throw error;\n entry = concurrent;\n await this.commercial.validateEntry?.(entry);\n }\n }\n const provider = existingCompensation\n ? null\n : await this.commercial.compensateProvider(entry);\n if (provider) {\n const snapshotId = await deterministicSnapshotId(\n 'service-compensation-snapshot',\n timeEntryId,\n );\n try {\n await this.compensation.create({\n id: snapshotId,\n tenantId: entry.tenantId,\n timeEntryId,\n amount: provider.amount,\n currency: provider.currency ?? 'USD',\n termsVersion: provider.version,\n rateSnapshot: JSON.stringify(provider.terms),\n _insertOnly: true,\n });\n } catch (error) {\n const concurrent =\n (await this.compensation.get(snapshotId)) ??\n (\n await this.compensation.list({\n where: { timeEntryId },\n limit: 1,\n })\n )[0];\n if (!concurrent) throw error;\n }\n }\n const client = existingCharge\n ? null\n : await this.commercial.priceClient(entry);\n if (client) {\n const snapshotId = await deterministicSnapshotId(\n 'service-charge-snapshot',\n timeEntryId,\n );\n try {\n await this.charges.create({\n id: snapshotId,\n tenantId: entry.tenantId,\n timeEntryId,\n amount: client.amount,\n currency: client.currency ?? 'USD',\n pricingVersion: client.version,\n strategy: client.strategy ?? '',\n rateSnapshot: JSON.stringify(client.terms),\n sourceChargeRef: client.sourceRef ?? '',\n _insertOnly: true,\n });\n } catch (error) {\n const concurrent =\n (await this.charges.get(snapshotId)) ??\n (\n await this.charges.list({\n where: { timeEntryId },\n limit: 1,\n })\n )[0];\n if (!concurrent) throw error;\n }\n }\n return entry;\n }\n\n async correct(\n entry: ServiceTimeEntry,\n input: RecordServiceTimeInput,\n ): Promise<ServiceTimeEntry> {\n if (entry.status !== 'approved')\n throw new Error('Only approved service time may be corrected.');\n if (input.tenantId !== undefined && input.tenantId !== entry.tenantId) {\n throw new Error(\n 'Service time corrections must remain in the original tenant.',\n );\n }\n const correction = await this.record({\n ...input,\n tenantId: entry.tenantId,\n });\n correction.correctionOfId = entry.id ?? null;\n await correction.save();\n entry.status = 'corrected';\n await entry.save();\n return correction;\n }\n}\n\nasync function deterministicSnapshotId(\n kind: string,\n timeEntryId: string,\n): Promise<string> {\n const bytes = new TextEncoder().encode(JSON.stringify([kind, timeEntryId]));\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));\n const uuid = digest.slice(0, 16);\n uuid[6] = (uuid[6] & 0x0f) | 0x50;\n uuid[8] = (uuid[8] & 0x3f) | 0x80;\n const hex = Array.from(uuid, (byte) =>\n byte.toString(16).padStart(2, '0'),\n ).join('');\n return [\n hex.slice(0, 8),\n hex.slice(8, 12),\n hex.slice(12, 16),\n hex.slice(16, 20),\n hex.slice(20),\n ].join('-');\n}\n","/**\n * Service-backed write actions for `ServiceTimeEntry` (#4001).\n *\n * The generated CRUD writes stay closed: these are the only generated write\n * path, and each delegates to {@link ServiceEvidenceService}, so the status\n * machine, the submission/approval trail and the charge/compensation\n * snapshots are set in exactly one place. The thin collection methods in\n * `models/service-time-entry.ts` call in here.\n */\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport {\n ServiceChargeSnapshotCollection,\n ServiceCompensationSnapshotCollection,\n} from '../models/service-snapshots.js';\nimport type {\n ServiceTimeEntry,\n ServiceTimeEntryCollection,\n} from '../models/service-time-entry.js';\nimport type { ServiceEvidence } from '../types.js';\nimport {\n authorizeTimesheetAction,\n principalHolds,\n TimesheetActionError,\n type TimesheetActionPrincipal,\n withActionPrincipal,\n} from './action-principal.js';\nimport {\n type ServiceCommercialResolver,\n ServiceEvidenceService,\n} from './service-evidence-service.js';\n\n/** Approval path stamped by the generated `approve` action. */\nexport const SERVICE_TIME_ENTRY_ACTION_APPROVAL_PATH = 'operator';\n\n/**\n * What `approve` prices with: a resolver, or a factory the action calls per\n * request with the collection's options. Use the factory whenever the\n * resolver writes (e.g. smrt-projects' `SubscriptionServiceCommercialResolver`\n * records client charges): `options.db` is the request-bound executor, so\n * those writes share the request's transaction and PostgreSQL RLS state\n * instead of a connection captured at startup.\n */\nexport type ServiceTimeEntryCommercialResolverSource =\n | ServiceCommercialResolver\n | ((context: {\n options: SmrtClassOptions;\n }) => ServiceCommercialResolver | Promise<ServiceCommercialResolver>);\n\nlet actionCommercialResolver: ServiceTimeEntryCommercialResolverSource | null =\n null;\n\n/**\n * Install what the generated `approve` action prices with. Pass `null` to\n * remove it. Without one, `approve` refuses with 409\n * (`approval_not_configured`) and writes nothing.\n *\n * @example\n * ```ts\n * setServiceTimeEntryCommercialResolver(({ options }) =>\n * SubscriptionServiceCommercialResolver.create(options, provider),\n * );\n * ```\n */\nexport function setServiceTimeEntryCommercialResolver(\n resolver: ServiceTimeEntryCommercialResolverSource | null,\n): void {\n actionCommercialResolver = resolver;\n}\n\n/** The source installed by {@link setServiceTimeEntryCommercialResolver}. */\nexport function getServiceTimeEntryCommercialResolver(): ServiceTimeEntryCommercialResolverSource | null {\n return actionCommercialResolver;\n}\n\n/** Body of the `record` action. Identity and tenant come from the session. */\nexport interface RecordServiceTimeActionInput {\n workRefType: string;\n workRefId: string;\n /** Defaults to the acting user's profile; another person needs `approve`. */\n participantProfileId?: string | null;\n /** `manual` (default) or `timer`; imports and agents use the service. */\n source?: 'manual' | 'timer';\n description?: string;\n /** ISO 8601 instant. */\n startedAt?: string | null;\n /** ISO 8601 instant. */\n endedAt?: string | null;\n durationSeconds?: number | null;\n /** Exact positive decimal text; exclusive with seconds and timestamps. */\n durationHours?: string;\n evidence?: ServiceEvidence[];\n metadata?: Record<string, unknown>;\n}\n\n/** Body of the `submit` and `approve` actions. */\nexport interface ServiceTimeEntryActionTarget {\n entryId: string;\n}\n\ntype ActionOptions = SmrtClassOptions;\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n 'The action body must be a JSON object.',\n );\n }\n return value as Record<string, unknown>;\n}\n\nfunction optionalString(\n body: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = body[key];\n if (value === undefined || value === null) return undefined;\n if (typeof value !== 'string') {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n `${key} must be a string.`,\n );\n }\n return value;\n}\n\nfunction requiredString(body: Record<string, unknown>, key: string): string {\n const value = optionalString(body, key)?.trim();\n if (!value) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n `${key} is required.`,\n );\n }\n return value;\n}\n\nfunction optionalInstant(\n body: Record<string, unknown>,\n key: string,\n): Date | null {\n const value = optionalString(body, key);\n if (value === undefined) return null;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n `${key} must be an ISO 8601 instant.`,\n );\n }\n return date;\n}\n\n/**\n * Plain `Error`s from {@link ServiceEvidenceService} are its own input and\n * state preconditions; surface them as 400 instead of an opaque 500. Typed\n * errors (database, revision conflicts) pass through unchanged.\n */\nasync function serviceCall<T>(fn: () => Promise<T>): Promise<T> {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && error.constructor === Error) {\n throw new TimesheetActionError(400, 'invalid_request', error.message);\n }\n throw error;\n }\n}\n\nasync function evidenceService(\n entries: ServiceTimeEntryCollection,\n commercial?: ServiceCommercialResolver,\n): Promise<ServiceEvidenceService> {\n const options = entries.options;\n const [charges, compensation] = await Promise.all([\n ServiceChargeSnapshotCollection.create(options),\n ServiceCompensationSnapshotCollection.create(options),\n ]);\n return new ServiceEvidenceService(entries, charges, compensation, commercial);\n}\n\nfunction itemClass(entries: ServiceTimeEntryCollection) {\n return (entries.constructor as unknown as { _itemClass: unknown })\n ._itemClass as typeof ServiceTimeEntry;\n}\n\nasync function loadEntry(\n entries: ServiceTimeEntryCollection,\n principal: TimesheetActionPrincipal,\n body: Record<string, unknown>,\n): Promise<ServiceTimeEntry> {\n const entryId = requiredString(body, 'entryId');\n const entry = await entries.get({ id: entryId });\n // Optional tenancy also admits global (NULL-tenant) rows in a tenant\n // context; a write must land on the principal's own tenant only.\n if (!entry || (entry.tenantId ?? null) !== principal.tenantId) {\n throw new TimesheetActionError(\n 404,\n 'not_found',\n `Service time entry ${entryId} was not found.`,\n );\n }\n return entry;\n}\n\n/** `record`: create a draft entry through `ServiceEvidenceService.record()`. */\nexport async function recordServiceTimeAction(\n entries: ServiceTimeEntryCollection,\n input: unknown,\n): Promise<ServiceTimeEntry> {\n const body = asRecord(input);\n const principal = await authorizeTimesheetAction(\n itemClass(entries),\n 'record',\n entries.options as ActionOptions,\n { allowSuperAdminBypass: true },\n );\n const participantProfileId =\n optionalString(body, 'participantProfileId')?.trim() || principal.profileId;\n if (\n participantProfileId !== principal.profileId &&\n !(await principalHolds(itemClass(entries), 'approve', entries.options))\n ) {\n throw new TimesheetActionError(\n 403,\n 'permission_denied',\n \"Recording another person's time needs the approve permission.\",\n );\n }\n const source = optionalString(body, 'source') ?? 'manual';\n if (source !== 'manual' && source !== 'timer') {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n \"source must be 'manual' or 'timer'.\",\n );\n }\n const durationSeconds = body.durationSeconds;\n if (\n durationSeconds !== undefined &&\n durationSeconds !== null &&\n typeof durationSeconds !== 'number'\n ) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n 'durationSeconds must be a number.',\n );\n }\n const evidence = body.evidence;\n if (evidence !== undefined && !Array.isArray(evidence)) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n 'evidence must be an array.',\n );\n }\n const metadata = body.metadata;\n if (\n metadata !== undefined &&\n (metadata === null ||\n typeof metadata !== 'object' ||\n Array.isArray(metadata))\n ) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n 'metadata must be an object.',\n );\n }\n return withActionPrincipal(principal, async () => {\n const service = await evidenceService(entries);\n return serviceCall(() =>\n service.record({\n tenantId: principal.tenantId,\n workRefType: requiredString(body, 'workRefType'),\n workRefId: requiredString(body, 'workRefId'),\n participantKind: 'human',\n participantProfileId,\n source,\n description: optionalString(body, 'description') ?? '',\n startedAt: optionalInstant(body, 'startedAt'),\n endedAt: optionalInstant(body, 'endedAt'),\n ...(durationSeconds !== undefined\n ? { durationSeconds: durationSeconds as number | null }\n : {}),\n ...(body.durationHours !== undefined\n ? { durationHours: optionalString(body, 'durationHours') }\n : {}),\n ...(evidence ? { evidence: evidence as ServiceEvidence[] } : {}),\n ...(metadata ? { metadata: metadata as Record<string, unknown> } : {}),\n }),\n );\n });\n}\n\n/**\n * `submit`: `draft | rejected → submitted` through\n * `ServiceEvidenceService.submit()`, stamping `submittedAt` and the acting\n * profile. Only the entry's participant may submit it, unless the caller\n * holds `approve` (a supervisor submitting on someone's behalf).\n */\nexport async function submitServiceTimeAction(\n entries: ServiceTimeEntryCollection,\n input: unknown,\n): Promise<ServiceTimeEntry> {\n const body = asRecord(input);\n const principal = await authorizeTimesheetAction(\n itemClass(entries),\n 'submit',\n entries.options,\n { allowSuperAdminBypass: true },\n );\n return withActionPrincipal(principal, async () => {\n const entry = await loadEntry(entries, principal, body);\n if (\n entry.participantProfileId !== principal.profileId &&\n !(await principalHolds(itemClass(entries), 'approve', entries.options))\n ) {\n throw new TimesheetActionError(\n 403,\n 'permission_denied',\n \"Submitting another person's time needs the approve permission.\",\n );\n }\n const service = await evidenceService(entries);\n return serviceCall(() => service.submit(entry, principal.profileId));\n });\n}\n\n/**\n * `approve`: `submitted → approved` through `ServiceEvidenceService.approve()`,\n * which stamps `approvedAt`, the acting profile and the approval path, then\n * writes the immutable charge and compensation snapshots through the\n * installed {@link setServiceTimeEntryCommercialResolver resolver} (a factory\n * is called per request with the collection's options). Money:\n * super-admin bypass does not apply, the grant must be explicit.\n */\nexport async function approveServiceTimeAction(\n entries: ServiceTimeEntryCollection,\n input: unknown,\n): Promise<ServiceTimeEntry> {\n const body = asRecord(input);\n const principal = await authorizeTimesheetAction(\n itemClass(entries),\n 'approve',\n entries.options,\n { allowSuperAdminBypass: false },\n );\n const source = actionCommercialResolver;\n if (!source) {\n throw new TimesheetActionError(\n // A 4xx so generated transports return the message; the host has not\n // installed the pricing the approval needs.\n 409,\n 'approval_not_configured',\n 'No ServiceCommercialResolver is installed for time entry approval.',\n );\n }\n return withActionPrincipal(principal, async () => {\n const entry = await loadEntry(entries, principal, body);\n // Built per request inside the principal's tenancy context, on the same\n // options (and so the same request-bound executor) as the entries.\n const commercial =\n typeof source === 'function'\n ? await source({ options: entries.options })\n : source;\n const service = await evidenceService(entries, commercial);\n return serviceCall(() =>\n service.approve(entry, {\n actorProfileId: principal.profileId,\n approvalPath: SERVICE_TIME_ENTRY_ACTION_APPROVAL_PATH,\n }),\n );\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"time-entry-actions-DaFXkjFC.js","names":[],"sources":["../../src/models/service-snapshots.ts","../../src/services/action-principal.ts","../../src/services/service-evidence-service.ts","../../src/services/time-entry-actions.ts"],"sourcesContent":["import {\n field,\n foreignKey,\n SmrtCollection,\n SmrtObject,\n type SmrtSaveOptions,\n smrt,\n ValidationError,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n\n/**\n * Refuse an amount that is not a JavaScript-safe integer number of minor\n * units. SQLite stores `19.99` in an INTEGER column without complaint and\n * PostgreSQL rejects it only at the driver, so the model boundary is the one\n * place both engines (and every writer: collections, `ServiceEvidenceService`,\n * resolvers) get the same answer. Sign is not constrained here.\n */\nexport function assertSnapshotMinorUnits(model: string, amount: unknown): void {\n if (typeof amount !== 'number' || !Number.isSafeInteger(amount)) {\n throw new ValidationError(\n `${model}.amount must be a safe integer number of minor units (cents) — ` +\n `got ${String(amount)}. $19.99 is 1999.`,\n 'VALIDATION_INVALID_VALUE',\n {\n fieldName: 'amount',\n value: amount,\n expectedType: 'safe integer minor units',\n },\n );\n }\n}\n\nconst chargeSnapshot = new WeakMap<ServiceChargeSnapshot, string>();\nconst compensationSnapshot = new WeakMap<ServiceCompensationSnapshot, string>();\n\nasync function immutableCommercialSnapshot(\n object: SmrtObject,\n fields: string[],\n): Promise<string | null> {\n if (!object.id) return null;\n try {\n const row = await object.db.get(object.tableName, { id: object.id });\n if (!row) return null;\n return JSON.stringify(\n Object.fromEntries(\n fields.map((key) => [\n key,\n row[key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)] ?? null,\n ]),\n ),\n );\n } catch {\n return null;\n }\n}\n\nfunction commercialSnapshot(\n object: Record<string, unknown>,\n fields: string[],\n): string {\n return JSON.stringify(\n Object.fromEntries(fields.map((key) => [key, object[key] ?? null])),\n );\n}\n\nconst CHARGE_FIELDS = [\n 'timeEntryId',\n 'amount',\n 'currency',\n 'pricingVersion',\n 'strategy',\n 'rateSnapshot',\n 'sourceChargeRef',\n];\nconst COMPENSATION_FIELDS = [\n 'timeEntryId',\n 'amount',\n 'currency',\n 'termsVersion',\n 'rateSnapshot',\n];\n\n/**\n * Immutable client-charge snapshot for one approved time entry. Money is\n * integer minor units, consistent with smrt-commerce (#2401).\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'service_charge_snapshots',\n // Moved from smrt-projects in #3288; stored references keep resolving.\n previousQualifiedNames: [\n '@happyvertical/smrt-projects:ServiceChargeSnapshot',\n ],\n conflictColumns: ['time_entry_id'],\n api: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n})\nexport class ServiceChargeSnapshot extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId({ nullable: true }) tenantId: string | null = null;\n @foreignKey('ServiceTimeEntry', { required: true }) timeEntryId: string = '';\n /**\n * Client charge in **integer minor units** of {@link currency} — `$19.99` is\n * `1999`. The `= 0` initializer is load-bearing: SMRT maps an integer literal\n * to INTEGER and a decimal literal to DECIMAL (#2401).\n *\n * Copied verbatim from the upstream `ClientCharge.amount`, so the two carry\n * the same unit by construction; a split would land cents in a major-units\n * column with no error anywhere.\n */\n amount: number = 0;\n @field({ type: 'text' }) currency: string = 'USD';\n @field({ type: 'text' }) pricingVersion: string = '';\n @field({ type: 'text' }) strategy: string = '';\n @field({ type: 'text' }) rateSnapshot: string = '{}';\n @field({ type: 'text' }) sourceChargeRef: string = '';\n createdAt: Date = new Date();\n\n override async initialize(): Promise<this> {\n await super.initialize();\n if (await this.isSaved())\n chargeSnapshot.set(\n this,\n commercialSnapshot(\n this as unknown as Record<string, unknown>,\n CHARGE_FIELDS,\n ),\n );\n return this;\n }\n\n override async save(options?: SmrtSaveOptions): Promise<this> {\n assertSnapshotMinorUnits('ServiceChargeSnapshot', this.amount);\n const current = commercialSnapshot(\n this as unknown as Record<string, unknown>,\n CHARGE_FIELDS,\n );\n const prior =\n (await immutableCommercialSnapshot(this, CHARGE_FIELDS)) ??\n chargeSnapshot.get(this) ??\n null;\n if (prior && prior !== current)\n throw new Error(\n 'ServiceChargeSnapshot is immutable; append an upstream adjustment instead.',\n );\n const result = (await super.save(options)) as this;\n chargeSnapshot.set(this, current);\n return result;\n }\n}\n\n/**\n * Immutable provider-compensation snapshot for one approved time entry,\n * kept separate from the client charge so the margin is `charge - compensation`.\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'service_compensation_snapshots',\n // Moved from smrt-projects in #3288; stored references keep resolving.\n previousQualifiedNames: [\n '@happyvertical/smrt-projects:ServiceCompensationSnapshot',\n ],\n conflictColumns: ['time_entry_id'],\n api: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n})\nexport class ServiceCompensationSnapshot extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n @tenantId({ nullable: true }) tenantId: string | null = null;\n @foreignKey('ServiceTimeEntry', { required: true }) timeEntryId: string = '';\n /**\n * Provider earning in **integer minor units** of {@link currency} (#2401).\n *\n * Converts with {@link ServiceChargeSnapshot.amount}: the delivery margin is\n * `charge - compensation`, so a mixed pair would make that subtraction\n * meaningless.\n */\n amount: number = 0;\n @field({ type: 'text' }) currency: string = 'USD';\n @field({ type: 'text' }) termsVersion: string = '';\n @field({ type: 'text' }) rateSnapshot: string = '{}';\n createdAt: Date = new Date();\n\n override async initialize(): Promise<this> {\n await super.initialize();\n if (await this.isSaved())\n compensationSnapshot.set(\n this,\n commercialSnapshot(\n this as unknown as Record<string, unknown>,\n COMPENSATION_FIELDS,\n ),\n );\n return this;\n }\n\n override async save(options?: SmrtSaveOptions): Promise<this> {\n assertSnapshotMinorUnits('ServiceCompensationSnapshot', this.amount);\n const current = commercialSnapshot(\n this as unknown as Record<string, unknown>,\n COMPENSATION_FIELDS,\n );\n const prior =\n (await immutableCommercialSnapshot(this, COMPENSATION_FIELDS)) ??\n compensationSnapshot.get(this) ??\n null;\n if (prior && prior !== current)\n throw new Error(\n 'ServiceCompensationSnapshot is immutable; create explicit settlement evidence instead.',\n );\n const result = (await super.save(options)) as this;\n compensationSnapshot.set(this, current);\n return result;\n }\n}\n\n/** Collection over `service_charge_snapshots`. */\n// Registered collection class: without its own surface it published\n// generated create/update/delete MCP tools, and a forged snapshot would make\n// approval skip pricing (#4001). Reads only, like the model.\n@smrt({ mcp: { include: ['list', 'get'] }, cli: false })\nexport class ServiceChargeSnapshotCollection extends SmrtCollection<ServiceChargeSnapshot> { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n static readonly _itemClass = ServiceChargeSnapshot;\n}\n/** Collection over `service_compensation_snapshots`. */\n// Registered collection class: without its own surface it published\n// generated create/update/delete MCP tools, and a forged snapshot would make\n// approval skip pricing (#4001). Reads only, like the model.\n@smrt({ mcp: { include: ['list', 'get'] }, cli: false })\nexport class ServiceCompensationSnapshotCollection extends SmrtCollection<ServiceCompensationSnapshot> { static __smrtPackage__ = \"@happyvertical/smrt-timesheets\";\n static readonly _itemClass = ServiceCompensationSnapshot;\n}\n","/**\n * The acting principal for the generated service-backed time entry actions\n * (#4001).\n *\n * Identity always comes from the ambient request context, never from the\n * request body: the tenancy ALS (`withTenant`, the SvelteKit/Express\n * adapters) first, then the authenticated smrt-users session-permission ALS\n * (`createSessionHandler()` without `enterTenantContext`). This is the\n * smrt-fields / smrt-preferences precedent. Authorization goes through\n * smrt-users' `assertOperationPermission()` against the principal's published\n * permission snapshot, so ceilings, super-admin and catalog presence behave\n * exactly as they do for every other guarded operation.\n */\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { getCurrentTenant, withTenant } from '@happyvertical/smrt-tenancy';\nimport {\n assertOperationPermission,\n getCurrentSessionPermissionContext,\n type OperationPermissionCollectionInput,\n OperationPermissionError,\n UserCollection,\n} from '@happyvertical/smrt-users';\n\n/** A refused action with a 4xx status the generated transports serialize. */\nexport class TimesheetActionError extends Error {\n readonly status: number;\n readonly code: string;\n readonly publicMessage: string;\n\n constructor(status: number, code: string, message: string) {\n super(message);\n this.name = 'TimesheetActionError';\n this.status = status;\n this.code = code;\n this.publicMessage = message;\n }\n}\n\n/** Who is acting, resolved from the ambient context. */\nexport interface TimesheetActionPrincipal {\n tenantId: string;\n userId: string;\n permissions: ReadonlySet<string>;\n /** smrt-profiles Profile linked to the user; stamped into audit fields. */\n profileId: string;\n /** True when the tenancy ALS already carries this principal. */\n fromTenancy: boolean;\n}\n\ninterface AmbientPrincipal {\n tenantId: string | null;\n userId: string | null;\n permissions: ReadonlySet<string>;\n profileId: string | null;\n fromTenancy: boolean;\n}\n\nfunction profileIdOf(user: unknown): string | null {\n if (!user || typeof user !== 'object') return null;\n const value = (user as { profileId?: unknown }).profileId;\n return typeof value === 'string' && value ? value : null;\n}\n\nfunction ambientPrincipal(): AmbientPrincipal {\n const tenant = getCurrentTenant();\n const session = getCurrentSessionPermissionContext();\n if (tenant?.tenantId) {\n // Generated SvelteKit routes enter `{ tenantId, userId, permissions }`\n // without the user object; the same person's session still carries it.\n const sessionProfile =\n session?.userId && session.userId === tenant.userId\n ? profileIdOf(session.user)\n : null;\n return {\n tenantId: tenant.tenantId,\n userId: tenant.userId ?? null,\n permissions: tenant.permissions,\n profileId: profileIdOf(tenant.user) ?? sessionProfile,\n fromTenancy: true,\n };\n }\n return {\n tenantId: session?.tenantId ?? null,\n userId: session?.userId ?? null,\n permissions: session?.permissionSet ?? new Set<string>(),\n profileId: profileIdOf(session?.user),\n fromTenancy: false,\n };\n}\n\nasync function loadProfileId(\n userId: string,\n options: SmrtClassOptions,\n): Promise<string | null> {\n try {\n const users = await UserCollection.create({\n db: options.db ?? options.persistence,\n } as SmrtClassOptions);\n return profileIdOf(await users.get({ id: userId }));\n } catch {\n return null;\n }\n}\n\n/** True when the ambient principal holds `<collection>.<action>`. */\nexport async function principalHolds(\n collection: OperationPermissionCollectionInput,\n action: string,\n options: SmrtClassOptions,\n): Promise<boolean> {\n const ambient = ambientPrincipal();\n try {\n await assertOperationPermission({\n collection,\n action,\n db: options.db ?? options.persistence,\n tenantId: ambient.tenantId,\n userId: ambient.userId,\n permissionSet: ambient.permissions,\n allowSystemContextBypass: false,\n allowSuperAdminBypass: false,\n });\n return true;\n } catch (error) {\n if (error instanceof OperationPermissionError) return false;\n throw error;\n }\n}\n\n/**\n * Authorize `<collection>.<action>` for the ambient principal and resolve the\n * profile it acts as. Fails closed (403) without an authenticated,\n * tenant-bound principal, without the permission, or without a linked\n * profile — an action always records who took it.\n *\n * System context never bypasses: trusted server code without a person calls\n * `ServiceEvidenceService` directly. `allowSuperAdminBypass: false` is for\n * money and separation-of-duties operations that need an explicit grant.\n */\nexport async function authorizeTimesheetAction(\n collection: OperationPermissionCollectionInput,\n action: string,\n options: SmrtClassOptions,\n guard: { allowSuperAdminBypass: boolean },\n): Promise<TimesheetActionPrincipal> {\n const ambient = ambientPrincipal();\n try {\n await assertOperationPermission({\n collection,\n action,\n db: options.db ?? options.persistence,\n tenantId: ambient.tenantId,\n userId: ambient.userId,\n permissionSet: ambient.permissions,\n allowSystemContextBypass: false,\n allowSuperAdminBypass: guard.allowSuperAdminBypass,\n });\n } catch (error) {\n if (error instanceof OperationPermissionError) {\n throw new TimesheetActionError(403, 'permission_denied', error.message);\n }\n throw error;\n }\n if (!ambient.tenantId || !ambient.userId) {\n throw new TimesheetActionError(\n 403,\n 'permission_denied',\n 'Time entry actions need an authenticated tenant context.',\n );\n }\n const profileId =\n ambient.profileId ?? (await loadProfileId(ambient.userId, options));\n if (!profileId) {\n throw new TimesheetActionError(\n 403,\n 'actor_profile_required',\n 'Time entry actions need a user with a linked profile.',\n );\n }\n return {\n tenantId: ambient.tenantId,\n userId: ambient.userId,\n permissions: ambient.permissions,\n profileId,\n fromTenancy: ambient.fromTenancy,\n };\n}\n\n/**\n * Run `fn` with the tenancy ALS carrying `principal`, so the tenancy\n * interceptor scopes every read and write. A session-only principal is\n * entered with the same tenant, user and permissions; nothing is widened.\n */\nexport async function withActionPrincipal<T>(\n principal: TimesheetActionPrincipal,\n fn: () => Promise<T>,\n): Promise<T> {\n if (principal.fromTenancy) return fn();\n return withTenant(\n {\n tenantId: principal.tenantId,\n userId: principal.userId,\n permissions: new Set(principal.permissions),\n },\n fn,\n );\n}\n","import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport {\n ServiceChargeSnapshotCollection,\n ServiceCompensationSnapshotCollection,\n} from '../models/service-snapshots.js';\nimport type { ServiceTimeEntry } from '../models/service-time-entry.js';\nimport {\n SERVICE_DURATION_HOURS_EVIDENCE,\n ServiceTimeEntryCollection,\n validateDurationHours,\n} from '../models/service-time-entry.js';\nimport type {\n ServiceEvidence,\n ServiceParticipantKind,\n ServiceTimeEntrySource,\n} from '../types.js';\n\n/**\n * Input for {@link ServiceEvidenceService.record}. The work reference is the\n * qualified class name and id of whatever the time was spent on.\n */\nexport interface RecordServiceTimeInput {\n tenantId?: string | null;\n workRefType?: string | null;\n workRefId?: string | null;\n participantKind: ServiceParticipantKind;\n participantProfileId?: string | null;\n agentRef?: string;\n source: ServiceTimeEntrySource;\n description: string;\n startedAt?: Date | null;\n endedAt?: Date | null;\n durationSeconds?: number | null;\n /** Exact positive decimal text; exclusive with seconds and timestamps. */\n durationHours?: string;\n evidence?: ServiceEvidence[];\n metadata?: Record<string, unknown>;\n}\n\nexport interface CommercialSnapshot {\n /**\n * Integer minor units of `currency` — `$19.99` is `1999` (#2401).\n *\n * Written straight into `ServiceChargeSnapshot.amount` /\n * `ServiceCompensationSnapshot.amount`, INTEGER columns whose `save()`\n * refuses anything but a safe integer on every engine. A fractional\n * major-unit value (`19.99`) is rejected; a whole one (`150` for $150.00)\n * is not detectable and is stored 100× too small, so return minor units.\n */\n amount: number;\n currency?: string;\n version: string;\n strategy?: string;\n terms: Record<string, unknown>;\n sourceRef?: string;\n}\n\nexport interface ServiceCommercialResolver {\n /** Pure preflight: reject unsupported evidence before approval or snapshot writes. */\n validateEntry?(entry: ServiceTimeEntry): void | Promise<void>;\n priceClient(entry: ServiceTimeEntry): Promise<CommercialSnapshot>;\n compensateProvider(entry: ServiceTimeEntry): Promise<CommercialSnapshot>;\n}\n\n/**\n * Records, submits, approves, and corrects time entries. Approval writes one\n * immutable client-charge and one provider-compensation snapshot per entry;\n * a correction is a new entry pointing at the approved one, which flips to\n * `corrected` with its snapshots intact.\n */\nexport class ServiceEvidenceService {\n constructor(\n private readonly entries: ServiceTimeEntryCollection,\n private readonly charges: ServiceChargeSnapshotCollection,\n private readonly compensation: ServiceCompensationSnapshotCollection,\n private readonly commercial?: ServiceCommercialResolver,\n ) {}\n\n static async create(\n options: SmrtClassOptions = {},\n commercial?: ServiceCommercialResolver,\n ): Promise<ServiceEvidenceService> {\n const [entries, charges, compensation] = await Promise.all([\n ServiceTimeEntryCollection.create(options),\n ServiceChargeSnapshotCollection.create(options),\n ServiceCompensationSnapshotCollection.create(options),\n ]);\n return new ServiceEvidenceService(\n entries,\n charges,\n compensation,\n commercial,\n );\n }\n\n async record(input: RecordServiceTimeInput): Promise<ServiceTimeEntry> {\n if (!['timer', 'manual', 'import', 'agent'].includes(input.source))\n throw new Error('Unsupported service time source.');\n if (Boolean(input.workRefType) !== Boolean(input.workRefId))\n throw new Error('workRefType and workRefId must be provided together.');\n if (!(input.workRefType && input.workRefId))\n throw new Error('Service time requires a work reference.');\n if (input.participantKind === 'human' && !input.participantProfileId)\n throw new Error('Human service time requires participantProfileId.');\n if (input.participantKind === 'agent' && !input.agentRef)\n throw new Error('Agent service time requires agentRef.');\n if (input.source === 'timer' && (!input.startedAt || !input.endedAt))\n throw new Error('Timer service time requires startedAt and endedAt.');\n if (input.startedAt && input.endedAt && input.startedAt >= input.endedAt)\n throw new Error('Service time startedAt must be before endedAt.');\n const { durationHours, ...fields } = input;\n if (\n input.evidence?.some(\n (item) => item?.kind === SERVICE_DURATION_HOURS_EVIDENCE,\n )\n )\n throw new Error(\n 'Use durationHours to supply reserved decimal-hours evidence.',\n );\n const hours =\n durationHours === undefined ? null : validateDurationHours(durationHours);\n if (\n hours !== null &&\n (input.durationSeconds != null || input.startedAt || input.endedAt)\n )\n throw new Error(\n 'Decimal-hours-only service time cannot also supply seconds or timestamps.',\n );\n if (hours === null && input.durationSeconds === null)\n throw new Error('Null durationSeconds requires durationHours.');\n const duration =\n hours !== null\n ? null\n : (input.durationSeconds ??\n (input.startedAt && input.endedAt\n ? Math.round(\n (input.endedAt.getTime() - input.startedAt.getTime()) / 1000,\n )\n : 0));\n if (duration !== null && (!Number.isSafeInteger(duration) || duration <= 0))\n throw new Error(\n 'Service time durationSeconds must be a positive integer.',\n );\n const entry = await this.entries.create({\n ...fields,\n durationSeconds: duration,\n evidence: JSON.stringify([\n ...(input.evidence ?? []),\n ...(hours === null\n ? []\n : [{ kind: SERVICE_DURATION_HOURS_EVIDENCE, hours }]),\n ]),\n metadata: JSON.stringify(input.metadata ?? {}),\n status: 'draft',\n submittedAt: null,\n submittedByProfileId: null,\n approvedAt: null,\n approvedByProfileId: null,\n approvalPath: '',\n rejectedAt: null,\n rejectedByProfileId: null,\n rejectionReason: '',\n correctionOfId: null,\n });\n await entry.save();\n return entry;\n }\n\n async submit(\n entry: ServiceTimeEntry,\n profileId?: string,\n ): Promise<ServiceTimeEntry> {\n if (entry.status !== 'draft' && entry.status !== 'rejected')\n throw new Error('Only draft or rejected service time may be submitted.');\n entry.status = 'submitted';\n entry.submittedAt = new Date();\n entry.submittedByProfileId = profileId ?? null;\n return entry.save();\n }\n\n async approve(\n entry: ServiceTimeEntry,\n options: { actorProfileId?: string; approvalPath: string },\n ): Promise<ServiceTimeEntry> {\n if (entry.status !== 'submitted' && entry.status !== 'approved')\n throw new Error(\n 'Only submitted service time or an incomplete approval may be approved.',\n );\n if (!this.commercial)\n throw new Error(\n 'A ServiceCommercialResolver is required to approve service time.',\n );\n const timeEntryId = entry.id;\n if (!timeEntryId)\n throw new Error('ServiceTimeEntry must be saved before approval.');\n const existingCharge = (\n await this.charges.list({ where: { timeEntryId }, limit: 1 })\n )[0];\n const existingCompensation = (\n await this.compensation.list({ where: { timeEntryId }, limit: 1 })\n )[0];\n if (entry.status === 'approved' && existingCharge && existingCompensation)\n return entry;\n\n await this.commercial.validateEntry?.(entry);\n\n // Commit approval before either commercial resolver runs. The subscription\n // resolver creates an approved ClientCharge as part of priceClient(), so a\n // failed approval must never leave billable spend behind.\n if (entry.status === 'submitted') {\n entry.status = 'approved';\n entry.approvedAt = new Date();\n entry.approvedByProfileId = options.actorProfileId ?? null;\n entry.approvalPath = options.approvalPath;\n try {\n await entry.save();\n } catch (error) {\n const revisionConflict =\n error instanceof Error &&\n 'code' in error &&\n error.code === 'RUNTIME_REVISION_CONFLICT';\n if (!revisionConflict) throw error;\n const concurrent = await this.entries.get(timeEntryId);\n if (concurrent?.status !== 'approved') throw error;\n entry = concurrent;\n await this.commercial.validateEntry?.(entry);\n }\n }\n const provider = existingCompensation\n ? null\n : await this.commercial.compensateProvider(entry);\n if (provider) {\n const snapshotId = await deterministicSnapshotId(\n 'service-compensation-snapshot',\n timeEntryId,\n );\n try {\n await this.compensation.create({\n id: snapshotId,\n tenantId: entry.tenantId,\n timeEntryId,\n amount: provider.amount,\n currency: provider.currency ?? 'USD',\n termsVersion: provider.version,\n rateSnapshot: JSON.stringify(provider.terms),\n _insertOnly: true,\n });\n } catch (error) {\n const concurrent =\n (await this.compensation.get(snapshotId)) ??\n (\n await this.compensation.list({\n where: { timeEntryId },\n limit: 1,\n })\n )[0];\n if (!concurrent) throw error;\n }\n }\n const client = existingCharge\n ? null\n : await this.commercial.priceClient(entry);\n if (client) {\n const snapshotId = await deterministicSnapshotId(\n 'service-charge-snapshot',\n timeEntryId,\n );\n try {\n await this.charges.create({\n id: snapshotId,\n tenantId: entry.tenantId,\n timeEntryId,\n amount: client.amount,\n currency: client.currency ?? 'USD',\n pricingVersion: client.version,\n strategy: client.strategy ?? '',\n rateSnapshot: JSON.stringify(client.terms),\n sourceChargeRef: client.sourceRef ?? '',\n _insertOnly: true,\n });\n } catch (error) {\n const concurrent =\n (await this.charges.get(snapshotId)) ??\n (\n await this.charges.list({\n where: { timeEntryId },\n limit: 1,\n })\n )[0];\n if (!concurrent) throw error;\n }\n }\n return entry;\n }\n\n async correct(\n entry: ServiceTimeEntry,\n input: RecordServiceTimeInput,\n ): Promise<ServiceTimeEntry> {\n if (entry.status !== 'approved')\n throw new Error('Only approved service time may be corrected.');\n if (input.tenantId !== undefined && input.tenantId !== entry.tenantId) {\n throw new Error(\n 'Service time corrections must remain in the original tenant.',\n );\n }\n const correction = await this.record({\n ...input,\n tenantId: entry.tenantId,\n });\n correction.correctionOfId = entry.id ?? null;\n await correction.save();\n entry.status = 'corrected';\n await entry.save();\n return correction;\n }\n}\n\nasync function deterministicSnapshotId(\n kind: string,\n timeEntryId: string,\n): Promise<string> {\n const bytes = new TextEncoder().encode(JSON.stringify([kind, timeEntryId]));\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));\n const uuid = digest.slice(0, 16);\n uuid[6] = (uuid[6] & 0x0f) | 0x50;\n uuid[8] = (uuid[8] & 0x3f) | 0x80;\n const hex = Array.from(uuid, (byte) =>\n byte.toString(16).padStart(2, '0'),\n ).join('');\n return [\n hex.slice(0, 8),\n hex.slice(8, 12),\n hex.slice(12, 16),\n hex.slice(16, 20),\n hex.slice(20),\n ].join('-');\n}\n","/**\n * Service-backed write actions for `ServiceTimeEntry` (#4001).\n *\n * The generated CRUD writes stay closed: these are the only generated write\n * path, and each delegates to {@link ServiceEvidenceService}, so the status\n * machine, the submission/approval trail and the charge/compensation\n * snapshots are set in exactly one place. The thin collection methods in\n * `models/service-time-entry.ts` call in here.\n */\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport {\n ServiceChargeSnapshotCollection,\n ServiceCompensationSnapshotCollection,\n} from '../models/service-snapshots.js';\nimport type {\n ServiceTimeEntry,\n ServiceTimeEntryCollection,\n} from '../models/service-time-entry.js';\nimport type { ServiceEvidence } from '../types.js';\nimport {\n authorizeTimesheetAction,\n principalHolds,\n TimesheetActionError,\n type TimesheetActionPrincipal,\n withActionPrincipal,\n} from './action-principal.js';\nimport {\n type ServiceCommercialResolver,\n ServiceEvidenceService,\n} from './service-evidence-service.js';\n\n/** Approval path stamped by the generated `approve` action. */\nexport const SERVICE_TIME_ENTRY_ACTION_APPROVAL_PATH = 'operator';\n\n/**\n * What `approve` prices with: a resolver, or a factory the action calls per\n * request with the collection's options. Use the factory whenever the\n * resolver writes (e.g. smrt-projects' `SubscriptionServiceCommercialResolver`\n * records client charges): `options.db` is the request-bound executor, so\n * those writes share the request's transaction and PostgreSQL RLS state\n * instead of a connection captured at startup.\n */\nexport type ServiceTimeEntryCommercialResolverSource =\n | ServiceCommercialResolver\n | ((context: {\n options: SmrtClassOptions;\n }) => ServiceCommercialResolver | Promise<ServiceCommercialResolver>);\n\nlet actionCommercialResolver: ServiceTimeEntryCommercialResolverSource | null =\n null;\n\n/**\n * Install what the generated `approve` action prices with. Pass `null` to\n * remove it. Without one, `approve` refuses with 409\n * (`approval_not_configured`) and writes nothing.\n *\n * @example\n * ```ts\n * setServiceTimeEntryCommercialResolver(({ options }) =>\n * SubscriptionServiceCommercialResolver.create(options, provider),\n * );\n * ```\n */\nexport function setServiceTimeEntryCommercialResolver(\n resolver: ServiceTimeEntryCommercialResolverSource | null,\n): void {\n actionCommercialResolver = resolver;\n}\n\n/** The source installed by {@link setServiceTimeEntryCommercialResolver}. */\nexport function getServiceTimeEntryCommercialResolver(): ServiceTimeEntryCommercialResolverSource | null {\n return actionCommercialResolver;\n}\n\n/** Body of the `record` action. Identity and tenant come from the session. */\nexport interface RecordServiceTimeActionInput {\n workRefType: string;\n workRefId: string;\n /** Defaults to the acting user's profile; another person needs `approve`. */\n participantProfileId?: string | null;\n /** `manual` (default) or `timer`; imports and agents use the service. */\n source?: 'manual' | 'timer';\n description?: string;\n /** ISO 8601 instant. */\n startedAt?: string | null;\n /** ISO 8601 instant. */\n endedAt?: string | null;\n durationSeconds?: number | null;\n /** Exact positive decimal text; exclusive with seconds and timestamps. */\n durationHours?: string;\n evidence?: ServiceEvidence[];\n metadata?: Record<string, unknown>;\n}\n\n/** Body of the `submit` and `approve` actions. */\nexport interface ServiceTimeEntryActionTarget {\n entryId: string;\n}\n\ntype ActionOptions = SmrtClassOptions;\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n 'The action body must be a JSON object.',\n );\n }\n return value as Record<string, unknown>;\n}\n\nfunction optionalString(\n body: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = body[key];\n if (value === undefined || value === null) return undefined;\n if (typeof value !== 'string') {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n `${key} must be a string.`,\n );\n }\n return value;\n}\n\nfunction requiredString(body: Record<string, unknown>, key: string): string {\n const value = optionalString(body, key)?.trim();\n if (!value) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n `${key} is required.`,\n );\n }\n return value;\n}\n\nfunction optionalInstant(\n body: Record<string, unknown>,\n key: string,\n): Date | null {\n const value = optionalString(body, key);\n if (value === undefined) return null;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n `${key} must be an ISO 8601 instant.`,\n );\n }\n return date;\n}\n\n/**\n * Plain `Error`s from {@link ServiceEvidenceService} are its own input and\n * state preconditions; surface them as 400 instead of an opaque 500. Typed\n * errors (database, revision conflicts) pass through unchanged.\n */\nasync function serviceCall<T>(fn: () => Promise<T>): Promise<T> {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && error.constructor === Error) {\n throw new TimesheetActionError(400, 'invalid_request', error.message);\n }\n throw error;\n }\n}\n\nasync function evidenceService(\n entries: ServiceTimeEntryCollection,\n commercial?: ServiceCommercialResolver,\n): Promise<ServiceEvidenceService> {\n const options = entries.options;\n const [charges, compensation] = await Promise.all([\n ServiceChargeSnapshotCollection.create(options),\n ServiceCompensationSnapshotCollection.create(options),\n ]);\n return new ServiceEvidenceService(entries, charges, compensation, commercial);\n}\n\nfunction itemClass(entries: ServiceTimeEntryCollection) {\n return (entries.constructor as unknown as { _itemClass: unknown })\n ._itemClass as typeof ServiceTimeEntry;\n}\n\nasync function loadEntry(\n entries: ServiceTimeEntryCollection,\n principal: TimesheetActionPrincipal,\n body: Record<string, unknown>,\n): Promise<ServiceTimeEntry> {\n const entryId = requiredString(body, 'entryId');\n const entry = await entries.get({ id: entryId });\n // Optional tenancy also admits global (NULL-tenant) rows in a tenant\n // context; a write must land on the principal's own tenant only.\n if (!entry || (entry.tenantId ?? null) !== principal.tenantId) {\n throw new TimesheetActionError(\n 404,\n 'not_found',\n `Service time entry ${entryId} was not found.`,\n );\n }\n return entry;\n}\n\n/** `record`: create a draft entry through `ServiceEvidenceService.record()`. */\nexport async function recordServiceTimeAction(\n entries: ServiceTimeEntryCollection,\n input: unknown,\n): Promise<ServiceTimeEntry> {\n const body = asRecord(input);\n const principal = await authorizeTimesheetAction(\n itemClass(entries),\n 'record',\n entries.options as ActionOptions,\n { allowSuperAdminBypass: true },\n );\n const participantProfileId =\n optionalString(body, 'participantProfileId')?.trim() || principal.profileId;\n if (\n participantProfileId !== principal.profileId &&\n !(await principalHolds(itemClass(entries), 'approve', entries.options))\n ) {\n throw new TimesheetActionError(\n 403,\n 'permission_denied',\n \"Recording another person's time needs the approve permission.\",\n );\n }\n const source = optionalString(body, 'source') ?? 'manual';\n if (source !== 'manual' && source !== 'timer') {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n \"source must be 'manual' or 'timer'.\",\n );\n }\n const durationSeconds = body.durationSeconds;\n if (\n durationSeconds !== undefined &&\n durationSeconds !== null &&\n typeof durationSeconds !== 'number'\n ) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n 'durationSeconds must be a number.',\n );\n }\n const evidence = body.evidence;\n if (evidence !== undefined && !Array.isArray(evidence)) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n 'evidence must be an array.',\n );\n }\n const metadata = body.metadata;\n if (\n metadata !== undefined &&\n (metadata === null ||\n typeof metadata !== 'object' ||\n Array.isArray(metadata))\n ) {\n throw new TimesheetActionError(\n 400,\n 'invalid_request',\n 'metadata must be an object.',\n );\n }\n return withActionPrincipal(principal, async () => {\n const service = await evidenceService(entries);\n return serviceCall(() =>\n service.record({\n tenantId: principal.tenantId,\n workRefType: requiredString(body, 'workRefType'),\n workRefId: requiredString(body, 'workRefId'),\n participantKind: 'human',\n participantProfileId,\n source,\n description: optionalString(body, 'description') ?? '',\n startedAt: optionalInstant(body, 'startedAt'),\n endedAt: optionalInstant(body, 'endedAt'),\n ...(durationSeconds !== undefined\n ? { durationSeconds: durationSeconds as number | null }\n : {}),\n ...(body.durationHours !== undefined\n ? { durationHours: optionalString(body, 'durationHours') }\n : {}),\n ...(evidence ? { evidence: evidence as ServiceEvidence[] } : {}),\n ...(metadata ? { metadata: metadata as Record<string, unknown> } : {}),\n }),\n );\n });\n}\n\n/**\n * `submit`: `draft | rejected → submitted` through\n * `ServiceEvidenceService.submit()`, stamping `submittedAt` and the acting\n * profile. Only the entry's participant may submit it, unless the caller\n * holds `approve` (a supervisor submitting on someone's behalf).\n */\nexport async function submitServiceTimeAction(\n entries: ServiceTimeEntryCollection,\n input: unknown,\n): Promise<ServiceTimeEntry> {\n const body = asRecord(input);\n const principal = await authorizeTimesheetAction(\n itemClass(entries),\n 'submit',\n entries.options,\n { allowSuperAdminBypass: true },\n );\n return withActionPrincipal(principal, async () => {\n const entry = await loadEntry(entries, principal, body);\n if (\n entry.participantProfileId !== principal.profileId &&\n !(await principalHolds(itemClass(entries), 'approve', entries.options))\n ) {\n throw new TimesheetActionError(\n 403,\n 'permission_denied',\n \"Submitting another person's time needs the approve permission.\",\n );\n }\n const service = await evidenceService(entries);\n return serviceCall(() => service.submit(entry, principal.profileId));\n });\n}\n\n/**\n * `approve`: `submitted → approved` through `ServiceEvidenceService.approve()`,\n * which stamps `approvedAt`, the acting profile and the approval path, then\n * writes the immutable charge and compensation snapshots through the\n * installed {@link setServiceTimeEntryCommercialResolver resolver} (a factory\n * is called per request with the collection's options). Money:\n * super-admin bypass does not apply, the grant must be explicit.\n */\nexport async function approveServiceTimeAction(\n entries: ServiceTimeEntryCollection,\n input: unknown,\n): Promise<ServiceTimeEntry> {\n const body = asRecord(input);\n const principal = await authorizeTimesheetAction(\n itemClass(entries),\n 'approve',\n entries.options,\n { allowSuperAdminBypass: false },\n );\n const source = actionCommercialResolver;\n if (!source) {\n throw new TimesheetActionError(\n // A 4xx so generated transports return the message; the host has not\n // installed the pricing the approval needs.\n 409,\n 'approval_not_configured',\n 'No ServiceCommercialResolver is installed for time entry approval.',\n );\n }\n return withActionPrincipal(principal, async () => {\n const entry = await loadEntry(entries, principal, body);\n // Built per request inside the principal's tenancy context, on the same\n // options (and so the same request-bound executor) as the entries.\n const commercial =\n typeof source === 'function'\n ? await source({ options: entries.options })\n : source;\n const service = await evidenceService(entries, commercial);\n return serviceCall(() =>\n service.approve(entry, {\n actorProfileId: principal.profileId,\n approvalPath: SERVICE_TIME_ENTRY_ACTION_APPROVAL_PATH,\n }),\n );\n });\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { a as setServiceTimeEntryCommercialResolver, c as ServiceEvidenceService, d as ServiceChargeSnapshotCollection, f as ServiceCompensationSnapshot, i as recordServiceTimeAction, l as TimesheetActionError, n as approveServiceTimeAction, o as submitServiceTimeAction, p as ServiceCompensationSnapshotCollection, r as getServiceTimeEntryCommercialResolver, t as SERVICE_TIME_ENTRY_ACTION_APPROVAL_PATH, u as ServiceChargeSnapshot } from "./chunks/time-entry-actions-
|
|
2
|
-
import { i as ServiceTimeEntryCollection, n as SERVICE_TIME_ENTRY_FROZEN_FIELDS, r as ServiceTimeEntry, t as SERVICE_DURATION_HOURS_EVIDENCE } from "./chunks/service-time-entry-
|
|
3
|
-
import { a as AttendanceReplay, c as AttendanceSegmentCollection, i as AttendancePunchCollection, n as AttendanceBreakCollection, o as AttendanceReplayCollection, r as AttendancePunch, s as AttendanceSegment, t as AttendanceBreak } from "./chunks/models-
|
|
4
|
-
import { a as continueSegment, c as openSegment, d as validateAttendanceWork, f as ATTENDANCE_CORRECTION_KINDS, i as closeOpenSegment, l as replaceSegment, m as AttendanceCorrectionCollection, n as AttendanceService, o as endSegment, p as AttendanceCorrection, r as beginSegment, s as listSegments, t as AttendanceCorrectionService, u as AttendanceConflict } from "./chunks/attendance-
|
|
1
|
+
import { a as setServiceTimeEntryCommercialResolver, c as ServiceEvidenceService, d as ServiceChargeSnapshotCollection, f as ServiceCompensationSnapshot, i as recordServiceTimeAction, l as TimesheetActionError, n as approveServiceTimeAction, o as submitServiceTimeAction, p as ServiceCompensationSnapshotCollection, r as getServiceTimeEntryCommercialResolver, t as SERVICE_TIME_ENTRY_ACTION_APPROVAL_PATH, u as ServiceChargeSnapshot } from "./chunks/time-entry-actions-DaFXkjFC.js";
|
|
2
|
+
import { i as ServiceTimeEntryCollection, n as SERVICE_TIME_ENTRY_FROZEN_FIELDS, r as ServiceTimeEntry, t as SERVICE_DURATION_HOURS_EVIDENCE } from "./chunks/service-time-entry-vfA38BNi.js";
|
|
3
|
+
import { a as AttendanceReplay, c as AttendanceSegmentCollection, i as AttendancePunchCollection, n as AttendanceBreakCollection, o as AttendanceReplayCollection, r as AttendancePunch, s as AttendanceSegment, t as AttendanceBreak } from "./chunks/models-BPvzhbBk.js";
|
|
4
|
+
import { a as continueSegment, c as openSegment, d as validateAttendanceWork, f as ATTENDANCE_CORRECTION_KINDS, i as closeOpenSegment, l as replaceSegment, m as AttendanceCorrectionCollection, n as AttendanceService, o as endSegment, p as AttendanceCorrection, r as beginSegment, s as listSegments, t as AttendanceCorrectionService, u as AttendanceConflict } from "./chunks/attendance-Cff4qsKO.js";
|
|
5
5
|
import { SERVICE_TIME_ENTRY_STATUS_TRANSITIONS } from "./types.js";
|
|
6
|
-
import { a as confirmedTimecardGuard, c as TimecardAdjustment, d as TimecardReopeningCollection, l as TimecardAdjustmentCollection, o as PeriodTimecard, s as PeriodTimecardCollection, t as PeriodRollupService, u as TimecardReopening } from "./chunks/rollup-
|
|
6
|
+
import { a as confirmedTimecardGuard, c as TimecardAdjustment, d as TimecardReopeningCollection, l as TimecardAdjustmentCollection, o as PeriodTimecard, s as PeriodTimecardCollection, t as PeriodRollupService, u as TimecardReopening } from "./chunks/rollup-MLc_2oKj.js";
|
|
7
7
|
import { TIMESHEETS_MODULE_META, TIMESHEETS_UI_SLOTS } from "./ui.js";
|
|
8
8
|
import { SmrtRecipe } from "@happyvertical/smrt-core";
|
|
9
9
|
//#region src/recipes.ts
|
package/dist/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-timesheets",
|
|
5
|
-
"packageVersion": "0.57.
|
|
5
|
+
"packageVersion": "0.57.47",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-timesheets:AttendanceCorrection": {
|
|
8
8
|
"name": "attendancecorrection",
|
|
@@ -362,6 +362,12 @@
|
|
|
362
362
|
"extendsTypeArg": "AttendanceCorrection",
|
|
363
363
|
"exportName": "AttendanceCorrectionCollection",
|
|
364
364
|
"collectionExportName": "AttendanceCorrectionCollectionCollection",
|
|
365
|
+
"collectionOf": "@happyvertical/smrt-timesheets:AttendanceCorrection",
|
|
366
|
+
"inheritedExposure": [
|
|
367
|
+
"api",
|
|
368
|
+
"mcp",
|
|
369
|
+
"cli"
|
|
370
|
+
],
|
|
365
371
|
"schema": {
|
|
366
372
|
"tableName": "attendance_corrections",
|
|
367
373
|
"ddl": "CREATE TABLE IF NOT EXISTS \"attendance_corrections\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID NOT NULL,\n \"subject_profile_id\" UUID,\n \"actor_profile_id\" UUID,\n \"kind\" TEXT,\n \"punch_id\" UUID,\n \"target_id\" TEXT,\n \"before_state\" TEXT,\n \"after_state\" TEXT,\n \"reason\" TEXT,\n \"acknowledgement_required\" BOOLEAN NOT NULL DEFAULT FALSE,\n \"acknowledged_at\" TIMESTAMP,\n \"acknowledged_by_profile_id\" UUID\n);",
|
|
@@ -1738,6 +1744,12 @@
|
|
|
1738
1744
|
"extendsTypeArg": "AttendancePunch",
|
|
1739
1745
|
"exportName": "AttendancePunchCollection",
|
|
1740
1746
|
"collectionExportName": "AttendancePunchCollectionCollection",
|
|
1747
|
+
"collectionOf": "@happyvertical/smrt-timesheets:AttendancePunch",
|
|
1748
|
+
"inheritedExposure": [
|
|
1749
|
+
"api",
|
|
1750
|
+
"mcp",
|
|
1751
|
+
"cli"
|
|
1752
|
+
],
|
|
1741
1753
|
"schema": {
|
|
1742
1754
|
"tableName": "attendance_punches",
|
|
1743
1755
|
"ddl": "CREATE TABLE IF NOT EXISTS \"attendance_punches\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID NOT NULL,\n \"profile_id\" UUID,\n \"started_at\" TIMESTAMP,\n \"ended_at\" TIMESTAMP,\n \"duration_seconds\" INTEGER NOT NULL DEFAULT 0,\n \"unpaid_break_seconds\" INTEGER NOT NULL DEFAULT 0,\n \"review_required\" BOOLEAN NOT NULL DEFAULT FALSE,\n \"auto_close_reason\" TEXT,\n \"recorded_offline\" BOOLEAN NOT NULL DEFAULT FALSE,\n \"source\" TEXT,\n \"device_ref_type\" TEXT,\n \"device_ref_id\" TEXT,\n \"service_time_entry_id\" UUID,\n \"open_slot\" TEXT NOT NULL\n);",
|
|
@@ -1944,6 +1956,12 @@
|
|
|
1944
1956
|
"extendsTypeArg": "AttendanceBreak",
|
|
1945
1957
|
"exportName": "AttendanceBreakCollection",
|
|
1946
1958
|
"collectionExportName": "AttendanceBreakCollectionCollection",
|
|
1959
|
+
"collectionOf": "@happyvertical/smrt-timesheets:AttendanceBreak",
|
|
1960
|
+
"inheritedExposure": [
|
|
1961
|
+
"api",
|
|
1962
|
+
"mcp",
|
|
1963
|
+
"cli"
|
|
1964
|
+
],
|
|
1947
1965
|
"schema": {
|
|
1948
1966
|
"tableName": "attendance_breaks",
|
|
1949
1967
|
"ddl": "CREATE TABLE IF NOT EXISTS \"attendance_breaks\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID NOT NULL,\n \"punch_id\" UUID,\n \"started_at\" TIMESTAMP,\n \"ended_at\" TIMESTAMP,\n \"paid\" BOOLEAN NOT NULL DEFAULT FALSE,\n \"kind\" TEXT,\n \"open_slot\" TEXT NOT NULL\n);",
|
|
@@ -2090,6 +2108,12 @@
|
|
|
2090
2108
|
"extendsTypeArg": "AttendanceSegment",
|
|
2091
2109
|
"exportName": "AttendanceSegmentCollection",
|
|
2092
2110
|
"collectionExportName": "AttendanceSegmentCollectionCollection",
|
|
2111
|
+
"collectionOf": "@happyvertical/smrt-timesheets:AttendanceSegment",
|
|
2112
|
+
"inheritedExposure": [
|
|
2113
|
+
"api",
|
|
2114
|
+
"mcp",
|
|
2115
|
+
"cli"
|
|
2116
|
+
],
|
|
2093
2117
|
"schema": {
|
|
2094
2118
|
"tableName": "attendance_segments",
|
|
2095
2119
|
"ddl": "CREATE TABLE IF NOT EXISTS \"attendance_segments\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID NOT NULL,\n \"punch_id\" UUID,\n \"profile_id\" UUID,\n \"work_ref_type\" TEXT,\n \"work_ref_id\" TEXT,\n \"task_ref_type\" TEXT,\n \"task_ref_id\" TEXT,\n \"kind\" TEXT,\n \"description\" TEXT,\n \"started_at\" TIMESTAMP,\n \"ended_at\" TIMESTAMP,\n \"duration_seconds\" INTEGER NOT NULL DEFAULT 0,\n \"closed_reason\" TEXT DEFAULT '',\n \"review_required\" BOOLEAN NOT NULL DEFAULT FALSE,\n \"open_slot\" TEXT NOT NULL\n);",
|
|
@@ -2300,6 +2324,12 @@
|
|
|
2300
2324
|
"extendsTypeArg": "AttendanceReplay",
|
|
2301
2325
|
"exportName": "AttendanceReplayCollection",
|
|
2302
2326
|
"collectionExportName": "AttendanceReplayCollectionCollection",
|
|
2327
|
+
"collectionOf": "@happyvertical/smrt-timesheets:AttendanceReplay",
|
|
2328
|
+
"inheritedExposure": [
|
|
2329
|
+
"api",
|
|
2330
|
+
"mcp",
|
|
2331
|
+
"cli"
|
|
2332
|
+
],
|
|
2303
2333
|
"schema": {
|
|
2304
2334
|
"tableName": "attendance_replays",
|
|
2305
2335
|
"ddl": "CREATE TABLE IF NOT EXISTS \"attendance_replays\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID NOT NULL,\n \"replay_key\" TEXT NOT NULL UNIQUE,\n \"content\" TEXT,\n \"outcome\" TEXT\n);",
|
|
@@ -2862,6 +2892,10 @@
|
|
|
2862
2892
|
"extendsTypeArg": "ServiceChargeSnapshot",
|
|
2863
2893
|
"exportName": "ServiceChargeSnapshotCollection",
|
|
2864
2894
|
"collectionExportName": "ServiceChargeSnapshotCollectionCollection",
|
|
2895
|
+
"collectionOf": "@happyvertical/smrt-timesheets:ServiceChargeSnapshot",
|
|
2896
|
+
"inheritedExposure": [
|
|
2897
|
+
"api"
|
|
2898
|
+
],
|
|
2865
2899
|
"schema": {
|
|
2866
2900
|
"tableName": "service_charge_snapshots",
|
|
2867
2901
|
"ddl": "CREATE TABLE IF NOT EXISTS \"service_charge_snapshots\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID,\n \"time_entry_id\" UUID NOT NULL,\n \"amount\" INTEGER DEFAULT 0,\n \"currency\" TEXT,\n \"pricing_version\" TEXT,\n \"strategy\" TEXT,\n \"rate_snapshot\" TEXT,\n \"source_charge_ref\" TEXT\n);",
|
|
@@ -2997,6 +3031,10 @@
|
|
|
2997
3031
|
"extendsTypeArg": "ServiceCompensationSnapshot",
|
|
2998
3032
|
"exportName": "ServiceCompensationSnapshotCollection",
|
|
2999
3033
|
"collectionExportName": "ServiceCompensationSnapshotCollectionCollection",
|
|
3034
|
+
"collectionOf": "@happyvertical/smrt-timesheets:ServiceCompensationSnapshot",
|
|
3035
|
+
"inheritedExposure": [
|
|
3036
|
+
"api"
|
|
3037
|
+
],
|
|
3000
3038
|
"schema": {
|
|
3001
3039
|
"tableName": "service_compensation_snapshots",
|
|
3002
3040
|
"ddl": "CREATE TABLE IF NOT EXISTS \"service_compensation_snapshots\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID,\n \"time_entry_id\" UUID NOT NULL,\n \"amount\" INTEGER DEFAULT 0,\n \"currency\" TEXT,\n \"terms_version\" TEXT,\n \"rate_snapshot\" TEXT\n);",
|
|
@@ -3730,6 +3768,7 @@
|
|
|
3730
3768
|
"extendsTypeArg": "ServiceTimeEntry",
|
|
3731
3769
|
"exportName": "ServiceTimeEntryCollection",
|
|
3732
3770
|
"collectionExportName": "ServiceTimeEntryCollectionCollection",
|
|
3771
|
+
"collectionOf": "@happyvertical/smrt-timesheets:ServiceTimeEntry",
|
|
3733
3772
|
"schema": {
|
|
3734
3773
|
"tableName": "service_time_entries",
|
|
3735
3774
|
"ddl": "CREATE TABLE IF NOT EXISTS \"service_time_entries\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID,\n \"work_ref_type\" TEXT,\n \"work_ref_id\" TEXT,\n \"participant_kind\" TEXT,\n \"participant_profile_id\" UUID,\n \"agent_ref\" TEXT,\n \"source\" TEXT,\n \"description\" TEXT,\n \"started_at\" TIMESTAMP,\n \"ended_at\" TIMESTAMP,\n \"duration_seconds\" INTEGER,\n \"evidence\" TEXT,\n \"status\" TEXT,\n \"submitted_at\" TIMESTAMP,\n \"submitted_by_profile_id\" UUID,\n \"approved_at\" TIMESTAMP,\n \"approved_by_profile_id\" UUID,\n \"approval_path\" TEXT,\n \"rejected_at\" TIMESTAMP,\n \"rejected_by_profile_id\" UUID,\n \"rejection_reason\" TEXT,\n \"correction_of_id\" UUID,\n \"metadata\" TEXT\n);",
|
|
@@ -4835,6 +4874,12 @@
|
|
|
4835
4874
|
"extendsTypeArg": "PeriodTimecard",
|
|
4836
4875
|
"exportName": "PeriodTimecardCollection",
|
|
4837
4876
|
"collectionExportName": "PeriodTimecardCollectionCollection",
|
|
4877
|
+
"collectionOf": "@happyvertical/smrt-timesheets:PeriodTimecard",
|
|
4878
|
+
"inheritedExposure": [
|
|
4879
|
+
"api",
|
|
4880
|
+
"mcp",
|
|
4881
|
+
"cli"
|
|
4882
|
+
],
|
|
4838
4883
|
"schema": {
|
|
4839
4884
|
"tableName": "period_timecards",
|
|
4840
4885
|
"ddl": "CREATE TABLE IF NOT EXISTS \"period_timecards\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID NOT NULL,\n \"profile_id\" UUID,\n \"starts_at\" TIMESTAMP,\n \"ends_at\" TIMESTAMP,\n \"timezone\" TEXT,\n \"rules_version\" TEXT,\n \"rules_snapshot\" TEXT,\n \"source_snapshot\" TEXT,\n \"regular_seconds\" INTEGER NOT NULL DEFAULT 0,\n \"overtime_seconds\" INTEGER NOT NULL DEFAULT 0,\n \"total_seconds\" INTEGER NOT NULL DEFAULT 0,\n \"status\" TEXT,\n \"submitted_at\" TIMESTAMP,\n \"submitted_by_profile_id\" UUID,\n \"confirmed_at\" TIMESTAMP,\n \"confirmed_by_profile_id\" UUID\n);",
|
|
@@ -5018,6 +5063,12 @@
|
|
|
5018
5063
|
"extendsTypeArg": "TimecardAdjustment",
|
|
5019
5064
|
"exportName": "TimecardAdjustmentCollection",
|
|
5020
5065
|
"collectionExportName": "TimecardAdjustmentCollectionCollection",
|
|
5066
|
+
"collectionOf": "@happyvertical/smrt-timesheets:TimecardAdjustment",
|
|
5067
|
+
"inheritedExposure": [
|
|
5068
|
+
"api",
|
|
5069
|
+
"mcp",
|
|
5070
|
+
"cli"
|
|
5071
|
+
],
|
|
5021
5072
|
"schema": {
|
|
5022
5073
|
"tableName": "timecard_adjustments",
|
|
5023
5074
|
"ddl": "CREATE TABLE IF NOT EXISTS \"timecard_adjustments\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID NOT NULL,\n \"timecard_id\" UUID,\n \"profile_id\" UUID,\n \"actor_profile_id\" UUID,\n \"operation_id\" TEXT,\n \"regular_seconds\" INTEGER NOT NULL DEFAULT 0,\n \"overtime_seconds\" INTEGER NOT NULL DEFAULT 0,\n \"reason\" TEXT\n);",
|
|
@@ -5166,6 +5217,12 @@
|
|
|
5166
5217
|
"extendsTypeArg": "TimecardReopening",
|
|
5167
5218
|
"exportName": "TimecardReopeningCollection",
|
|
5168
5219
|
"collectionExportName": "TimecardReopeningCollectionCollection",
|
|
5220
|
+
"collectionOf": "@happyvertical/smrt-timesheets:TimecardReopening",
|
|
5221
|
+
"inheritedExposure": [
|
|
5222
|
+
"api",
|
|
5223
|
+
"mcp",
|
|
5224
|
+
"cli"
|
|
5225
|
+
],
|
|
5169
5226
|
"schema": {
|
|
5170
5227
|
"tableName": "timecard_reopenings",
|
|
5171
5228
|
"ddl": "CREATE TABLE IF NOT EXISTS \"timecard_reopenings\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" UUID NOT NULL,\n \"timecard_id\" UUID,\n \"profile_id\" UUID,\n \"actor_profile_id\" UUID,\n \"operation_id\" TEXT,\n \"from_status\" TEXT,\n \"reason\" TEXT,\n \"previous_state\" TEXT\n);",
|
package/dist/rollup.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as confirmedTimecardGuard, c as TimecardAdjustment, d as TimecardReopeningCollection, l as TimecardAdjustmentCollection, o as PeriodTimecard, s as PeriodTimecardCollection, t as PeriodRollupService, u as TimecardReopening } from "./chunks/rollup-
|
|
1
|
+
import { a as confirmedTimecardGuard, c as TimecardAdjustment, d as TimecardReopeningCollection, l as TimecardAdjustmentCollection, o as PeriodTimecard, s as PeriodTimecardCollection, t as PeriodRollupService, u as TimecardReopening } from "./chunks/rollup-MLc_2oKj.js";
|
|
2
2
|
export { PeriodRollupService, PeriodTimecard, PeriodTimecardCollection, TimecardAdjustment, TimecardAdjustmentCollection, TimecardReopening, TimecardReopeningCollection, confirmedTimecardGuard };
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-timesheets",
|
|
6
|
-
"packageVersion": "0.57.
|
|
6
|
+
"packageVersion": "0.57.47",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "346956cf095f2ce80dc4389e293ae9f3cd6ccfa5168a7f6709f97866138365f5",
|
|
11
|
+
"packageJson": "6150b529674ea0042b4532f07fdc69569593d53cd77564bd2a27388e99e6eae4",
|
|
12
|
+
"agents": "ef4aff8756a13f4014b4d87c881710ad0605100f6ee76db3ab6e6e2fea84cf0b",
|
|
13
13
|
"moduleDoc:agents/period-rollup.md": "68a7427e9d15815e04e8e8e77f52041224c96dd82f93712f741d471c81feada6"
|
|
14
14
|
},
|
|
15
15
|
"exports": [
|
|
@@ -2695,7 +2695,7 @@
|
|
|
2695
2695
|
"polymorphicAssociations": 0,
|
|
2696
2696
|
"uuidColumns": 94
|
|
2697
2697
|
},
|
|
2698
|
-
"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). An open punch accepts only a `startedAt` correction, closed breaks/segments inside it, and `addPunch({ open: true })`; the open break/segment and closing stay `AttendanceService`'s. 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).\n`PeriodRollupOptions.attribution` (`'clip'` default | `'start'`) decides crossing\nsources: `'start'` keeps a punch (breaks and segments included) or bounded entry\nwhole in the period containing its start, unclipped, and excludes it from the\nnext (`attributeSource` in `rollup/calculation.ts`) (#4028).\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## Generated write actions (#4001)\n\nGenerated CRUD writes stay closed. `ServiceTimeEntryCollection` hosts the only\ngenerated writes, `record` / `submit` / `approve` (`POST\n/servicetimeentries/<action>`, body `{ entryId }` for submit/approve), on REST\nonly; MCP and CLI stay read-only until per-operation MCP authorization (#4004).\n`services/time-entry-actions.ts` delegates each to `ServiceEvidenceService`, so\nthe trail and snapshots are set in one place. `services/action-principal.ts`\ntakes identity from the tenancy ALS, then the smrt-users session ALS, never the\nbody; authorizes `servicetimeentries.<action>` with `assertOperationPermission`\n(system context never bypasses; `approve` refuses super-admin bypass); and\nstamps the user's linked profile. Only the participant records/submits their\nown time unless the caller holds `approve`. `approve` prices with the host's\n`setServiceTimeEntryCommercialResolver()` — a factory called per request with\nthe collection's options, so a writing resolver shares the request executor;\nwithout one it is a 409 and writes nothing. The runtime REST transport gates\na collection-hosted action on the ITEM's `api.include`, SvelteKit on the\ncollection's, so both list the actions;\na consumer closing them overrides both registrations. The collection and both\nsnapshot collections carry their own `@smrt()` surface: undecorated, they\npublished generated create/update/delete MCP tools. `reject` waits for #3994.\n\n## Recipe\n\n`src/recipes.ts` declares one `SmrtRecipe`, `timesheets.time-entries`\n(label \"Time entries\", section `work`), emitted into the built\n`manifest.json` and `smrt-knowledge.json` `recipes`. It shows\n`ServiceTimeEntry` alone, with the generic work reference, evidence, agent\nand approval-trail fields hidden; attendance and rollup models stay out of\nit. `status` and its trail are hidden and locked, and the help never says to\nset them: `save()` accepts a bare transition, so submit and approve must go\nthrough `ServiceEvidenceService` (the recipe declares no surface for that). Help lives in `src/time-entries.recipe.md` (#3591); keep it and the recipe\nin plain language. See [core recipes](../core/agents/recipes.md).\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, reason and the\npunch's `recordedOffline`/`reviewRequired` (both default true and stay set across\nlater taps; false never clears an earlier flag). A reviewed correction clears\n`reviewRequired`. 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",
|
|
2698
|
+
"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). An open punch accepts only a `startedAt` correction, closed breaks/segments inside it, and `addPunch({ open: true })`; the open break/segment and closing stay `AttendanceService`'s. 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).\n`PeriodRollupOptions.attribution` (`'clip'` default | `'start'`) decides crossing\nsources: `'start'` keeps a punch (breaks and segments included) or bounded entry\nwhole in the period containing its start, unclipped, and excludes it from the\nnext (`attributeSource` in `rollup/calculation.ts`) (#4028).\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## Generated write actions (#4001)\n\nGenerated CRUD writes stay closed. `ServiceTimeEntryCollection` hosts the only\ngenerated writes, `record` / `submit` / `approve` (`POST\n/servicetimeentries/<action>`, body `{ entryId }` for submit/approve), on REST\nonly; MCP and CLI stay read-only until per-operation MCP authorization (#4004).\nA consumer that closes a model also closes its collection's generated CRUD;\n`ServiceTimeEntryCollection`'s own `forWork` / `pendingApproval` stay open\nuntil the collection class is closed with `registerOverride` too. Surface tests\nmatch `<model>collection_` tool names as well as `<model>_` (#3468).\n`services/time-entry-actions.ts` delegates each to `ServiceEvidenceService`, so\nthe trail and snapshots are set in one place. `services/action-principal.ts`\ntakes identity from the tenancy ALS, then the smrt-users session ALS, never the\nbody; authorizes `servicetimeentries.<action>` with `assertOperationPermission`\n(system context never bypasses; `approve` refuses super-admin bypass); and\nstamps the user's linked profile. Only the participant records/submits their\nown time unless the caller holds `approve`. `approve` prices with the host's\n`setServiceTimeEntryCommercialResolver()` — a factory called per request with\nthe collection's options, so a writing resolver shares the request executor;\nwithout one it is a 409 and writes nothing. The runtime REST transport gates\na collection-hosted action on the ITEM's `api.include`, SvelteKit on the\ncollection's, so both list the actions;\na consumer closing them overrides both registrations. The collection and both\nsnapshot collections carry their own `@smrt()` surface: undecorated, they\npublished generated create/update/delete MCP tools. `reject` waits for #3994.\n\n## Recipe\n\n`src/recipes.ts` declares one `SmrtRecipe`, `timesheets.time-entries`\n(label \"Time entries\", section `work`), emitted into the built\n`manifest.json` and `smrt-knowledge.json` `recipes`. It shows\n`ServiceTimeEntry` alone, with the generic work reference, evidence, agent\nand approval-trail fields hidden; attendance and rollup models stay out of\nit. `status` and its trail are hidden and locked, and the help never says to\nset them: `save()` accepts a bare transition, so submit and approve must go\nthrough `ServiceEvidenceService` (the recipe declares no surface for that). Help lives in `src/time-entries.recipe.md` (#3591); keep it and the recipe\nin plain language. See [core recipes](../core/agents/recipes.md).\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, reason and the\npunch's `recordedOffline`/`reviewRequired` (both default true and stay set across\nlater taps; false never clears an earlier flag). A reviewed correction clears\n`reviewRequired`. 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",
|
|
2699
2699
|
"moduleDocs": [
|
|
2700
2700
|
{
|
|
2701
2701
|
"path": "agents/period-rollup.md",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-timesheets",
|
|
3
|
-
"version": "0.57.
|
|
3
|
+
"version": "0.57.47",
|
|
4
4
|
"smrtJsdoc": "strict",
|
|
5
5
|
"description": "Shared time entries for the SMRT framework: who worked how long on what, approval and correction, with immutable charge and compensation snapshots",
|
|
6
6
|
"type": "module",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@happyvertical/smrt-core": "0.57.
|
|
43
|
-
"@happyvertical/smrt-tenancy": "0.57.
|
|
44
|
-
"@happyvertical/smrt-types": "0.57.
|
|
45
|
-
"@happyvertical/smrt-ui": "0.57.
|
|
46
|
-
"@happyvertical/smrt-users": "0.57.
|
|
47
|
-
"@happyvertical/sql": "^0.111.
|
|
42
|
+
"@happyvertical/smrt-core": "0.57.47",
|
|
43
|
+
"@happyvertical/smrt-tenancy": "0.57.47",
|
|
44
|
+
"@happyvertical/smrt-types": "0.57.47",
|
|
45
|
+
"@happyvertical/smrt-ui": "0.57.47",
|
|
46
|
+
"@happyvertical/smrt-users": "0.57.47",
|
|
47
|
+
"@happyvertical/sql": "^0.111.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"svelte": "^5.57.0"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@happyvertical/smrt-vitest": "0.57.
|
|
58
|
+
"@happyvertical/smrt-vitest": "0.57.47",
|
|
59
59
|
"@sveltejs/package": "^2.5.8",
|
|
60
60
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
61
61
|
"@types/node": "24.13.6",
|