@happyvertical/smrt-timesheets 0.54.2 → 0.54.3

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.
@@ -1 +0,0 @@
1
- {"version":3,"file":"service-time-entry-DGt3ZoVw.js","names":[],"sources":["../../src/__smrt-register__.ts","../../src/models/json.ts","../../src/models/service-time-entry.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` so its top-level\n * side effect runs ahead of any class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// During library builds, smrtPlugin replaces this entire module with generated\n// code that embeds the scanned manifest inline (#1506/#1507) — published dists\n// never resolve this URL, so downstream bundlers cannot break registration by\n// relocating the compiled module away from dist/manifest.json. The runtime\n// lookup below is the fallback for source-mode runs without that transform.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * Parse a JSON value stored as text, returning `fallback` on malformed input.\n * Stored JSON may predate a schema change, so reads never throw.\n */\nexport function parseTimesheetJson<T>(value: string, fallback: T): T {\n try {\n return JSON.parse(value) as T;\n } catch {\n return fallback;\n }\n}\n","import {\n crossPackageRef,\n field,\n foreignKey,\n SmrtCollection,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport {\n SERVICE_TIME_ENTRY_STATUS_TRANSITIONS,\n type ServiceEvidence,\n type ServiceParticipantKind,\n type ServiceTimeEntrySource,\n type ServiceTimeEntryStatus,\n} from '../types.js';\nimport { parseTimesheetJson } from './json.js';\n\n/**\n * Fields that may not change once an entry is approved (or corrected). A\n * subtype that adds its own work context extends the list through\n * {@link ServiceTimeEntry.frozenFieldNames}.\n */\nexport const SERVICE_TIME_ENTRY_FROZEN_FIELDS = [\n 'tenantId',\n 'workRefType',\n 'workRefId',\n 'participantKind',\n 'participantProfileId',\n 'agentRef',\n 'source',\n 'description',\n 'startedAt',\n 'endedAt',\n 'durationSeconds',\n 'evidence',\n 'correctionOfId',\n] as const;\n\nfunction toColumn(key: string): string {\n return key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n}\n\nfunction frozenView(\n source: Record<string, unknown>,\n fields: readonly string[],\n): string {\n const result: Record<string, unknown> = {};\n for (const key of fields) {\n let value = source[key] ?? null;\n if ((key === 'startedAt' || key === 'endedAt') && value) {\n const date = value instanceof Date ? value : new Date(String(value));\n value = Number.isNaN(date.getTime()) ? String(value) : date.toISOString();\n }\n if (key === 'durationSeconds' && value !== null) value = Number(value);\n result[key] = value === '' ? null : value;\n }\n return JSON.stringify(result);\n}\n\nconst loadedStatus = new WeakMap<ServiceTimeEntry, ServiceTimeEntryStatus>();\n\n/**\n * A person or agent worked this long on this thing, and someone approved it.\n *\n * The base entry carries a generic work reference (`workRefType` qualified\n * class name + `workRefId`) and no domain-specific foreign key; a domain adds\n * its own context by subclassing over the same `service_time_entries` table\n * (smrt-support's subtype adds `caseId` / `specialistId`). Entries carry no\n * rate: charge and compensation live on the immutable snapshot models.\n *\n * Status moves `draft → submitted → approved | rejected`; an approved entry\n * never changes, and a correction is a new row whose `correctionOfId` points\n * at the entry it corrects, which flips to `corrected`.\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'service_time_entries',\n // Moved from smrt-projects in #3288; stored references keep resolving.\n previousQualifiedNames: ['@happyvertical/smrt-projects:ServiceTimeEntry'],\n api: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n})\nexport class ServiceTimeEntry extends SmrtObject {\n @tenantId({ nullable: true }) tenantId: string | null = null;\n @field({ type: 'text', nullable: true }) workRefType: string | null = null;\n @field({ type: 'text', nullable: true }) workRefId: string | null = null;\n @field({ type: 'text' }) participantKind: ServiceParticipantKind = 'human';\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', { nullable: true })\n participantProfileId: string | null = null;\n @field({ type: 'text' }) agentRef: string = '';\n @field({ type: 'text' }) source: ServiceTimeEntrySource = 'manual';\n @field({ type: 'text' }) description: string = '';\n startedAt: Date | null = null;\n endedAt: Date | null = null;\n /** Integer seconds, or null for an explicit decimal-hours-only source. */\n @field({ type: 'integer', nullable: true })\n durationSeconds: number | null = 0;\n @field({ type: 'text' }) evidence: string = '[]';\n @field({ type: 'text' }) status: ServiceTimeEntryStatus = 'draft';\n submittedAt: Date | null = null;\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', { nullable: true })\n submittedByProfileId: string | null = null;\n approvedAt: Date | null = null;\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', { nullable: true })\n approvedByProfileId: string | null = null;\n @field({ type: 'text' }) approvalPath: string = '';\n rejectedAt: Date | null = null;\n @crossPackageRef('@happyvertical/smrt-profiles:Profile', { nullable: true })\n rejectedByProfileId: string | null = null;\n @field({ type: 'text' }) rejectionReason: string = '';\n @foreignKey('ServiceTimeEntry') correctionOfId: string | null = null;\n @field({ type: 'text' }) metadata: string = '{}';\n\n getEvidence(): ServiceEvidence[] {\n return parseTimesheetJson(this.evidence, []);\n }\n setEvidence(value: ServiceEvidence[]): void {\n this.evidence = JSON.stringify(value ?? []);\n }\n getMetadata(): Record<string, unknown> {\n return parseTimesheetJson(this.metadata, {});\n }\n setMetadata(value: Record<string, unknown>): void {\n this.metadata = JSON.stringify(value ?? {});\n }\n /** Original decimal-hours text, without binary floating-point conversion. */\n durationHoursExact(): string | null {\n const evidence = this.getEvidence();\n const quantities = (Array.isArray(evidence) ? evidence : []).filter(\n (item) => item?.kind === SERVICE_DURATION_HOURS_EVIDENCE,\n );\n if (quantities.length === 0) return null;\n if (quantities.length !== 1)\n throw new Error(\n 'Service time requires exactly one decimal-hours quantity.',\n );\n return validateDurationHours(quantities[0].hours);\n }\n\n /** Numeric convenience for display; use durationHoursExact() for exact terms. */\n durationHours(): number {\n if (this.durationSeconds !== null) return this.durationSeconds / 3600;\n const hours = this.durationHoursExact();\n if (hours === null)\n throw new Error('Service time has no supported duration quantity.');\n return Number(hours);\n }\n\n /** Guard for consumers whose policies require independently supplied seconds. */\n requireDurationSeconds(): number {\n if (this.durationSeconds === null)\n throw new Error(\n 'Decimal-hours-only service time has no measured durationSeconds; use durationHoursExact() with an hours-aware policy.',\n );\n return this.durationSeconds;\n }\n\n /**\n * Fields frozen once the entry is approved. Subtypes that add work context\n * (e.g. a support case) return `[...super.frozenFieldNames(), ...]`.\n */\n protected frozenFieldNames(): readonly string[] {\n return SERVICE_TIME_ENTRY_FROZEN_FIELDS;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n if (await this.isSaved()) loadedStatus.set(this, this.status);\n return this;\n }\n\n override async save(): Promise<this> {\n const hours = this.durationHoursExact();\n if (hours !== null) {\n if (\n this.durationSeconds !== null ||\n this.startedAt ||\n this.endedAt ||\n this.source === 'timer'\n )\n throw new Error(\n 'Decimal-hours-only service time cannot also supply seconds or timestamps.',\n );\n } else if (this.durationSeconds === null) {\n throw new Error(\n 'Null durationSeconds requires explicit decimal-hours evidence.',\n );\n }\n const prior = await this.readPrior();\n if (\n prior &&\n prior.status !== this.status &&\n !SERVICE_TIME_ENTRY_STATUS_TRANSITIONS[prior.status].includes(this.status)\n ) {\n throw new Error(\n `ServiceTimeEntry ${this.id}: illegal status transition '${prior.status}' → '${this.status}'.`,\n );\n }\n if (\n prior &&\n (prior.status === 'approved' || prior.status === 'corrected')\n ) {\n const current = frozenView(\n this as unknown as Record<string, unknown>,\n this.frozenFieldNames(),\n );\n if (current !== prior.frozen) {\n throw new Error(\n 'Approved ServiceTimeEntry evidence is immutable; create a correction instead.',\n );\n }\n }\n const result = (await super.save()) as this;\n loadedStatus.set(this, this.status);\n return result;\n }\n\n private async readPrior(): Promise<{\n status: ServiceTimeEntryStatus;\n frozen: string;\n } | null> {\n if (!this.id) return null;\n const fields = this.frozenFieldNames();\n try {\n const row = await this.db.get(this.tableName, { id: this.id });\n if (!row?.status) return null;\n const values: Record<string, unknown> = {};\n for (const key of fields) {\n values[key] = row[toColumn(key)];\n }\n return {\n status: row.status as ServiceTimeEntryStatus,\n frozen: frozenView(values, fields),\n };\n } catch {\n const status = loadedStatus.get(this);\n return status\n ? {\n status,\n frozen: frozenView(\n this as unknown as Record<string, unknown>,\n fields,\n ),\n }\n : null;\n }\n }\n}\n\n/** Collection over `service_time_entries`. */\nexport class ServiceTimeEntryCollection extends SmrtCollection<ServiceTimeEntry> {\n static readonly _itemClass = ServiceTimeEntry;\n async forWork(\n workRefType: string,\n workRefId: string,\n ): Promise<ServiceTimeEntry[]> {\n return this.list({\n where: { workRefType, workRefId },\n orderBy: 'created_at ASC',\n });\n }\n async pendingApproval(): Promise<ServiceTimeEntry[]> {\n return this.list({\n where: { status: 'submitted' },\n orderBy: 'submitted_at ASC',\n });\n }\n}\n\n/** Reserved evidence kind for a source quantity with no measured elapsed seconds. */\nexport const SERVICE_DURATION_HOURS_EVIDENCE =\n '@happyvertical/smrt-timesheets:duration-hours';\n\n/** Validate without normalizing the original accepted decimal text. */\nexport function validateDurationHours(value: unknown): string {\n if (\n typeof value !== 'string' ||\n !/^\\d+(?:\\.\\d+)?$/.test(value) ||\n !Number.isFinite(Number(value)) ||\n Number(value) <= 0 ||\n Number(value) >= Number.MAX_SAFE_INTEGER\n )\n throw new Error(\n 'Service time durationHours must be positive plain decimal text within the supported numeric range.',\n );\n return value;\n}\n"],"mappings":"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"}