@happyvertical/smrt-timesheets 0.55.13 → 0.55.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/attendance.js +2 -2
- package/dist/chunks/{attendance-D_med-TK.js → attendance-CMItbQiC.js} +3 -3
- package/dist/chunks/{attendance-D_med-TK.js.map → attendance-CMItbQiC.js.map} +1 -1
- package/dist/chunks/{models-tm9MaqZf.js → models-DIsi4S0Y.js} +2 -2
- package/dist/chunks/{models-tm9MaqZf.js.map → models-DIsi4S0Y.js.map} +1 -1
- package/dist/chunks/{rollup-ZZmiv7U3.js → rollup-BEETso9S.js} +3 -3
- package/dist/chunks/{rollup-ZZmiv7U3.js.map → rollup-BEETso9S.js.map} +1 -1
- package/dist/chunks/{service-time-entry-BtSPdeYN.js → service-time-entry-D1mOz-uA.js} +2 -2
- package/dist/chunks/{service-time-entry-BtSPdeYN.js.map → service-time-entry-D1mOz-uA.js.map} +1 -1
- package/dist/index.js +161 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/rollup.js +1 -1
- package/dist/services/dated-labour-commercial-resolver.d.ts +51 -0
- package/dist/services/dated-labour-commercial-resolver.d.ts.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { a as validateDurationHours, i as ServiceTimeEntryCollection, n as SERVICE_TIME_ENTRY_FROZEN_FIELDS, r as ServiceTimeEntry, t as SERVICE_DURATION_HOURS_EVIDENCE } from "./chunks/service-time-entry-
|
|
2
|
-
import { a as AttendanceReplay, i as AttendancePunchCollection, n as AttendanceBreakCollection, o as AttendanceReplayCollection, r as AttendancePunch, t as AttendanceBreak } from "./chunks/models-
|
|
1
|
+
import { a as validateDurationHours, i as ServiceTimeEntryCollection, n as SERVICE_TIME_ENTRY_FROZEN_FIELDS, r as ServiceTimeEntry, t as SERVICE_DURATION_HOURS_EVIDENCE } from "./chunks/service-time-entry-D1mOz-uA.js";
|
|
2
|
+
import { a as AttendanceReplay, i as AttendancePunchCollection, n as AttendanceBreakCollection, o as AttendanceReplayCollection, r as AttendancePunch, t as AttendanceBreak } from "./chunks/models-DIsi4S0Y.js";
|
|
3
3
|
import { SERVICE_TIME_ENTRY_STATUS_TRANSITIONS } from "./types.js";
|
|
4
4
|
import { AttendanceConflict, AttendanceService } from "./attendance.js";
|
|
5
|
-
import { a as PeriodTimecard, c as TimecardAdjustmentCollection, o as PeriodTimecardCollection, s as TimecardAdjustment, t as PeriodRollupService } from "./chunks/rollup-
|
|
5
|
+
import { a as PeriodTimecard, c as TimecardAdjustmentCollection, o as PeriodTimecardCollection, s as TimecardAdjustment, t as PeriodRollupService } from "./chunks/rollup-BEETso9S.js";
|
|
6
6
|
import { TIMESHEETS_MODULE_META, TIMESHEETS_UI_SLOTS } from "./ui.js";
|
|
7
7
|
import { SmrtCollection, SmrtObject, ValidationError, field, foreignKey, smrt } from "@happyvertical/smrt-core";
|
|
8
8
|
import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
|
|
@@ -161,6 +161,163 @@ var ServiceCompensationSnapshotCollection = class extends SmrtCollection {
|
|
|
161
161
|
static _itemClass = ServiceCompensationSnapshot;
|
|
162
162
|
};
|
|
163
163
|
//#endregion
|
|
164
|
+
//#region src/services/dated-labour-commercial-resolver.ts
|
|
165
|
+
var MAX = BigInt(Number.MAX_SAFE_INTEGER);
|
|
166
|
+
function decimal(value, label) {
|
|
167
|
+
if (typeof value !== "string" || value.length > 40 || !/^\d+(?:\.\d{1,18})?$/.test(value)) throw new Error(`${label} must be nonnegative plain decimal text.`);
|
|
168
|
+
const [whole, fraction = ""] = value.split(".");
|
|
169
|
+
const denominator = 10n ** BigInt(fraction.length);
|
|
170
|
+
const numerator = BigInt(whole + fraction);
|
|
171
|
+
if (numerator > MAX * denominator) throw new Error(`${label} exceeds the safe numeric range.`);
|
|
172
|
+
return {
|
|
173
|
+
numerator,
|
|
174
|
+
denominator
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function date(value) {
|
|
178
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value) || value.startsWith("0000-")) throw new Error("Expected a valid YYYY-MM-DD calendar date.");
|
|
179
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
180
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) throw new Error("Expected a valid YYYY-MM-DD calendar date.");
|
|
181
|
+
}
|
|
182
|
+
function identity(value, label) {
|
|
183
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} is required.`);
|
|
184
|
+
}
|
|
185
|
+
function rate(value) {
|
|
186
|
+
if (!value || typeof value !== "object") throw new Error("Rate is required.");
|
|
187
|
+
identity(value.id, "Rate id");
|
|
188
|
+
identity(value.version, "Rate version");
|
|
189
|
+
decimal(value.rateMinor, "Hourly rate");
|
|
190
|
+
return {
|
|
191
|
+
id: value.id,
|
|
192
|
+
version: value.version,
|
|
193
|
+
rateMinor: value.rateMinor
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function amount(hours, hourly) {
|
|
197
|
+
const value = decimal(hourly.rateMinor, "Hourly rate");
|
|
198
|
+
const numerator = hours.numerator * value.numerator;
|
|
199
|
+
const denominator = hours.denominator * value.denominator;
|
|
200
|
+
const result = (2n * numerator + denominator) / (2n * denominator);
|
|
201
|
+
if (result > MAX) throw new Error("Labour amount exceeds safe integer minor units.");
|
|
202
|
+
return Number(result);
|
|
203
|
+
}
|
|
204
|
+
var DatedLabourCommercialResolver = class {
|
|
205
|
+
input;
|
|
206
|
+
selected;
|
|
207
|
+
constructor(input) {
|
|
208
|
+
identity(input.tenantId, "Tenant id");
|
|
209
|
+
identity(input.participantProfileId, "Worker profile id");
|
|
210
|
+
date(input.workDate);
|
|
211
|
+
if (typeof input.currency !== "string" || !/^[A-Z]{3}$/.test(input.currency)) throw new Error("Currency must be an explicit three-letter uppercase code.");
|
|
212
|
+
if (typeof input.billable !== "boolean") throw new Error("Billable must be explicit.");
|
|
213
|
+
if (!Array.isArray(input.schedule) || !input.schedule.length) throw new Error("A rate schedule is required.");
|
|
214
|
+
const schedule = input.schedule.map((period) => {
|
|
215
|
+
date(period.effectiveFrom);
|
|
216
|
+
if (period.effectiveUntil !== void 0) {
|
|
217
|
+
date(period.effectiveUntil);
|
|
218
|
+
if (period.effectiveUntil <= period.effectiveFrom) throw new Error("Effective period must have positive length.");
|
|
219
|
+
}
|
|
220
|
+
if (period.currency !== input.currency) throw new Error("Rate schedule currency mismatch.");
|
|
221
|
+
return {
|
|
222
|
+
effectiveFrom: period.effectiveFrom,
|
|
223
|
+
effectiveUntil: period.effectiveUntil,
|
|
224
|
+
currency: period.currency,
|
|
225
|
+
customer: rate(period.customer),
|
|
226
|
+
wage: rate(period.wage),
|
|
227
|
+
burden: rate(period.burden)
|
|
228
|
+
};
|
|
229
|
+
}).sort((a, b) => a.effectiveFrom.localeCompare(b.effectiveFrom));
|
|
230
|
+
for (let i = 1; i < schedule.length; i++) {
|
|
231
|
+
const previousEnd = schedule[i - 1].effectiveUntil;
|
|
232
|
+
if (previousEnd === void 0 || previousEnd > schedule[i].effectiveFrom) throw new Error("Overlapping or ambiguous rate schedule.");
|
|
233
|
+
}
|
|
234
|
+
const selected = schedule.find((period) => period.effectiveFrom <= input.workDate && (period.effectiveUntil === void 0 || input.workDate < period.effectiveUntil));
|
|
235
|
+
if (!selected) throw new Error("No applicable labour rate for work date.");
|
|
236
|
+
this.selected = selected;
|
|
237
|
+
this.input = {
|
|
238
|
+
tenantId: input.tenantId,
|
|
239
|
+
participantProfileId: input.participantProfileId,
|
|
240
|
+
workDate: input.workDate,
|
|
241
|
+
currency: input.currency,
|
|
242
|
+
billable: input.billable
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
resolve(entry) {
|
|
246
|
+
if (entry.tenantId !== this.input.tenantId || entry.participantKind !== "human" || entry.participantProfileId !== this.input.participantProfileId) throw new Error("Labour entry tenant/worker identity mismatch.");
|
|
247
|
+
const text = entry.durationHoursExact();
|
|
248
|
+
let hours;
|
|
249
|
+
let quantity;
|
|
250
|
+
if (text !== null) {
|
|
251
|
+
if (entry.durationSeconds !== null || entry.startedAt || entry.endedAt) throw new Error("Ambiguous hours and measured duration.");
|
|
252
|
+
hours = decimal(text, "Duration hours");
|
|
253
|
+
quantity = { hours: text };
|
|
254
|
+
} else {
|
|
255
|
+
const seconds = entry.durationSeconds;
|
|
256
|
+
if (!Number.isSafeInteger(seconds) || seconds === null || seconds <= 0) throw new Error("Duration seconds must be a positive safe integer.");
|
|
257
|
+
hours = {
|
|
258
|
+
numerator: BigInt(seconds),
|
|
259
|
+
denominator: 3600n
|
|
260
|
+
};
|
|
261
|
+
quantity = { seconds };
|
|
262
|
+
}
|
|
263
|
+
if (hours.numerator <= 0n) throw new Error("Duration hours must be positive.");
|
|
264
|
+
const customer = amount(hours, this.selected.customer);
|
|
265
|
+
const wage = amount(hours, this.selected.wage);
|
|
266
|
+
const burden = amount(hours, this.selected.burden);
|
|
267
|
+
if (BigInt(wage) + BigInt(burden) > MAX) throw new Error("Loaded labour cost exceeds safe integer minor units.");
|
|
268
|
+
const common = {
|
|
269
|
+
workDate: this.input.workDate,
|
|
270
|
+
effectiveFrom: this.selected.effectiveFrom,
|
|
271
|
+
...this.selected.effectiveUntil === void 0 ? {} : { effectiveUntil: this.selected.effectiveUntil },
|
|
272
|
+
currency: this.input.currency,
|
|
273
|
+
billable: this.input.billable,
|
|
274
|
+
quantity,
|
|
275
|
+
rounding: "half-up-per-component"
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
client: {
|
|
279
|
+
amount: this.input.billable ? customer : 0,
|
|
280
|
+
currency: this.input.currency,
|
|
281
|
+
version: "dated-labour-v1",
|
|
282
|
+
strategy: "dated-hourly-labour",
|
|
283
|
+
terms: {
|
|
284
|
+
...common,
|
|
285
|
+
customer: { ...this.selected.customer }
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
provider: {
|
|
289
|
+
amount: wage,
|
|
290
|
+
currency: this.input.currency,
|
|
291
|
+
version: "dated-labour-v1",
|
|
292
|
+
terms: {
|
|
293
|
+
...common,
|
|
294
|
+
loadedCostMinor: wage + burden,
|
|
295
|
+
wage: {
|
|
296
|
+
...this.selected.wage,
|
|
297
|
+
amount: wage
|
|
298
|
+
},
|
|
299
|
+
burden: {
|
|
300
|
+
...this.selected.burden,
|
|
301
|
+
amount: burden
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/** Validate both results before ServiceEvidenceService freezes an entry. */
|
|
308
|
+
validateEntry(entry) {
|
|
309
|
+
this.resolve(entry);
|
|
310
|
+
}
|
|
311
|
+
/** Client-only terms: no wage, burden or compensation rate provenance. */
|
|
312
|
+
async priceClient(entry) {
|
|
313
|
+
return this.resolve(entry).client;
|
|
314
|
+
}
|
|
315
|
+
/** Provider earnings are wages; private terms also retain burden and loaded cost. */
|
|
316
|
+
async compensateProvider(entry) {
|
|
317
|
+
return this.resolve(entry).provider;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
//#endregion
|
|
164
321
|
//#region src/services/service-evidence-service.ts
|
|
165
322
|
var ServiceEvidenceService = class ServiceEvidenceService {
|
|
166
323
|
constructor(entries, charges, compensation, commercial) {
|
|
@@ -335,6 +492,6 @@ async function deterministicSnapshotId(kind, timeEntryId) {
|
|
|
335
492
|
].join("-");
|
|
336
493
|
}
|
|
337
494
|
//#endregion
|
|
338
|
-
export { AttendanceBreak, AttendanceBreakCollection, AttendanceConflict, AttendancePunch, AttendancePunchCollection, AttendanceReplay, AttendanceReplayCollection, AttendanceService, PeriodRollupService, PeriodTimecard, PeriodTimecardCollection, SERVICE_DURATION_HOURS_EVIDENCE, SERVICE_TIME_ENTRY_FROZEN_FIELDS, SERVICE_TIME_ENTRY_STATUS_TRANSITIONS, ServiceChargeSnapshot, ServiceChargeSnapshotCollection, ServiceCompensationSnapshot, ServiceCompensationSnapshotCollection, ServiceEvidenceService, ServiceTimeEntry, ServiceTimeEntryCollection, TIMESHEETS_MODULE_META, TIMESHEETS_UI_SLOTS, TimecardAdjustment, TimecardAdjustmentCollection };
|
|
495
|
+
export { AttendanceBreak, AttendanceBreakCollection, AttendanceConflict, AttendancePunch, AttendancePunchCollection, AttendanceReplay, AttendanceReplayCollection, AttendanceService, DatedLabourCommercialResolver, PeriodRollupService, PeriodTimecard, PeriodTimecardCollection, SERVICE_DURATION_HOURS_EVIDENCE, SERVICE_TIME_ENTRY_FROZEN_FIELDS, SERVICE_TIME_ENTRY_STATUS_TRANSITIONS, ServiceChargeSnapshot, ServiceChargeSnapshotCollection, ServiceCompensationSnapshot, ServiceCompensationSnapshotCollection, ServiceEvidenceService, ServiceTimeEntry, ServiceTimeEntryCollection, TIMESHEETS_MODULE_META, TIMESHEETS_UI_SLOTS, TimecardAdjustment, TimecardAdjustmentCollection };
|
|
339
496
|
|
|
340
497
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/models/service-snapshots.ts","../src/services/service-evidence-service.ts"],"sourcesContent":["import {\n field,\n foreignKey,\n SmrtCollection,\n SmrtObject,\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(): 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()) 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(): 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()) as this;\n compensationSnapshot.set(this, current);\n return result;\n }\n}\n\n/** Collection over `service_charge_snapshots`. */\nexport class ServiceChargeSnapshotCollection extends SmrtCollection<ServiceChargeSnapshot> {\n static readonly _itemClass = ServiceChargeSnapshot;\n}\n/** Collection over `service_compensation_snapshots`. */\nexport class ServiceCompensationSnapshotCollection extends SmrtCollection<ServiceCompensationSnapshot> {\n static readonly _itemClass = ServiceCompensationSnapshot;\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/models/service-snapshots.ts","../src/services/dated-labour-commercial-resolver.ts","../src/services/service-evidence-service.ts"],"sourcesContent":["import {\n field,\n foreignKey,\n SmrtCollection,\n SmrtObject,\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(): 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()) 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(): 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()) as this;\n compensationSnapshot.set(this, current);\n return result;\n }\n}\n\n/** Collection over `service_charge_snapshots`. */\nexport class ServiceChargeSnapshotCollection extends SmrtCollection<ServiceChargeSnapshot> {\n static readonly _itemClass = ServiceChargeSnapshot;\n}\n/** Collection over `service_compensation_snapshots`. */\nexport class ServiceCompensationSnapshotCollection extends SmrtCollection<ServiceCompensationSnapshot> {\n static readonly _itemClass = ServiceCompensationSnapshot;\n}\n","import type { ServiceTimeEntry } from '../models/service-time-entry.js';\nimport type {\n CommercialSnapshot,\n ServiceCommercialResolver,\n} from './service-evidence-service.js';\n\n/** Explicit hourly rate in currency minor units, with immutable host provenance. */\nexport interface DatedLabourRate {\n id: string;\n version: string;\n /** Nonnegative plain decimal text, at most 18 fractional digits. */\n rateMinor: string;\n}\n\n/** A half-open effective period. An omitted end means no upper bound. */\nexport interface DatedLabourRatePeriod {\n effectiveFrom: string;\n effectiveUntil?: string;\n currency: string;\n customer: DatedLabourRate;\n wage: DatedLabourRate;\n /** Additive hourly burden amount; never a percentage or statutory default. */\n burden: DatedLabourRate;\n}\n\n/**\n * Trusted host input: authorize this tenant, worker and schedule before use.\n * Dates are YYYY-MM-DD calendar dates, never inferred from process time.\n */\nexport interface DatedLabourCommercialInput {\n tenantId: string;\n participantProfileId: string;\n workDate: string;\n currency: string;\n billable: boolean;\n schedule: readonly DatedLabourRatePeriod[];\n}\n\ntype Rational = { numerator: bigint; denominator: bigint };\nconst MAX = BigInt(Number.MAX_SAFE_INTEGER);\n\nfunction decimal(value: string, label: string): Rational {\n if (\n typeof value !== 'string' ||\n value.length > 40 ||\n !/^\\d+(?:\\.\\d{1,18})?$/.test(value)\n )\n throw new Error(`${label} must be nonnegative plain decimal text.`);\n const [whole, fraction = ''] = value.split('.');\n const denominator = 10n ** BigInt(fraction.length);\n const numerator = BigInt(whole + fraction);\n if (numerator > MAX * denominator)\n throw new Error(`${label} exceeds the safe numeric range.`);\n return { numerator, denominator };\n}\n\nfunction date(value: string): void {\n if (\n typeof value !== 'string' ||\n !/^\\d{4}-\\d{2}-\\d{2}$/.test(value) ||\n value.startsWith('0000-')\n )\n throw new Error('Expected a valid YYYY-MM-DD calendar date.');\n const parsed = new Date(`${value}T00:00:00.000Z`);\n if (\n !Number.isFinite(parsed.getTime()) ||\n parsed.toISOString().slice(0, 10) !== value\n )\n throw new Error('Expected a valid YYYY-MM-DD calendar date.');\n}\n\nfunction identity(value: string, label: string): void {\n if (typeof value !== 'string' || !value.trim())\n throw new Error(`${label} is required.`);\n}\n\nfunction rate(value: DatedLabourRate): DatedLabourRate {\n if (!value || typeof value !== 'object') throw new Error('Rate is required.');\n identity(value.id, 'Rate id');\n identity(value.version, 'Rate version');\n decimal(value.rateMinor, 'Hourly rate');\n // Whitelist fields: never copy arbitrary host/private facts to client terms.\n return { id: value.id, version: value.version, rateMinor: value.rateMinor };\n}\n\nfunction amount(hours: Rational, hourly: DatedLabourRate): number {\n const value = decimal(hourly.rateMinor, 'Hourly rate');\n const numerator = hours.numerator * value.numerator;\n const denominator = hours.denominator * value.denominator;\n // Exact nonnegative half-up rounding, once per independent component.\n const result = (2n * numerator + denominator) / (2n * denominator);\n if (result > MAX)\n throw new Error('Labour amount exceeds safe integer minor units.');\n return Number(result);\n}\n\n/**\n * Opt-in effective-dated labour calculator behind ServiceCommercialResolver.\n * Copies selected authorized terms at construction. Retain those same terms\n * when reconstructing a resolver for an incomplete approval retry. Wage and\n * burden are rounded separately. Provider amount is wage only; their sum is\n * private loadedCostMinor. Neither appears in client terms.\n */\nexport class DatedLabourCommercialResolver\n implements ServiceCommercialResolver\n{\n private readonly input: Omit<DatedLabourCommercialInput, 'schedule'>;\n private readonly selected: DatedLabourRatePeriod;\n\n constructor(input: DatedLabourCommercialInput) {\n identity(input.tenantId, 'Tenant id');\n identity(input.participantProfileId, 'Worker profile id');\n date(input.workDate);\n if (\n typeof input.currency !== 'string' ||\n !/^[A-Z]{3}$/.test(input.currency)\n )\n throw new Error(\n 'Currency must be an explicit three-letter uppercase code.',\n );\n if (typeof input.billable !== 'boolean')\n throw new Error('Billable must be explicit.');\n if (!Array.isArray(input.schedule) || !input.schedule.length)\n throw new Error('A rate schedule is required.');\n const schedule = input.schedule\n .map((period) => {\n date(period.effectiveFrom);\n if (period.effectiveUntil !== undefined) {\n date(period.effectiveUntil);\n if (period.effectiveUntil <= period.effectiveFrom)\n throw new Error('Effective period must have positive length.');\n }\n if (period.currency !== input.currency)\n throw new Error('Rate schedule currency mismatch.');\n return {\n effectiveFrom: period.effectiveFrom,\n effectiveUntil: period.effectiveUntil,\n currency: period.currency,\n customer: rate(period.customer),\n wage: rate(period.wage),\n burden: rate(period.burden),\n };\n })\n .sort((a, b) => a.effectiveFrom.localeCompare(b.effectiveFrom));\n for (let i = 1; i < schedule.length; i++) {\n const previousEnd = schedule[i - 1].effectiveUntil;\n if (previousEnd === undefined || previousEnd > schedule[i].effectiveFrom)\n throw new Error('Overlapping or ambiguous rate schedule.');\n }\n const selected = schedule.find(\n (period) =>\n period.effectiveFrom <= input.workDate &&\n (period.effectiveUntil === undefined ||\n input.workDate < period.effectiveUntil),\n );\n if (!selected) throw new Error('No applicable labour rate for work date.');\n this.selected = selected;\n this.input = {\n tenantId: input.tenantId,\n participantProfileId: input.participantProfileId,\n workDate: input.workDate,\n currency: input.currency,\n billable: input.billable,\n };\n }\n\n private resolve(entry: ServiceTimeEntry) {\n if (\n entry.tenantId !== this.input.tenantId ||\n entry.participantKind !== 'human' ||\n entry.participantProfileId !== this.input.participantProfileId\n )\n throw new Error('Labour entry tenant/worker identity mismatch.');\n const text = entry.durationHoursExact();\n let hours: Rational;\n let quantity: { hours: string } | { seconds: number };\n if (text !== null) {\n if (entry.durationSeconds !== null || entry.startedAt || entry.endedAt)\n throw new Error('Ambiguous hours and measured duration.');\n hours = decimal(text, 'Duration hours');\n quantity = { hours: text };\n } else {\n const seconds = entry.durationSeconds;\n if (!Number.isSafeInteger(seconds) || seconds === null || seconds <= 0)\n throw new Error('Duration seconds must be a positive safe integer.');\n hours = { numerator: BigInt(seconds), denominator: 3600n };\n quantity = { seconds };\n }\n if (hours.numerator <= 0n)\n throw new Error('Duration hours must be positive.');\n const customer = amount(hours, this.selected.customer);\n const wage = amount(hours, this.selected.wage);\n const burden = amount(hours, this.selected.burden);\n if (BigInt(wage) + BigInt(burden) > MAX)\n throw new Error('Loaded labour cost exceeds safe integer minor units.');\n const common = {\n workDate: this.input.workDate,\n effectiveFrom: this.selected.effectiveFrom,\n ...(this.selected.effectiveUntil === undefined\n ? {}\n : { effectiveUntil: this.selected.effectiveUntil }),\n currency: this.input.currency,\n billable: this.input.billable,\n quantity,\n rounding: 'half-up-per-component',\n };\n return {\n client: {\n amount: this.input.billable ? customer : 0,\n currency: this.input.currency,\n version: 'dated-labour-v1',\n strategy: 'dated-hourly-labour',\n terms: { ...common, customer: { ...this.selected.customer } },\n },\n provider: {\n amount: wage,\n currency: this.input.currency,\n version: 'dated-labour-v1',\n terms: {\n ...common,\n loadedCostMinor: wage + burden,\n wage: { ...this.selected.wage, amount: wage },\n burden: { ...this.selected.burden, amount: burden },\n },\n },\n };\n }\n\n /** Validate both results before ServiceEvidenceService freezes an entry. */\n validateEntry(entry: ServiceTimeEntry): void {\n this.resolve(entry);\n }\n\n /** Client-only terms: no wage, burden or compensation rate provenance. */\n async priceClient(entry: ServiceTimeEntry): Promise<CommercialSnapshot> {\n return this.resolve(entry).client;\n }\n\n /** Provider earnings are wages; private terms also retain burden and loaded cost. */\n async compensateProvider(\n entry: ServiceTimeEntry,\n ): Promise<CommercialSnapshot> {\n return this.resolve(entry).provider;\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAiBO,SAAS,yBAAyB,OAAe,QAAuB;CAC7E,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,cAAc,MAAM,GAC5D,MAAM,IAAI,gBACR,GAAG,MAAK,0EACC,OAAO,MAAM,EAAC,oBACvB,4BACA;EACE,WAAW;EACX,OAAO;EACP,cAAc;CAChB,CACF;AAEJ;AAEA,IAAM,iCAAiB,IAAI,QAAuC;AAClE,IAAM,uCAAuB,IAAI,QAA6C;AAE9E,eAAe,4BACb,QACA,QACwB;CACxB,IAAI,CAAC,OAAO,IAAI,OAAO;CACvB,IAAI;EACF,MAAM,MAAM,MAAM,OAAO,GAAG,IAAI,OAAO,WAAW,EAAE,IAAI,OAAO,GAAG,CAAC;EACnE,IAAI,CAAC,KAAK,OAAO;EACjB,OAAO,KAAK,UACV,OAAO,YACL,OAAO,KAAK,QAAQ,CAClB,KACA,IAAI,IAAI,QAAQ,WAAW,MAAM,IAAI,EAAE,YAAY,GAAG,MAAM,IAC9D,CAAC,CACH,CACF;CACF,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,mBACP,QACA,QACQ;CACR,OAAO,KAAK,UACV,OAAO,YAAY,OAAO,KAAK,QAAQ,CAAC,KAAK,OAAO,QAAQ,IAAI,CAAC,CAAC,CACpE;AACF;AAEA,IAAM,gBAAgB;CACpB;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AACA,IAAM,sBAAsB;CAC1B;CACA;CACA;CACA;CACA;AACF;AAkBO,IAAM,wBAAN,cAAoC,WAAW;CACtB,WAA0B;CACJ,cAAsB;;;;;;;;;;CAU1E,SAAiB;CACQ,WAAmB;CACnB,iBAAyB;CACzB,WAAmB;CACnB,eAAuB;CACvB,kBAA0B;CACnD,4BAAkB,IAAI,KAAK;CAE3B,MAAe,aAA4B;EACzC,MAAM,MAAM,WAAW;EACvB,IAAI,MAAM,KAAK,QAAQ,GACrB,eAAe,IACb,MACA,mBACE,MACA,aACF,CACF;EACF,OAAO;CACT;CAEA,MAAe,OAAsB;EACnC,yBAAyB,yBAAyB,KAAK,MAAM;EAC7D,MAAM,UAAU,mBACd,MACA,aACF;EACA,MAAM,QACH,MAAM,4BAA4B,MAAM,aAAa,KACtD,eAAe,IAAI,IAAI,KACvB;EACF,IAAI,SAAS,UAAU,SACrB,MAAM,IAAI,MACR,4EACF;EACF,MAAM,SAAU,MAAM,MAAM,KAAK;EACjC,eAAe,IAAI,MAAM,OAAO;EAChC,OAAO;CACT;AACF;AAnDwD,cAA3C,uBAAkD,mBAAkB,gCAAA;AACjD,gBAAA,CAA7B,SAAS,EAAE,UAAU,KAAK,CAAC,CAAA,GADjB,sBACmB,WAAA,YAAA,CAAA;AACsB,gBAAA,CAAnD,WAAW,oBAAoB,EAAE,UAAU,KAAK,CAAC,CAAA,GAFvC,sBAEyC,WAAA,eAAA,CAAA;AAW3B,gBAAA,CAAxB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAA,GAbZ,sBAac,WAAA,YAAA,CAAA;AACA,gBAAA,CAAxB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAA,GAdZ,sBAcc,WAAA,kBAAA,CAAA;AACA,gBAAA,CAAxB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAA,GAfZ,sBAec,WAAA,YAAA,CAAA;AACA,gBAAA,CAAxB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAA,GAhBZ,sBAgBc,WAAA,gBAAA,CAAA;AACA,gBAAA,CAAxB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAA,GAjBZ,sBAiBc,WAAA,mBAAA,CAAA;AAjBd,wBAAN,gBAAA,CAZN,aAAa,EAAE,MAAM,WAAW,CAAC,GACjC,KAAK;CACJ,WAAW;CAEX,wBAAwB,CACtB,oDACF;CACA,iBAAiB,CAAC,eAAe;CACjC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;AAClC,CAAC,CAAA,GACY,qBAAA;AAqEN,IAAM,8BAAN,cAA0C,WAAW;CAC5B,WAA0B;CACJ,cAAsB;;;;;;;;CAQ1E,SAAiB;CACQ,WAAmB;CACnB,eAAuB;CACvB,eAAuB;CAChD,4BAAkB,IAAI,KAAK;CAE3B,MAAe,aAA4B;EACzC,MAAM,MAAM,WAAW;EACvB,IAAI,MAAM,KAAK,QAAQ,GACrB,qBAAqB,IACnB,MACA,mBACE,MACA,mBACF,CACF;EACF,OAAO;CACT;CAEA,MAAe,OAAsB;EACnC,yBAAyB,+BAA+B,KAAK,MAAM;EACnE,MAAM,UAAU,mBACd,MACA,mBACF;EACA,MAAM,QACH,MAAM,4BAA4B,MAAM,mBAAmB,KAC5D,qBAAqB,IAAI,IAAI,KAC7B;EACF,IAAI,SAAS,UAAU,SACrB,MAAM,IAAI,MACR,wFACF;EACF,MAAM,SAAU,MAAM,MAAM,KAAK;EACjC,qBAAqB,IAAI,MAAM,OAAO;EACtC,OAAO;CACT;AACF;AA/C8D,cAAjD,6BAAwD,mBAAkB,gCAAA;AACvD,gBAAA,CAA7B,SAAS,EAAE,UAAU,KAAK,CAAC,CAAA,GADjB,4BACmB,WAAA,YAAA,CAAA;AACsB,gBAAA,CAAnD,WAAW,oBAAoB,EAAE,UAAU,KAAK,CAAC,CAAA,GAFvC,4BAEyC,WAAA,eAAA,CAAA;AAS3B,gBAAA,CAAxB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAA,GAXZ,4BAWc,WAAA,YAAA,CAAA;AACA,gBAAA,CAAxB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAA,GAZZ,4BAYc,WAAA,gBAAA,CAAA;AACA,gBAAA,CAAxB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAA,GAbZ,4BAac,WAAA,gBAAA,CAAA;AAbd,8BAAN,gBAAA,CAZN,aAAa,EAAE,MAAM,WAAW,CAAC,GACjC,KAAK;CACJ,WAAW;CAEX,wBAAwB,CACtB,0DACF;CACA,iBAAiB,CAAC,eAAe;CACjC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;AAClC,CAAC,CAAA,GACY,2BAAA;AAkDN,IAAM,kCAAN,cAA8C,eAAsC;CACzF,OAAgB,aAAa;AAC/B;AAEO,IAAM,wCAAN,cAAoD,eAA4C;CACrG,OAAgB,aAAa;AAC/B;;;ACxLA,IAAM,MAAM,OAAO,OAAO,gBAAgB;AAE1C,SAAS,QAAQ,OAAe,OAAyB;CACvD,IACE,OAAO,UAAU,YACjB,MAAM,SAAS,MACf,CAAC,uBAAuB,KAAK,KAAK,GAElC,MAAM,IAAI,MAAM,GAAG,MAAK,yCAA0C;CACpE,MAAM,CAAC,OAAO,WAAW,MAAM,MAAM,MAAM,GAAG;CAC9C,MAAM,cAAc,OAAO,OAAO,SAAS,MAAM;CACjD,MAAM,YAAY,OAAO,QAAQ,QAAQ;CACzC,IAAI,YAAY,MAAM,aACpB,MAAM,IAAI,MAAM,GAAG,MAAK,iCAAkC;CAC5D,OAAO;EAAE;EAAW;CAAY;AAClC;AAEA,SAAS,KAAK,OAAqB;CACjC,IACE,OAAO,UAAU,YACjB,CAAC,sBAAsB,KAAK,KAAK,KACjC,MAAM,WAAW,OAAO,GAExB,MAAM,IAAI,MAAM,4CAA4C;CAC9D,MAAM,yBAAS,IAAI,KAAK,GAAG,MAAK,eAAgB;CAChD,IACE,CAAC,OAAO,SAAS,OAAO,QAAQ,CAAC,KACjC,OAAO,YAAY,CAAA,CAAE,MAAM,GAAG,EAAE,MAAM,OAEtC,MAAM,IAAI,MAAM,4CAA4C;AAChE;AAEA,SAAS,SAAS,OAAe,OAAqB;CACpD,IAAI,OAAO,UAAU,YAAY,CAAC,MAAM,KAAK,GAC3C,MAAM,IAAI,MAAM,GAAG,MAAK,cAAe;AAC3C;AAEA,SAAS,KAAK,OAAyC;CACrD,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,MAAM,IAAI,MAAM,mBAAmB;CAC5E,SAAS,MAAM,IAAI,SAAS;CAC5B,SAAS,MAAM,SAAS,cAAc;CACtC,QAAQ,MAAM,WAAW,aAAa;CAEtC,OAAO;EAAE,IAAI,MAAM;EAAI,SAAS,MAAM;EAAS,WAAW,MAAM;CAAU;AAC5E;AAEA,SAAS,OAAO,OAAiB,QAAiC;CAChE,MAAM,QAAQ,QAAQ,OAAO,WAAW,aAAa;CACrD,MAAM,YAAY,MAAM,YAAY,MAAM;CAC1C,MAAM,cAAc,MAAM,cAAc,MAAM;CAE9C,MAAM,UAAU,KAAK,YAAY,gBAAgB,KAAK;CACtD,IAAI,SAAS,KACX,MAAM,IAAI,MAAM,iDAAiD;CACnE,OAAO,OAAO,MAAM;AACtB;AASO,IAAM,gCAAN,MAEP;CACmB;CACA;CAEjB,YAAY,OAAmC;EAC7C,SAAS,MAAM,UAAU,WAAW;EACpC,SAAS,MAAM,sBAAsB,mBAAmB;EACxD,KAAK,MAAM,QAAQ;EACnB,IACE,OAAO,MAAM,aAAa,YAC1B,CAAC,aAAa,KAAK,MAAM,QAAQ,GAEjC,MAAM,IAAI,MACR,2DACF;EACF,IAAI,OAAO,MAAM,aAAa,WAC5B,MAAM,IAAI,MAAM,4BAA4B;EAC9C,IAAI,CAAC,MAAM,QAAQ,MAAM,QAAQ,KAAK,CAAC,MAAM,SAAS,QACpD,MAAM,IAAI,MAAM,8BAA8B;EAChD,MAAM,WAAW,MAAM,SACpB,KAAK,WAAW;GACf,KAAK,OAAO,aAAa;GACzB,IAAI,OAAO,mBAAmB,KAAA,GAAW;IACvC,KAAK,OAAO,cAAc;IAC1B,IAAI,OAAO,kBAAkB,OAAO,eAClC,MAAM,IAAI,MAAM,6CAA6C;GACjE;GACA,IAAI,OAAO,aAAa,MAAM,UAC5B,MAAM,IAAI,MAAM,kCAAkC;GACpD,OAAO;IACL,eAAe,OAAO;IACtB,gBAAgB,OAAO;IACvB,UAAU,OAAO;IACjB,UAAU,KAAK,OAAO,QAAQ;IAC9B,MAAM,KAAK,OAAO,IAAI;IACtB,QAAQ,KAAK,OAAO,MAAM;GAC5B;EACF,CAAC,CAAA,CACA,MAAM,GAAG,MAAM,EAAE,cAAc,cAAc,EAAE,aAAa,CAAC;EAChE,KAAA,IAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;GACxC,MAAM,cAAc,SAAS,IAAI,EAAC,CAAE;GACpC,IAAI,gBAAgB,KAAA,KAAa,cAAc,SAAS,EAAC,CAAE,eACzD,MAAM,IAAI,MAAM,yCAAyC;EAC7D;EACA,MAAM,WAAW,SAAS,MACvB,WACC,OAAO,iBAAiB,MAAM,aAC7B,OAAO,mBAAmB,KAAA,KACzB,MAAM,WAAW,OAAO,eAC9B;EACA,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,0CAA0C;EACzE,KAAK,WAAW;EAChB,KAAK,QAAQ;GACX,UAAU,MAAM;GAChB,sBAAsB,MAAM;GAC5B,UAAU,MAAM;GAChB,UAAU,MAAM;GAChB,UAAU,MAAM;EAClB;CACF;CAEQ,QAAQ,OAAyB;EACvC,IACE,MAAM,aAAa,KAAK,MAAM,YAC9B,MAAM,oBAAoB,WAC1B,MAAM,yBAAyB,KAAK,MAAM,sBAE1C,MAAM,IAAI,MAAM,+CAA+C;EACjE,MAAM,OAAO,MAAM,mBAAmB;EACtC,IAAI;EACJ,IAAI;EACJ,IAAI,SAAS,MAAM;GACjB,IAAI,MAAM,oBAAoB,QAAQ,MAAM,aAAa,MAAM,SAC7D,MAAM,IAAI,MAAM,wCAAwC;GAC1D,QAAQ,QAAQ,MAAM,gBAAgB;GACtC,WAAW,EAAE,OAAO,KAAK;EAC3B,OAAO;GACL,MAAM,UAAU,MAAM;GACtB,IAAI,CAAC,OAAO,cAAc,OAAO,KAAK,YAAY,QAAQ,WAAW,GACnE,MAAM,IAAI,MAAM,mDAAmD;GACrE,QAAQ;IAAE,WAAW,OAAO,OAAO;IAAG,aAAa;GAAM;GACzD,WAAW,EAAE,QAAQ;EACvB;EACA,IAAI,MAAM,aAAa,IACrB,MAAM,IAAI,MAAM,kCAAkC;EACpD,MAAM,WAAW,OAAO,OAAO,KAAK,SAAS,QAAQ;EACrD,MAAM,OAAO,OAAO,OAAO,KAAK,SAAS,IAAI;EAC7C,MAAM,SAAS,OAAO,OAAO,KAAK,SAAS,MAAM;EACjD,IAAI,OAAO,IAAI,IAAI,OAAO,MAAM,IAAI,KAClC,MAAM,IAAI,MAAM,sDAAsD;EACxE,MAAM,SAAS;GACb,UAAU,KAAK,MAAM;GACrB,eAAe,KAAK,SAAS;GAC7B,GAAI,KAAK,SAAS,mBAAmB,KAAA,IACjC,CAAC,IACD,EAAE,gBAAgB,KAAK,SAAS,eAAe;GACnD,UAAU,KAAK,MAAM;GACrB,UAAU,KAAK,MAAM;GACrB;GACA,UAAU;EACZ;EACA,OAAO;GACL,QAAQ;IACN,QAAQ,KAAK,MAAM,WAAW,WAAW;IACzC,UAAU,KAAK,MAAM;IACrB,SAAS;IACT,UAAU;IACV,OAAO;KAAE,GAAG;KAAQ,UAAU,EAAE,GAAG,KAAK,SAAS,SAAS;IAAE;GAC9D;GACA,UAAU;IACR,QAAQ;IACR,UAAU,KAAK,MAAM;IACrB,SAAS;IACT,OAAO;KACL,GAAG;KACH,iBAAiB,OAAO;KACxB,MAAM;MAAE,GAAG,KAAK,SAAS;MAAM,QAAQ;KAAK;KAC5C,QAAQ;MAAE,GAAG,KAAK,SAAS;MAAQ,QAAQ;KAAO;IACpD;GACF;EACF;CACF;;CAGA,cAAc,OAA+B;EAC3C,KAAK,QAAQ,KAAK;CACpB;;CAGA,MAAM,YAAY,OAAsD;EACtE,OAAO,KAAK,QAAQ,KAAK,CAAA,CAAE;CAC7B;;CAGA,MAAM,mBACJ,OAC6B;EAC7B,OAAO,KAAK,QAAQ,KAAK,CAAA,CAAE;CAC7B;AACF;;;AC9KO,IAAM,yBAAN,MAAM,uBAAuB;CAClC,YACmB,SACA,SACA,cACA,YACjB;EAJiB,KAAA,UAAA;EACA,KAAA,UAAA;EACA,KAAA,eAAA;EACA,KAAA,aAAA;CAChB;CAJgB;CACA;CACA;CACA;CAGnB,aAAa,OACX,UAA4B,CAAC,GAC7B,YACiC;EACjC,MAAM,CAAC,SAAS,SAAS,gBAAgB,MAAM,QAAQ,IAAI;GACzD,2BAA2B,OAAO,OAAO;GACzC,gCAAgC,OAAO,OAAO;GAC9C,sCAAsC,OAAO,OAAO;EACtD,CAAC;EACD,OAAO,IAAI,uBACT,SACA,SACA,cACA,UACF;CACF;CAEA,MAAM,OAAO,OAA0D;EACrE,IAAI,CAAC;GAAC;GAAS;GAAU;GAAU;EAAO,CAAA,CAAE,SAAS,MAAM,MAAM,GAC/D,MAAM,IAAI,MAAM,kCAAkC;EACpD,IAAI,QAAQ,MAAM,WAAW,MAAM,QAAQ,MAAM,SAAS,GACxD,MAAM,IAAI,MAAM,sDAAsD;EACxE,IAAI,EAAE,MAAM,eAAe,MAAM,YAC/B,MAAM,IAAI,MAAM,yCAAyC;EAC3D,IAAI,MAAM,oBAAoB,WAAW,CAAC,MAAM,sBAC9C,MAAM,IAAI,MAAM,mDAAmD;EACrE,IAAI,MAAM,oBAAoB,WAAW,CAAC,MAAM,UAC9C,MAAM,IAAI,MAAM,uCAAuC;EACzD,IAAI,MAAM,WAAW,YAAY,CAAC,MAAM,aAAa,CAAC,MAAM,UAC1D,MAAM,IAAI,MAAM,oDAAoD;EACtE,IAAI,MAAM,aAAa,MAAM,WAAW,MAAM,aAAa,MAAM,SAC/D,MAAM,IAAI,MAAM,gDAAgD;EAClE,MAAM,EAAE,eAAe,GAAG,WAAW;EACrC,IACE,MAAM,UAAU,MACb,SAAS,MAAM,SAAA,+CAClB,GAEA,MAAM,IAAI,MACR,8DACF;EACF,MAAM,QACJ,kBAAkB,KAAA,IAAY,OAAO,sBAAsB,aAAa;EAC1E,IACE,UAAU,SACT,MAAM,mBAAmB,QAAQ,MAAM,aAAa,MAAM,UAE3D,MAAM,IAAI,MACR,2EACF;EACF,IAAI,UAAU,QAAQ,MAAM,oBAAoB,MAC9C,MAAM,IAAI,MAAM,8CAA8C;EAChE,MAAM,WACJ,UAAU,OACN,OACC,MAAM,oBACN,MAAM,aAAa,MAAM,UACtB,KAAK,OACF,MAAM,QAAQ,QAAQ,IAAI,MAAM,UAAU,QAAQ,KAAK,GAC1D,IACA;EACV,IAAI,aAAa,SAAS,CAAC,OAAO,cAAc,QAAQ,KAAK,YAAY,IACvE,MAAM,IAAI,MACR,0DACF;EACF,MAAM,QAAQ,MAAM,KAAK,QAAQ,OAAO;GACtC,GAAG;GACH,iBAAiB;GACjB,UAAU,KAAK,UAAU,CACvB,GAAI,MAAM,YAAY,CAAC,GACvB,GAAI,UAAU,OACV,CAAC,IACD,CAAC;IAAE,MAAM;IAAiC;GAAM,CAAC,CACvD,CAAC;GACD,UAAU,KAAK,UAAU,MAAM,YAAY,CAAC,CAAC;GAC7C,QAAQ;GACR,aAAa;GACb,sBAAsB;GACtB,YAAY;GACZ,qBAAqB;GACrB,cAAc;GACd,YAAY;GACZ,qBAAqB;GACrB,iBAAiB;GACjB,gBAAgB;EAClB,CAAC;EACD,MAAM,MAAM,KAAK;EACjB,OAAO;CACT;CAEA,MAAM,OACJ,OACA,WAC2B;EAC3B,IAAI,MAAM,WAAW,WAAW,MAAM,WAAW,YAC/C,MAAM,IAAI,MAAM,uDAAuD;EACzE,MAAM,SAAS;EACf,MAAM,8BAAc,IAAI,KAAK;EAC7B,MAAM,uBAAuB,aAAa;EAC1C,OAAO,MAAM,KAAK;CACpB;CAEA,MAAM,QACJ,OACA,SAC2B;EAC3B,IAAI,MAAM,WAAW,eAAe,MAAM,WAAW,YACnD,MAAM,IAAI,MACR,wEACF;EACF,IAAI,CAAC,KAAK,YACR,MAAM,IAAI,MACR,kEACF;EACF,MAAM,cAAc,MAAM;EAC1B,IAAI,CAAC,aACH,MAAM,IAAI,MAAM,iDAAiD;EACnE,MAAM,kBACJ,MAAM,KAAK,QAAQ,KAAK;GAAE,OAAO,EAAE,YAAY;GAAG,OAAO;EAAE,CAAC,EAAA,CAC5D;EACF,MAAM,wBACJ,MAAM,KAAK,aAAa,KAAK;GAAE,OAAO,EAAE,YAAY;GAAG,OAAO;EAAE,CAAC,EAAA,CACjE;EACF,IAAI,MAAM,WAAW,cAAc,kBAAkB,sBACnD,OAAO;EAET,MAAM,KAAK,WAAW,gBAAgB,KAAK;EAK3C,IAAI,MAAM,WAAW,aAAa;GAChC,MAAM,SAAS;GACf,MAAM,6BAAa,IAAI,KAAK;GAC5B,MAAM,sBAAsB,QAAQ,kBAAkB;GACtD,MAAM,eAAe,QAAQ;GAC7B,IAAI;IACF,MAAM,MAAM,KAAK;GACnB,SAAS,OAAO;IAKd,IAAI,EAHF,iBAAiB,SACjB,UAAU,SACV,MAAM,SAAS,8BACM,MAAM;IAC7B,MAAM,aAAa,MAAM,KAAK,QAAQ,IAAI,WAAW;IACrD,IAAI,YAAY,WAAW,YAAY,MAAM;IAC7C,QAAQ;IACR,MAAM,KAAK,WAAW,gBAAgB,KAAK;GAC7C;EACF;EACA,MAAM,WAAW,uBACb,OACA,MAAM,KAAK,WAAW,mBAAmB,KAAK;EAClD,IAAI,UAAU;GACZ,MAAM,aAAa,MAAM,wBACvB,iCACA,WACF;GACA,IAAI;IACF,MAAM,KAAK,aAAa,OAAO;KAC7B,IAAI;KACJ,UAAU,MAAM;KAChB;KACA,QAAQ,SAAS;KACjB,UAAU,SAAS,YAAY;KAC/B,cAAc,SAAS;KACvB,cAAc,KAAK,UAAU,SAAS,KAAK;KAC3C,aAAa;IACf,CAAC;GACH,SAAS,OAAO;IASd,IAAI,EAPD,MAAM,KAAK,aAAa,IAAI,UAAU,MAErC,MAAM,KAAK,aAAa,KAAK;KAC3B,OAAO,EAAE,YAAY;KACrB,OAAO;IACT,CAAC,EAAA,CACD,KACa,MAAM;GACzB;EACF;EACA,MAAM,SAAS,iBACX,OACA,MAAM,KAAK,WAAW,YAAY,KAAK;EAC3C,IAAI,QAAQ;GACV,MAAM,aAAa,MAAM,wBACvB,2BACA,WACF;GACA,IAAI;IACF,MAAM,KAAK,QAAQ,OAAO;KACxB,IAAI;KACJ,UAAU,MAAM;KAChB;KACA,QAAQ,OAAO;KACf,UAAU,OAAO,YAAY;KAC7B,gBAAgB,OAAO;KACvB,UAAU,OAAO,YAAY;KAC7B,cAAc,KAAK,UAAU,OAAO,KAAK;KACzC,iBAAiB,OAAO,aAAa;KACrC,aAAa;IACf,CAAC;GACH,SAAS,OAAO;IASd,IAAI,EAPD,MAAM,KAAK,QAAQ,IAAI,UAAU,MAEhC,MAAM,KAAK,QAAQ,KAAK;KACtB,OAAO,EAAE,YAAY;KACrB,OAAO;IACT,CAAC,EAAA,CACD,KACa,MAAM;GACzB;EACF;EACA,OAAO;CACT;CAEA,MAAM,QACJ,OACA,OAC2B;EAC3B,IAAI,MAAM,WAAW,YACnB,MAAM,IAAI,MAAM,8CAA8C;EAChE,IAAI,MAAM,aAAa,KAAA,KAAa,MAAM,aAAa,MAAM,UAC3D,MAAM,IAAI,MACR,8DACF;EAEF,MAAM,aAAa,MAAM,KAAK,OAAO;GACnC,GAAG;GACH,UAAU,MAAM;EAClB,CAAC;EACD,WAAW,iBAAiB,MAAM,MAAM;EACxC,MAAM,WAAW,KAAK;EACtB,MAAM,SAAS;EACf,MAAM,MAAM,KAAK;EACjB,OAAO;CACT;AACF;AAEA,eAAe,wBACb,MACA,aACiB;CACjB,MAAM,QAAQ,IAAI,YAAY,CAAA,CAAE,OAAO,KAAK,UAAU,CAAC,MAAM,WAAW,CAAC,CAAC;CAE1E,MAAM,OAAO,IADM,WAAW,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK,CAC5D,CAAA,CAAO,MAAM,GAAG,EAAE;CAC/B,KAAK,KAAM,KAAK,KAAK,KAAQ;CAC7B,KAAK,KAAM,KAAK,KAAK,KAAQ;CAC7B,MAAM,MAAM,MAAM,KAAK,OAAO,SAC5B,KAAK,SAAS,EAAE,CAAA,CAAE,SAAS,GAAG,GAAG,CACnC,CAAA,CAAE,KAAK,EAAE;CACT,OAAO;EACL,IAAI,MAAM,GAAG,CAAC;EACd,IAAI,MAAM,GAAG,EAAE;EACf,IAAI,MAAM,IAAI,EAAE;EAChB,IAAI,MAAM,IAAI,EAAE;EAChB,IAAI,MAAM,EAAE;CACd,CAAA,CAAE,KAAK,GAAG;AACZ"}
|
package/dist/manifest.json
CHANGED
package/dist/rollup.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as PeriodTimecard, c as TimecardAdjustmentCollection, o as PeriodTimecardCollection, s as TimecardAdjustment, t as PeriodRollupService } from "./chunks/rollup-
|
|
1
|
+
import { a as PeriodTimecard, c as TimecardAdjustmentCollection, o as PeriodTimecardCollection, s as TimecardAdjustment, t as PeriodRollupService } from "./chunks/rollup-BEETso9S.js";
|
|
2
2
|
export { PeriodRollupService, PeriodTimecard, PeriodTimecardCollection, TimecardAdjustment, TimecardAdjustmentCollection };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ServiceTimeEntry } from '../models/service-time-entry.js';
|
|
2
|
+
import type { CommercialSnapshot, ServiceCommercialResolver } from './service-evidence-service.js';
|
|
3
|
+
/** Explicit hourly rate in currency minor units, with immutable host provenance. */
|
|
4
|
+
export interface DatedLabourRate {
|
|
5
|
+
id: string;
|
|
6
|
+
version: string;
|
|
7
|
+
/** Nonnegative plain decimal text, at most 18 fractional digits. */
|
|
8
|
+
rateMinor: string;
|
|
9
|
+
}
|
|
10
|
+
/** A half-open effective period. An omitted end means no upper bound. */
|
|
11
|
+
export interface DatedLabourRatePeriod {
|
|
12
|
+
effectiveFrom: string;
|
|
13
|
+
effectiveUntil?: string;
|
|
14
|
+
currency: string;
|
|
15
|
+
customer: DatedLabourRate;
|
|
16
|
+
wage: DatedLabourRate;
|
|
17
|
+
/** Additive hourly burden amount; never a percentage or statutory default. */
|
|
18
|
+
burden: DatedLabourRate;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Trusted host input: authorize this tenant, worker and schedule before use.
|
|
22
|
+
* Dates are YYYY-MM-DD calendar dates, never inferred from process time.
|
|
23
|
+
*/
|
|
24
|
+
export interface DatedLabourCommercialInput {
|
|
25
|
+
tenantId: string;
|
|
26
|
+
participantProfileId: string;
|
|
27
|
+
workDate: string;
|
|
28
|
+
currency: string;
|
|
29
|
+
billable: boolean;
|
|
30
|
+
schedule: readonly DatedLabourRatePeriod[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Opt-in effective-dated labour calculator behind ServiceCommercialResolver.
|
|
34
|
+
* Copies selected authorized terms at construction. Retain those same terms
|
|
35
|
+
* when reconstructing a resolver for an incomplete approval retry. Wage and
|
|
36
|
+
* burden are rounded separately. Provider amount is wage only; their sum is
|
|
37
|
+
* private loadedCostMinor. Neither appears in client terms.
|
|
38
|
+
*/
|
|
39
|
+
export declare class DatedLabourCommercialResolver implements ServiceCommercialResolver {
|
|
40
|
+
private readonly input;
|
|
41
|
+
private readonly selected;
|
|
42
|
+
constructor(input: DatedLabourCommercialInput);
|
|
43
|
+
private resolve;
|
|
44
|
+
/** Validate both results before ServiceEvidenceService freezes an entry. */
|
|
45
|
+
validateEntry(entry: ServiceTimeEntry): void;
|
|
46
|
+
/** Client-only terms: no wage, burden or compensation rate provenance. */
|
|
47
|
+
priceClient(entry: ServiceTimeEntry): Promise<CommercialSnapshot>;
|
|
48
|
+
/** Provider earnings are wages; private terms also retain burden and loaded cost. */
|
|
49
|
+
compensateProvider(entry: ServiceTimeEntry): Promise<CommercialSnapshot>;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=dated-labour-commercial-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dated-labour-commercial-resolver.d.ts","sourceRoot":"","sources":["../../src/services/dated-labour-commercial-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EACV,kBAAkB,EAClB,yBAAyB,EAC1B,MAAM,+BAA+B,CAAC;AAEvC,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,yEAAyE;AACzE,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,eAAe,CAAC;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,8EAA8E;IAC9E,MAAM,EAAE,eAAe,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,SAAS,qBAAqB,EAAE,CAAC;CAC5C;AA4DD;;;;;;GAMG;AACH,qBAAa,6BACX,YAAW,yBAAyB;IAEpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+C;IACrE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;gBAErC,KAAK,EAAE,0BAA0B;IAyD7C,OAAO,CAAC,OAAO;IA8Df,4EAA4E;IAC5E,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAI5C,0EAA0E;IACpE,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAIvE,qFAAqF;IAC/E,kBAAkB,CACtB,KAAK,EAAE,gBAAgB,GACtB,OAAO,CAAC,kBAAkB,CAAC;CAG/B"}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,+BAA+B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-timesheets",
|
|
6
|
-
"packageVersion": "0.55.
|
|
6
|
+
"packageVersion": "0.55.14",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "6c53ad3e5bd980009a1a0a530c39eed4b995966873b34be16ba7812ea4769d8b",
|
|
11
|
+
"packageJson": "c69988b2116750130a37e9d9a8c3be49a4a311db93f6cd0faa9c5a77d5103da4",
|
|
12
12
|
"agents": "46a4e8437424b494645e90ce4a4869be04956c5da6a42f89697cbd3ab2199a7e",
|
|
13
13
|
"moduleDoc:agents/period-rollup.md": "5fb741659d81dbef2e4850c10c2d65f09e3f5a83e8a88e2e0f19585f25158a6a"
|
|
14
14
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-timesheets",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.14",
|
|
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,10 +39,10 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@happyvertical/smrt-core": "0.55.
|
|
43
|
-
"@happyvertical/smrt-tenancy": "0.55.
|
|
44
|
-
"@happyvertical/smrt-types": "0.55.
|
|
45
|
-
"@happyvertical/smrt-ui": "0.55.
|
|
42
|
+
"@happyvertical/smrt-core": "0.55.14",
|
|
43
|
+
"@happyvertical/smrt-tenancy": "0.55.14",
|
|
44
|
+
"@happyvertical/smrt-types": "0.55.14",
|
|
45
|
+
"@happyvertical/smrt-ui": "0.55.14",
|
|
46
46
|
"@happyvertical/sql": "^0.103.2"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@happyvertical/smrt-vitest": "0.55.
|
|
57
|
+
"@happyvertical/smrt-vitest": "0.55.14",
|
|
58
58
|
"@sveltejs/package": "^2.5.8",
|
|
59
59
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
60
60
|
"@types/node": "24.13.6",
|