@happyvertical/smrt-support 0.39.2
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 +270 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4358 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +6770 -0
- package/dist/models/index.d.ts +17 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/service-time-entry.d.ts +75 -0
- package/dist/models/service-time-entry.d.ts.map +1 -0
- package/dist/models/support-ai-run.d.ts +45 -0
- package/dist/models/support-ai-run.d.ts.map +1 -0
- package/dist/models/support-case-event.d.ts +25 -0
- package/dist/models/support-case-event.d.ts.map +1 -0
- package/dist/models/support-case.d.ts +125 -0
- package/dist/models/support-case.d.ts.map +1 -0
- package/dist/models/support-channel-binding.d.ts +47 -0
- package/dist/models/support-channel-binding.d.ts.map +1 -0
- package/dist/models/support-compensation-plan.d.ts +42 -0
- package/dist/models/support-compensation-plan.d.ts.map +1 -0
- package/dist/models/support-interaction.d.ts +49 -0
- package/dist/models/support-interaction.d.ts.map +1 -0
- package/dist/models/support-plan.d.ts +92 -0
- package/dist/models/support-plan.d.ts.map +1 -0
- package/dist/models/support-policy.d.ts +84 -0
- package/dist/models/support-policy.d.ts.map +1 -0
- package/dist/models/support-service-target.d.ts +90 -0
- package/dist/models/support-service-target.d.ts.map +1 -0
- package/dist/models/support-settlement.d.ts +58 -0
- package/dist/models/support-settlement.d.ts.map +1 -0
- package/dist/models/support-specialist.d.ts +71 -0
- package/dist/models/support-specialist.d.ts.map +1 -0
- package/dist/models/support-work-link.d.ts +31 -0
- package/dist/models/support-work-link.d.ts.map +1 -0
- package/dist/permissions.d.ts +32 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/services/coverage-calendar.d.ts +53 -0
- package/dist/services/coverage-calendar.d.ts.map +1 -0
- package/dist/services/human-handoff-service.d.ts +113 -0
- package/dist/services/human-handoff-service.d.ts.map +1 -0
- package/dist/services/index.d.ts +15 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/keyed-mutex.d.ts +22 -0
- package/dist/services/keyed-mutex.d.ts.map +1 -0
- package/dist/services/service-target-engine.d.ts +133 -0
- package/dist/services/service-target-engine.d.ts.map +1 -0
- package/dist/services/service-time-entry-service.d.ts +73 -0
- package/dist/services/service-time-entry-service.d.ts.map +1 -0
- package/dist/services/support-ai-workflow.d.ts +178 -0
- package/dist/services/support-ai-workflow.d.ts.map +1 -0
- package/dist/services/support-case-service.d.ts +162 -0
- package/dist/services/support-case-service.d.ts.map +1 -0
- package/dist/services/support-intake-service.d.ts +106 -0
- package/dist/services/support-intake-service.d.ts.map +1 -0
- package/dist/services/support-plan-admin-service.d.ts +45 -0
- package/dist/services/support-plan-admin-service.d.ts.map +1 -0
- package/dist/services/support-routing-service.d.ts +117 -0
- package/dist/services/support-routing-service.d.ts.map +1 -0
- package/dist/services/time-entry-approval-service.d.ts +162 -0
- package/dist/services/time-entry-approval-service.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +4242 -0
- package/dist/svelte/components/CaseDetail.svelte +277 -0
- package/dist/svelte/components/CaseDetail.svelte.d.ts +10 -0
- package/dist/svelte/components/CaseDetail.svelte.d.ts.map +1 -0
- package/dist/svelte/components/CaseQueue.svelte +148 -0
- package/dist/svelte/components/CaseQueue.svelte.d.ts +12 -0
- package/dist/svelte/components/CaseQueue.svelte.d.ts.map +1 -0
- package/dist/svelte/components/RoutingRationale.svelte +131 -0
- package/dist/svelte/components/RoutingRationale.svelte.d.ts +11 -0
- package/dist/svelte/components/RoutingRationale.svelte.d.ts.map +1 -0
- package/dist/svelte/components/TargetList.svelte +127 -0
- package/dist/svelte/components/TargetList.svelte.d.ts +9 -0
- package/dist/svelte/components/TargetList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/TimeEntryApprovalQueue.svelte +226 -0
- package/dist/svelte/components/TimeEntryApprovalQueue.svelte.d.ts +11 -0
- package/dist/svelte/components/TimeEntryApprovalQueue.svelte.d.ts.map +1 -0
- package/dist/svelte/components/__tests__/CaseDetail.test.js +86 -0
- package/dist/svelte/components/__tests__/CaseQueue.test.js +67 -0
- package/dist/svelte/components/__tests__/RoutingRationale.test.js +69 -0
- package/dist/svelte/components/__tests__/TargetList.test.js +84 -0
- package/dist/svelte/components/__tests__/TimeEntryApprovalQueue.test.js +105 -0
- package/dist/svelte/index.d.ts +25 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +18 -0
- package/dist/svelte/types.d.ts +129 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +182 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +109 -0
- package/dist/types.js.map +1 -0
- package/package.json +86 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4358 @@
|
|
|
1
|
+
import { DEFAULT_SEVERITY_DEFINITIONS, OPEN_SUPPORT_CASE_STATUSES, SERVICE_TIME_ENTRY_STATUS_TRANSITIONS, SUPPORT_CASE_STATUS_TRANSITIONS, parseJsonField, parseStringArrayField } from "./types.js";
|
|
2
|
+
import { GlobalInterceptors, ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
|
|
3
|
+
import { registerPermissionDefinitions } from "@happyvertical/smrt-users";
|
|
4
|
+
import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
|
|
5
|
+
import { SmrtJobCollection, backgroundEligible } from "@happyvertical/smrt-jobs";
|
|
6
|
+
import { createLogger } from "@happyvertical/logger";
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __defProp$13 = Object.defineProperty;
|
|
9
|
+
var __exportAll = (all, no_symbols) => {
|
|
10
|
+
let target = {};
|
|
11
|
+
for (var name in all) __defProp$13(target, name, {
|
|
12
|
+
get: all[name],
|
|
13
|
+
enumerable: true
|
|
14
|
+
});
|
|
15
|
+
if (!no_symbols) __defProp$13(target, Symbol.toStringTag, { value: "Module" });
|
|
16
|
+
return target;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/__smrt-register__.ts
|
|
20
|
+
ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":1783741880336,\"packageName\":\"@happyvertical/smrt-support\",\"packageVersion\":\"0.39.2\",\"objects\":{\"@happyvertical/smrt-support:ServiceTimeEntry\":{\"name\":\"servicetimeentry\",\"className\":\"ServiceTimeEntry\",\"qualifiedName\":\"@happyvertical/smrt-support:ServiceTimeEntry\",\"collection\":\"servicetimeentries\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/service-time-entry.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"caseId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportCase\"},\"workRefType\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"workRefId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"specialistId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportSpecialist\"},\"participantKind\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"participantProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"agentRef\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"source\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"description\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"startedAt\":{\"type\":\"datetime\",\"required\":false},\"endedAt\":{\"type\":\"datetime\",\"required\":false},\"durationSeconds\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"evidence\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"submittedAt\":{\"type\":\"datetime\",\"required\":false},\"submittedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"approvedAt\":{\"type\":\"datetime\",\"required\":false},\"approvedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"approvalPath\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"rejectedAt\":{\"type\":\"datetime\",\"required\":false},\"rejectedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"rejectionReason\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"correctionOfId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"ServiceTimeEntry\"},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getEvidence\":{\"name\":\"getEvidence\",\"async\":false,\"parameters\":[],\"returnType\":\"TimeEntryEvidence[]\",\"isStatic\":false,\"isPublic\":true},\"setEvidence\":{\"name\":\"setEvidence\",\"async\":false,\"parameters\":[{\"name\":\"items\",\"type\":\"TimeEntryEvidence[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"durationHours\":{\"name\":\"durationHours\",\"async\":false,\"parameters\":[],\"returnType\":\"number\",\"isStatic\":false,\"isPublic\":true},\"initialize\":{\"name\":\"initialize\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"service_time_entries\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"ServiceTimeEntry\",\"collectionExportName\":\"ServiceTimeEntryCollection\",\"schema\":{\"tableName\":\"service_time_entries\",\"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 \\\"case_id\\\" UUID,\\n \\\"work_ref_type\\\" TEXT,\\n \\\"work_ref_id\\\" TEXT,\\n \\\"specialist_id\\\" UUID,\\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 DEFAULT 0,\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"case_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"work_ref_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"work_ref_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"specialist_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"participant_kind\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"participant_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"agent_ref\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"source\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"description\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"started_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"ended_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"duration_seconds\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"evidence\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"submitted_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"submitted_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"approved_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"approved_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"approval_path\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"rejected_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"rejected_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"rejection_reason\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"correction_of_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"service_time_entries_id_idx\",\"columns\":[\"id\"]},{\"name\":\"service_time_entries_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"9367aec5\"}},\"@happyvertical/smrt-support:ServiceTimeEntryCollection\":{\"name\":\"servicetimeentrycollection\",\"className\":\"ServiceTimeEntryCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:ServiceTimeEntryCollection\",\"collection\":\"servicetimeentries\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/service-time-entry.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forCase\":{\"name\":\"forCase\",\"async\":true,\"parameters\":[{\"name\":\"caseId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<ServiceTimeEntry[]>\",\"isStatic\":false,\"isPublic\":true},\"forSpecialist\":{\"name\":\"forSpecialist\",\"async\":true,\"parameters\":[{\"name\":\"specialistId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<ServiceTimeEntry[]>\",\"isStatic\":false,\"isPublic\":true},\"pendingApproval\":{\"name\":\"pendingApproval\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<ServiceTimeEntry[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"service_time_entries\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"ServiceTimeEntry\",\"exportName\":\"ServiceTimeEntryCollection\",\"collectionExportName\":\"ServiceTimeEntryCollectionCollection\",\"schema\":{\"tableName\":\"service_time_entries\",\"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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"service_time_entries_id_idx\",\"columns\":[\"id\"]},{\"name\":\"service_time_entries_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"d9c34ac0\"}},\"@happyvertical/smrt-support:SupportAiRun\":{\"name\":\"supportairun\",\"className\":\"SupportAiRun\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportAiRun\",\"collection\":\"supportairuns\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-ai-run.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"caseId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"SupportCase\",\"_meta\":{\"required\":true}},\"interactionId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportInteraction\"},\"phase\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"outcome\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"confidence\":{\"type\":\"decimal\",\"required\":false,\"_meta\":{\"nullable\":true}},\"classification\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"responseInteractionId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportInteraction\"},\"knowledgeRefs\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"toolCalls\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"error\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"correlationId\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"model\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"startedAt\":{\"type\":\"datetime\",\"required\":false},\"completedAt\":{\"type\":\"datetime\",\"required\":false},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getClassification\":{\"name\":\"getClassification\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setClassification\":{\"name\":\"setClassification\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getKnowledgeRefs\":{\"name\":\"getKnowledgeRefs\",\"async\":false,\"parameters\":[],\"returnType\":\"Array<Record<string>>\",\"isStatic\":false,\"isPublic\":true},\"setKnowledgeRefs\":{\"name\":\"setKnowledgeRefs\",\"async\":false,\"parameters\":[{\"name\":\"refs\",\"type\":\"Array<Record<string>>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getToolCalls\":{\"name\":\"getToolCalls\",\"async\":false,\"parameters\":[],\"returnType\":\"Array<Record<string>>\",\"isStatic\":false,\"isPublic\":true},\"setToolCalls\":{\"name\":\"setToolCalls\",\"async\":false,\"parameters\":[{\"name\":\"calls\",\"type\":\"Array<Record<string>>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_ai_runs\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportAiRun\",\"collectionExportName\":\"SupportAiRunCollection\",\"validationRules\":[{\"field\":\"caseId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"support_ai_runs\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_ai_runs\\\" (\\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 \\\"case_id\\\" UUID NOT NULL,\\n \\\"interaction_id\\\" UUID,\\n \\\"phase\\\" TEXT,\\n \\\"outcome\\\" TEXT,\\n \\\"confidence\\\" REAL,\\n \\\"classification\\\" TEXT,\\n \\\"response_interaction_id\\\" UUID,\\n \\\"knowledge_refs\\\" TEXT,\\n \\\"tool_calls\\\" TEXT,\\n \\\"error\\\" TEXT,\\n \\\"correlation_id\\\" TEXT,\\n \\\"model\\\" TEXT,\\n \\\"started_at\\\" TIMESTAMP,\\n \\\"completed_at\\\" TIMESTAMP,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"case_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false},\"interaction_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"phase\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"outcome\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"confidence\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false},\"classification\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"response_interaction_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"knowledge_refs\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"tool_calls\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"error\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"correlation_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"model\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"started_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"completed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_ai_runs_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_ai_runs_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"0caab968\"}},\"@happyvertical/smrt-support:SupportAiRunCollection\":{\"name\":\"supportairuncollection\",\"className\":\"SupportAiRunCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportAiRunCollection\",\"collection\":\"supportairuns\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-ai-run.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forCase\":{\"name\":\"forCase\",\"async\":true,\"parameters\":[{\"name\":\"caseId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportAiRun[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_ai_runs\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportAiRun\",\"exportName\":\"SupportAiRunCollection\",\"collectionExportName\":\"SupportAiRunCollectionCollection\",\"schema\":{\"tableName\":\"support_ai_runs\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_ai_runs\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_ai_runs_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_ai_runs_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"87728103\"}},\"@happyvertical/smrt-support:SupportCaseEvent\":{\"name\":\"supportcaseevent\",\"className\":\"SupportCaseEvent\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportCaseEvent\",\"collection\":\"supportcaseevents\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-case-event.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"caseId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"SupportCase\",\"_meta\":{\"required\":true}},\"eventType\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"actorKind\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"actorProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"occurredAt\":{\"type\":\"datetime\",\"required\":false},\"summary\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"payload\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getPayload\":{\"name\":\"getPayload\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setPayload\":{\"name\":\"setPayload\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_case_events\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportCaseEvent\",\"collectionExportName\":\"SupportCaseEventCollection\",\"validationRules\":[{\"field\":\"caseId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"support_case_events\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_case_events\\\" (\\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 \\\"case_id\\\" UUID NOT NULL,\\n \\\"event_type\\\" TEXT,\\n \\\"actor_kind\\\" TEXT,\\n \\\"actor_profile_id\\\" UUID,\\n \\\"occurred_at\\\" TIMESTAMP,\\n \\\"summary\\\" TEXT,\\n \\\"payload\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"case_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false},\"event_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"actor_kind\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"actor_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"occurred_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"summary\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"payload\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_case_events_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_case_events_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"cd3fc7c1\"}},\"@happyvertical/smrt-support:SupportCaseEventCollection\":{\"name\":\"supportcaseeventcollection\",\"className\":\"SupportCaseEventCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportCaseEventCollection\",\"collection\":\"supportcaseevents\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-case-event.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forCase\":{\"name\":\"forCase\",\"async\":true,\"parameters\":[{\"name\":\"caseId\",\"type\":\"string\",\"optional\":false},{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<SupportCaseEvent[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_case_events\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportCaseEvent\",\"exportName\":\"SupportCaseEventCollection\",\"collectionExportName\":\"SupportCaseEventCollectionCollection\",\"schema\":{\"tableName\":\"support_case_events\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_case_events\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_case_events_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_case_events_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"15415d2b\"}},\"@happyvertical/smrt-support:SupportCase\":{\"name\":\"supportcase\",\"className\":\"SupportCase\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportCase\",\"collection\":\"supportcases\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-case.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"caseNumber\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"subject\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"description\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"priority\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"severity\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"category\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"sensitive\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"channelKind\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"clientProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"openedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"projectId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"bindingId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportChannelBinding\"},\"threadKey\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"planId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportPlan\"},\"planSnapshot\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"assignedSpecialistId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportSpecialist\"},\"assignedAt\":{\"type\":\"datetime\",\"required\":false},\"preferredSpecialistId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportSpecialist\"},\"escalationLevel\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"escalatedAt\":{\"type\":\"datetime\",\"required\":false},\"acknowledgedAt\":{\"type\":\"datetime\",\"required\":false},\"firstRespondedAt\":{\"type\":\"datetime\",\"required\":false},\"humanRequestedAt\":{\"type\":\"datetime\",\"required\":false},\"resolvedAt\":{\"type\":\"datetime\",\"required\":false},\"resolvedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"resolutionKind\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"resolutionSummary\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"closedAt\":{\"type\":\"datetime\",\"required\":false},\"reopenCount\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"lastReopenedAt\":{\"type\":\"datetime\",\"required\":false},\"aiEnabled\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"isOpen\":{\"name\":\"isOpen\",\"async\":false,\"parameters\":[],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true},\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"updateMetadata\":{\"name\":\"updateMetadata\",\"async\":false,\"parameters\":[{\"name\":\"patch\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getPlanSnapshot\":{\"name\":\"getPlanSnapshot\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setPlanSnapshot\":{\"name\":\"setPlanSnapshot\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"initialize\":{\"name\":\"initialize\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_cases\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportCase\",\"collectionExportName\":\"SupportCaseCollection\",\"schema\":{\"tableName\":\"support_cases\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_cases\\\" (\\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 \\\"case_number\\\" TEXT,\\n \\\"subject\\\" TEXT,\\n \\\"description\\\" TEXT,\\n \\\"status\\\" TEXT,\\n \\\"priority\\\" TEXT,\\n \\\"severity\\\" TEXT,\\n \\\"category\\\" TEXT,\\n \\\"sensitive\\\" BOOLEAN,\\n \\\"channel_kind\\\" TEXT,\\n \\\"client_profile_id\\\" UUID,\\n \\\"opened_by_profile_id\\\" UUID,\\n \\\"project_id\\\" TEXT,\\n \\\"binding_id\\\" UUID,\\n \\\"thread_key\\\" TEXT,\\n \\\"plan_id\\\" UUID,\\n \\\"plan_snapshot\\\" TEXT,\\n \\\"assigned_specialist_id\\\" UUID,\\n \\\"assigned_at\\\" TIMESTAMP,\\n \\\"preferred_specialist_id\\\" UUID,\\n \\\"escalation_level\\\" INTEGER DEFAULT 0,\\n \\\"escalated_at\\\" TIMESTAMP,\\n \\\"acknowledged_at\\\" TIMESTAMP,\\n \\\"first_responded_at\\\" TIMESTAMP,\\n \\\"human_requested_at\\\" TIMESTAMP,\\n \\\"resolved_at\\\" TIMESTAMP,\\n \\\"resolved_by_profile_id\\\" UUID,\\n \\\"resolution_kind\\\" TEXT,\\n \\\"resolution_summary\\\" TEXT,\\n \\\"closed_at\\\" TIMESTAMP,\\n \\\"reopen_count\\\" INTEGER DEFAULT 0,\\n \\\"last_reopened_at\\\" TIMESTAMP,\\n \\\"ai_enabled\\\" BOOLEAN,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"case_number\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"subject\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"description\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"priority\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"severity\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"category\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"sensitive\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"channel_kind\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"client_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"opened_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"project_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"binding_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"thread_key\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"plan_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"plan_snapshot\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"assigned_specialist_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"assigned_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"preferred_specialist_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"escalation_level\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"escalated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"acknowledged_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"first_responded_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"human_requested_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"resolved_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"resolved_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"resolution_kind\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"resolution_summary\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"closed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"reopen_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"last_reopened_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"ai_enabled\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_cases_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_cases_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"f9609447\"}},\"@happyvertical/smrt-support:SupportCaseCollection\":{\"name\":\"supportcasecollection\",\"className\":\"SupportCaseCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportCaseCollection\",\"collection\":\"supportcases\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-case.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"findOpenByThreadKey\":{\"name\":\"findOpenByThreadKey\",\"async\":true,\"parameters\":[{\"name\":\"threadKey\",\"type\":\"string\",\"optional\":false},{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<SupportCase | null>\",\"isStatic\":false,\"isPublic\":true},\"findQueue\":{\"name\":\"findQueue\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<SupportCase[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_cases\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportCase\",\"exportName\":\"SupportCaseCollection\",\"collectionExportName\":\"SupportCaseCollectionCollection\",\"schema\":{\"tableName\":\"support_cases\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_cases\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_cases_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_cases_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"a0ea314b\"}},\"@happyvertical/smrt-support:SupportChannelBinding\":{\"name\":\"supportchannelbinding\",\"className\":\"SupportChannelBinding\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportChannelBinding\",\"collection\":\"supportchannelbindings\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-channel-binding.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"name\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"bindingKind\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"channelKind\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"targetType\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"targetId\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"clientProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"projectId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"planId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportPlan\"},\"enabled\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"selfAddresses\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getSelfAddresses\":{\"name\":\"getSelfAddresses\",\"async\":false,\"parameters\":[],\"returnType\":\"string[]\",\"isStatic\":false,\"isPublic\":true},\"setSelfAddresses\":{\"name\":\"setSelfAddresses\",\"async\":false,\"parameters\":[{\"name\":\"addresses\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_channel_bindings\",\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\",\"delete\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"target_type\",\"target_id\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportChannelBinding\",\"collectionExportName\":\"SupportChannelBindingCollection\",\"schema\":{\"tableName\":\"support_channel_bindings\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_channel_bindings\\\" (\\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 \\\"name\\\" TEXT DEFAULT '',\\n \\\"binding_kind\\\" TEXT,\\n \\\"channel_kind\\\" TEXT,\\n \\\"target_type\\\" TEXT,\\n \\\"target_id\\\" TEXT,\\n \\\"client_profile_id\\\" UUID,\\n \\\"project_id\\\" TEXT,\\n \\\"plan_id\\\" UUID,\\n \\\"enabled\\\" BOOLEAN,\\n \\\"self_addresses\\\" TEXT,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"name\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"binding_kind\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"channel_kind\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"target_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"target_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"client_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"project_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"plan_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"enabled\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"self_addresses\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_channel_bindings_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_channel_bindings_target_type_target_id_idx\",\"columns\":[\"target_type\",\"target_id\"],\"unique\":true}],\"version\":\"eb6f0d2e\"}},\"@happyvertical/smrt-support:SupportChannelBindingCollection\":{\"name\":\"supportchannelbindingcollection\",\"className\":\"SupportChannelBindingCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportChannelBindingCollection\",\"collection\":\"supportchannelbindings\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-channel-binding.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"findForTarget\":{\"name\":\"findForTarget\",\"async\":true,\"parameters\":[{\"name\":\"targetType\",\"type\":\"string\",\"optional\":false},{\"name\":\"targetId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportChannelBinding | null>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_channel_bindings\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportChannelBinding\",\"exportName\":\"SupportChannelBindingCollection\",\"collectionExportName\":\"SupportChannelBindingCollectionCollection\",\"schema\":{\"tableName\":\"support_channel_bindings\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_channel_bindings\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_channel_bindings_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_channel_bindings_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"662a042a\"}},\"@happyvertical/smrt-support:SupportCompensationPlan\":{\"name\":\"supportcompensationplan\",\"className\":\"SupportCompensationPlan\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportCompensationPlan\",\"collection\":\"supportcompensationplans\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-compensation-plan.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"specialistId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportSpecialist\"},\"name\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"hourlyRate\":{\"type\":\"decimal\",\"required\":false,\"default\":0},\"currency\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"effectiveFrom\":{\"type\":\"datetime\",\"required\":false},\"effectiveTo\":{\"type\":\"datetime\",\"required\":false},\"status\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"terms\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getTerms\":{\"name\":\"getTerms\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setTerms\":{\"name\":\"setTerms\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"isEffectiveAt\":{\"name\":\"isEffectiveAt\",\"async\":false,\"parameters\":[{\"name\":\"at\",\"type\":\"Date\",\"optional\":true}],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_compensation_plans\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"tenant_id\",\"specialist_id\",\"effective_from\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportCompensationPlan\",\"collectionExportName\":\"SupportCompensationPlanCollection\",\"schema\":{\"tableName\":\"support_compensation_plans\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_compensation_plans\\\" (\\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 \\\"specialist_id\\\" UUID,\\n \\\"name\\\" TEXT DEFAULT '',\\n \\\"hourly_rate\\\" REAL DEFAULT 0,\\n \\\"currency\\\" TEXT,\\n \\\"effective_from\\\" TIMESTAMP,\\n \\\"effective_to\\\" TIMESTAMP,\\n \\\"status\\\" TEXT,\\n \\\"terms\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"specialist_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"name\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"hourly_rate\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false,\"default\":0},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"effective_from\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"effective_to\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"terms\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_compensation_plans_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_compensation_plans_tenant_id_specialist_id_idx\",\"columns\":[\"tenant_id\",\"specialist_id\",\"effective_from\"],\"unique\":true}],\"version\":\"818494af\"}},\"@happyvertical/smrt-support:SupportCompensationPlanCollection\":{\"name\":\"supportcompensationplancollection\",\"className\":\"SupportCompensationPlanCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportCompensationPlanCollection\",\"collection\":\"supportcompensationplans\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-compensation-plan.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"resolveForSpecialist\":{\"name\":\"resolveForSpecialist\",\"async\":true,\"parameters\":[{\"name\":\"specialistId\",\"type\":\"string\",\"optional\":false},{\"name\":\"at\",\"type\":\"Date\",\"optional\":true},{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<SupportCompensationPlan | null>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_compensation_plans\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportCompensationPlan\",\"exportName\":\"SupportCompensationPlanCollection\",\"collectionExportName\":\"SupportCompensationPlanCollectionCollection\",\"schema\":{\"tableName\":\"support_compensation_plans\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_compensation_plans\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_compensation_plans_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_compensation_plans_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"121b3db1\"}},\"@happyvertical/smrt-support:SupportInteraction\":{\"name\":\"supportinteraction\",\"className\":\"SupportInteraction\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportInteraction\",\"collection\":\"supportinteractions\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-interaction.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"caseId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"SupportCase\",\"_meta\":{\"required\":true}},\"direction\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"channelKind\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"actorKind\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"authorProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"occurredAt\":{\"type\":\"datetime\",\"required\":false},\"body\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"sourceType\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"sourceId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"sourceKey\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"rfcMessageId\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_interactions\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"source_key\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportInteraction\",\"collectionExportName\":\"SupportInteractionCollection\",\"validationRules\":[{\"field\":\"caseId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"support_interactions\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_interactions\\\" (\\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 \\\"case_id\\\" UUID NOT NULL,\\n \\\"direction\\\" TEXT,\\n \\\"channel_kind\\\" TEXT,\\n \\\"actor_kind\\\" TEXT,\\n \\\"author_profile_id\\\" UUID,\\n \\\"occurred_at\\\" TIMESTAMP,\\n \\\"body\\\" TEXT,\\n \\\"source_type\\\" TEXT,\\n \\\"source_id\\\" TEXT,\\n \\\"source_key\\\" TEXT,\\n \\\"rfc_message_id\\\" TEXT,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"case_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false},\"direction\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"channel_kind\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"actor_kind\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"author_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"occurred_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"body\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"source_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"source_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"source_key\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"rfc_message_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_interactions_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_interactions_source_key_idx\",\"columns\":[\"source_key\"],\"unique\":true}],\"version\":\"5e5f214e\"}},\"@happyvertical/smrt-support:SupportInteractionCollection\":{\"name\":\"supportinteractioncollection\",\"className\":\"SupportInteractionCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportInteractionCollection\",\"collection\":\"supportinteractions\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-interaction.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forCase\":{\"name\":\"forCase\",\"async\":true,\"parameters\":[{\"name\":\"caseId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportInteraction[]>\",\"isStatic\":false,\"isPublic\":true},\"bySourceKey\":{\"name\":\"bySourceKey\",\"async\":true,\"parameters\":[{\"name\":\"sourceKey\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportInteraction | null>\",\"isStatic\":false,\"isPublic\":true},\"byRfcMessageId\":{\"name\":\"byRfcMessageId\",\"async\":true,\"parameters\":[{\"name\":\"rfcMessageId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportInteraction | null>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_interactions\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportInteraction\",\"exportName\":\"SupportInteractionCollection\",\"collectionExportName\":\"SupportInteractionCollectionCollection\",\"schema\":{\"tableName\":\"support_interactions\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_interactions\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_interactions_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_interactions_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"b1c57eb7\"}},\"@happyvertical/smrt-support:SupportPlan\":{\"name\":\"supportplan\",\"className\":\"SupportPlan\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportPlan\",\"collection\":\"supportplans\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-plan.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"planKey\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"name\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"description\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"channels\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"timezone\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"coverage\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"holidays\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"severityDefinitions\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"targets\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"pauseStatuses\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"escalationPolicy\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"availabilityFeeAmount\":{\"type\":\"decimal\",\"required\":false,\"default\":0},\"currency\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"includedMinutes\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"overageHourlyRate\":{\"type\":\"decimal\",\"required\":false,\"default\":0},\"onCallHourlyRate\":{\"type\":\"decimal\",\"required\":false,\"default\":0},\"timeApprovalPolicy\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"subscriptionPlanKey\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getChannels\":{\"name\":\"getChannels\",\"async\":false,\"parameters\":[],\"returnType\":\"string[]\",\"isStatic\":false,\"isPublic\":true},\"setChannels\":{\"name\":\"setChannels\",\"async\":false,\"parameters\":[{\"name\":\"channels\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getCoverage\":{\"name\":\"getCoverage\",\"async\":false,\"parameters\":[],\"returnType\":\"CoverageWindow[]\",\"isStatic\":false,\"isPublic\":true},\"setCoverage\":{\"name\":\"setCoverage\",\"async\":false,\"parameters\":[{\"name\":\"windows\",\"type\":\"CoverageWindow[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getHolidays\":{\"name\":\"getHolidays\",\"async\":false,\"parameters\":[],\"returnType\":\"string[]\",\"isStatic\":false,\"isPublic\":true},\"setHolidays\":{\"name\":\"setHolidays\",\"async\":false,\"parameters\":[{\"name\":\"dates\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getSeverityDefinitions\":{\"name\":\"getSeverityDefinitions\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string, SeverityDefinition>\",\"isStatic\":false,\"isPublic\":true},\"setSeverityDefinitions\":{\"name\":\"setSeverityDefinitions\",\"async\":false,\"parameters\":[{\"name\":\"defs\",\"type\":\"Record<string, SeverityDefinition>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getTargets\":{\"name\":\"getTargets\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string, Partial<ServiceTargetMinutes>>\",\"isStatic\":false,\"isPublic\":true},\"setTargets\":{\"name\":\"setTargets\",\"async\":false,\"parameters\":[{\"name\":\"targets\",\"type\":\"Record<string, Partial<ServiceTargetMinutes>>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"targetsForSeverity\":{\"name\":\"targetsForSeverity\",\"async\":false,\"parameters\":[{\"name\":\"severity\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"ServiceTargetMinutes\",\"isStatic\":false,\"isPublic\":true},\"getPauseStatuses\":{\"name\":\"getPauseStatuses\",\"async\":false,\"parameters\":[],\"returnType\":\"string[]\",\"isStatic\":false,\"isPublic\":true},\"setPauseStatuses\":{\"name\":\"setPauseStatuses\",\"async\":false,\"parameters\":[{\"name\":\"statuses\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getEscalationPolicy\":{\"name\":\"getEscalationPolicy\",\"async\":false,\"parameters\":[],\"returnType\":\"EscalationStep[]\",\"isStatic\":false,\"isPublic\":true},\"setEscalationPolicy\":{\"name\":\"setEscalationPolicy\",\"async\":false,\"parameters\":[{\"name\":\"steps\",\"type\":\"EscalationStep[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getTimeApprovalPolicy\":{\"name\":\"getTimeApprovalPolicy\",\"async\":false,\"parameters\":[],\"returnType\":\"TimeApprovalPolicy\",\"isStatic\":false,\"isPublic\":true},\"setTimeApprovalPolicy\":{\"name\":\"setTimeApprovalPolicy\",\"async\":false,\"parameters\":[{\"name\":\"policy\",\"type\":\"TimeApprovalPolicy\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"snapshotTerms\":{\"name\":\"snapshotTerms\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_plans\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"tenant_id\",\"plan_key\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportPlan\",\"collectionExportName\":\"SupportPlanCollection\",\"schema\":{\"tableName\":\"support_plans\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_plans\\\" (\\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 \\\"plan_key\\\" TEXT,\\n \\\"name\\\" TEXT DEFAULT '',\\n \\\"description\\\" TEXT,\\n \\\"status\\\" TEXT,\\n \\\"channels\\\" TEXT,\\n \\\"timezone\\\" TEXT,\\n \\\"coverage\\\" TEXT,\\n \\\"holidays\\\" TEXT,\\n \\\"severity_definitions\\\" TEXT,\\n \\\"targets\\\" TEXT,\\n \\\"pause_statuses\\\" TEXT,\\n \\\"escalation_policy\\\" TEXT,\\n \\\"availability_fee_amount\\\" REAL DEFAULT 0,\\n \\\"currency\\\" TEXT,\\n \\\"included_minutes\\\" INTEGER DEFAULT 0,\\n \\\"overage_hourly_rate\\\" REAL DEFAULT 0,\\n \\\"on_call_hourly_rate\\\" REAL DEFAULT 0,\\n \\\"time_approval_policy\\\" TEXT,\\n \\\"subscription_plan_key\\\" TEXT,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"plan_key\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"name\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"description\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"channels\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"timezone\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"coverage\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"holidays\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"severity_definitions\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"targets\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"pause_statuses\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"escalation_policy\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"availability_fee_amount\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false,\"default\":0},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"included_minutes\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"overage_hourly_rate\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false,\"default\":0},\"on_call_hourly_rate\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false,\"default\":0},\"time_approval_policy\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"subscription_plan_key\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_plans_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_plans_tenant_id_plan_key_idx\",\"columns\":[\"tenant_id\",\"plan_key\"],\"unique\":true}],\"version\":\"b472a876\"}},\"@happyvertical/smrt-support:SupportPlanCollection\":{\"name\":\"supportplancollection\",\"className\":\"SupportPlanCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportPlanCollection\",\"collection\":\"supportplans\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-plan.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"byPlanKey\":{\"name\":\"byPlanKey\",\"async\":true,\"parameters\":[{\"name\":\"planKey\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportPlan | null>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_plans\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportPlan\",\"exportName\":\"SupportPlanCollection\",\"collectionExportName\":\"SupportPlanCollectionCollection\",\"schema\":{\"tableName\":\"support_plans\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_plans\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_plans_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_plans_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"a2548347\"}},\"@happyvertical/smrt-support:SupportPolicy\":{\"name\":\"supportpolicy\",\"className\":\"SupportPolicy\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportPolicy\",\"collection\":\"supportpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-policy.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"name\":{\"type\":\"text\",\"required\":false,\"default\":\"\"},\"planId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportPlan\"},\"projectId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"enabled\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"autoAcknowledge\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"autoClassify\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"autoAnswer\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"autoTroubleshoot\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"autoResolve\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"autoResolveMaxSeverity\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"confidenceThreshold\":{\"type\":\"decimal\",\"required\":false,\"default\":0.7},\"maxAutoAttempts\":{\"type\":\"integer\",\"required\":false,\"default\":1},\"autoSendEmailReplies\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"sensitiveCategories\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"allowedTools\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getSensitiveCategories\":{\"name\":\"getSensitiveCategories\",\"async\":false,\"parameters\":[],\"returnType\":\"string[]\",\"isStatic\":false,\"isPublic\":true},\"setSensitiveCategories\":{\"name\":\"setSensitiveCategories\",\"async\":false,\"parameters\":[{\"name\":\"categories\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getAllowedTools\":{\"name\":\"getAllowedTools\",\"async\":false,\"parameters\":[],\"returnType\":\"string[]\",\"isStatic\":false,\"isPublic\":true},\"setAllowedTools\":{\"name\":\"setAllowedTools\",\"async\":false,\"parameters\":[{\"name\":\"tools\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"scopeRank\":{\"name\":\"scopeRank\",\"async\":false,\"parameters\":[],\"returnType\":\"number\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_policies\",\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\",\"delete\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"tenant_id\",\"name\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportPolicy\",\"collectionExportName\":\"SupportPolicyCollection\",\"schema\":{\"tableName\":\"support_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_policies\\\" (\\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 \\\"name\\\" TEXT DEFAULT '',\\n \\\"plan_id\\\" UUID,\\n \\\"project_id\\\" TEXT,\\n \\\"enabled\\\" BOOLEAN,\\n \\\"auto_acknowledge\\\" BOOLEAN,\\n \\\"auto_classify\\\" BOOLEAN,\\n \\\"auto_answer\\\" BOOLEAN,\\n \\\"auto_troubleshoot\\\" BOOLEAN,\\n \\\"auto_resolve\\\" BOOLEAN,\\n \\\"auto_resolve_max_severity\\\" TEXT,\\n \\\"confidence_threshold\\\" REAL DEFAULT 0.7,\\n \\\"max_auto_attempts\\\" INTEGER DEFAULT 1,\\n \\\"auto_send_email_replies\\\" BOOLEAN,\\n \\\"sensitive_categories\\\" TEXT,\\n \\\"allowed_tools\\\" TEXT,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"name\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false,\"default\":\"\"},\"plan_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"project_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"enabled\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"auto_acknowledge\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"auto_classify\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"auto_answer\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"auto_troubleshoot\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"auto_resolve\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"auto_resolve_max_severity\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"confidence_threshold\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false,\"default\":0.7},\"max_auto_attempts\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":1},\"auto_send_email_replies\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"sensitive_categories\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"allowed_tools\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_policies_tenant_id_name_idx\",\"columns\":[\"tenant_id\",\"name\"],\"unique\":true}],\"version\":\"3bdcc3f5\"}},\"@happyvertical/smrt-support:SupportPolicyCollection\":{\"name\":\"supportpolicycollection\",\"className\":\"SupportPolicyCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportPolicyCollection\",\"collection\":\"supportpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-policy.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"resolveForScope\":{\"name\":\"resolveForScope\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":false}],\"returnType\":\"Promise<SupportPolicy | null>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_policies\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportPolicy\",\"exportName\":\"SupportPolicyCollection\",\"collectionExportName\":\"SupportPolicyCollectionCollection\",\"schema\":{\"tableName\":\"support_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_policies\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_policies_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"555a3403\"}},\"@happyvertical/smrt-support:SupportServiceTarget\":{\"name\":\"supportservicetarget\",\"className\":\"SupportServiceTarget\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportServiceTarget\",\"collection\":\"supportservicetargets\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-service-target.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"caseId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"SupportCase\",\"_meta\":{\"required\":true}},\"targetType\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"cycle\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"severity\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"baseMinutes\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"startedAt\":{\"type\":\"datetime\",\"required\":false},\"dueAt\":{\"type\":\"datetime\",\"required\":false},\"status\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"pausedAt\":{\"type\":\"datetime\",\"required\":false},\"pausedTotalSeconds\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"satisfiedAt\":{\"type\":\"datetime\",\"required\":false},\"breachedAt\":{\"type\":\"datetime\",\"required\":false},\"cancelledAt\":{\"type\":\"datetime\",\"required\":false},\"escalationJobId\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"isRunning\":{\"name\":\"isRunning\",\"async\":false,\"parameters\":[],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true},\"checkAndEscalate\":{\"name\":\"checkAndEscalate\",\"async\":true,\"parameters\":[{\"name\":\"args\",\"type\":\"Record<string>\",\"optional\":true},{\"name\":\"_context\",\"type\":\"any\",\"optional\":true}],\"returnType\":\"Promise<CheckAndEscalateResult>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_service_targets\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"case_id\",\"target_type\",\"cycle\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportServiceTarget\",\"collectionExportName\":\"SupportServiceTargetCollection\",\"validationRules\":[{\"field\":\"caseId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"support_service_targets\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_service_targets\\\" (\\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 \\\"case_id\\\" UUID NOT NULL,\\n \\\"target_type\\\" TEXT,\\n \\\"cycle\\\" INTEGER DEFAULT 0,\\n \\\"severity\\\" TEXT,\\n \\\"base_minutes\\\" INTEGER DEFAULT 0,\\n \\\"started_at\\\" TIMESTAMP,\\n \\\"due_at\\\" TIMESTAMP,\\n \\\"status\\\" TEXT,\\n \\\"paused_at\\\" TIMESTAMP,\\n \\\"paused_total_seconds\\\" INTEGER DEFAULT 0,\\n \\\"satisfied_at\\\" TIMESTAMP,\\n \\\"breached_at\\\" TIMESTAMP,\\n \\\"cancelled_at\\\" TIMESTAMP,\\n \\\"escalation_job_id\\\" TEXT,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"case_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false},\"target_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"cycle\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"severity\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"base_minutes\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"started_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"due_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"paused_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"paused_total_seconds\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"satisfied_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"breached_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"cancelled_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"escalation_job_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_service_targets_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_service_targets_case_id_target_type_idx\",\"columns\":[\"case_id\",\"target_type\",\"cycle\"],\"unique\":true}],\"version\":\"8af54dc4\"}},\"@happyvertical/smrt-support:SupportServiceTargetCollection\":{\"name\":\"supportservicetargetcollection\",\"className\":\"SupportServiceTargetCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportServiceTargetCollection\",\"collection\":\"supportservicetargets\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-service-target.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forCase\":{\"name\":\"forCase\",\"async\":true,\"parameters\":[{\"name\":\"caseId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportServiceTarget[]>\",\"isStatic\":false,\"isPublic\":true},\"activeTarget\":{\"name\":\"activeTarget\",\"async\":true,\"parameters\":[{\"name\":\"caseId\",\"type\":\"string\",\"optional\":false},{\"name\":\"targetType\",\"type\":\"ServiceTargetType\",\"optional\":false}],\"returnType\":\"Promise<SupportServiceTarget | null>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_service_targets\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportServiceTarget\",\"exportName\":\"SupportServiceTargetCollection\",\"collectionExportName\":\"SupportServiceTargetCollectionCollection\",\"schema\":{\"tableName\":\"support_service_targets\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_service_targets\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_service_targets_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_service_targets_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"f201a103\"}},\"@happyvertical/smrt-support:SupportEscalation\":{\"name\":\"supportescalation\",\"className\":\"SupportEscalation\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportEscalation\",\"collection\":\"supportescalations\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-service-target.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"caseId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"SupportCase\",\"_meta\":{\"required\":true}},\"level\":{\"type\":\"integer\",\"required\":false,\"default\":1},\"reason\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"targetId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportServiceTarget\"},\"targetType\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"action\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"fromSpecialistId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportSpecialist\"},\"toSpecialistId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportSpecialist\"},\"notifiedProfileIds\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"occurredAt\":{\"type\":\"datetime\",\"required\":false},\"note\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getNotifiedProfileIds\":{\"name\":\"getNotifiedProfileIds\",\"async\":false,\"parameters\":[],\"returnType\":\"string[]\",\"isStatic\":false,\"isPublic\":true},\"setNotifiedProfileIds\":{\"name\":\"setNotifiedProfileIds\",\"async\":false,\"parameters\":[{\"name\":\"ids\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_escalations\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportEscalation\",\"collectionExportName\":\"SupportEscalationCollection\",\"validationRules\":[{\"field\":\"caseId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"support_escalations\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_escalations\\\" (\\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 \\\"case_id\\\" UUID NOT NULL,\\n \\\"level\\\" INTEGER DEFAULT 1,\\n \\\"reason\\\" TEXT,\\n \\\"target_id\\\" UUID,\\n \\\"target_type\\\" TEXT,\\n \\\"action\\\" TEXT,\\n \\\"from_specialist_id\\\" UUID,\\n \\\"to_specialist_id\\\" UUID,\\n \\\"notified_profile_ids\\\" TEXT,\\n \\\"occurred_at\\\" TIMESTAMP,\\n \\\"note\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"case_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false},\"level\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":1},\"reason\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"target_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"target_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"action\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"from_specialist_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"to_specialist_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"notified_profile_ids\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"occurred_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"note\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_escalations_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_escalations_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"977ea730\"}},\"@happyvertical/smrt-support:SupportEscalationCollection\":{\"name\":\"supportescalationcollection\",\"className\":\"SupportEscalationCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportEscalationCollection\",\"collection\":\"supportescalations\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-service-target.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forCase\":{\"name\":\"forCase\",\"async\":true,\"parameters\":[{\"name\":\"caseId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportEscalation[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_escalations\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportEscalation\",\"exportName\":\"SupportEscalationCollection\",\"collectionExportName\":\"SupportEscalationCollectionCollection\",\"schema\":{\"tableName\":\"support_escalations\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_escalations\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_escalations_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_escalations_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"f9b3556a\"}},\"@happyvertical/smrt-support:SupportCharge\":{\"name\":\"supportcharge\",\"className\":\"SupportCharge\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportCharge\",\"collection\":\"supportcharges\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-settlement.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"timeEntryId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"ServiceTimeEntry\",\"_meta\":{\"required\":true}},\"caseId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportCase\"},\"planId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportPlan\"},\"amount\":{\"type\":\"decimal\",\"required\":false,\"default\":0},\"currency\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"billableSeconds\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"includedSecondsApplied\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"rateSnapshot\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"finalizedAt\":{\"type\":\"datetime\",\"required\":false},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getRateSnapshot\":{\"name\":\"getRateSnapshot\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setRateSnapshot\":{\"name\":\"setRateSnapshot\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_charges\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"time_entry_id\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportCharge\",\"collectionExportName\":\"SupportChargeCollection\",\"validationRules\":[{\"field\":\"timeEntryId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"support_charges\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_charges\\\" (\\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 \\\"case_id\\\" UUID,\\n \\\"plan_id\\\" UUID,\\n \\\"amount\\\" REAL DEFAULT 0,\\n \\\"currency\\\" TEXT,\\n \\\"billable_seconds\\\" INTEGER DEFAULT 0,\\n \\\"included_seconds_applied\\\" INTEGER DEFAULT 0,\\n \\\"rate_snapshot\\\" TEXT,\\n \\\"status\\\" TEXT,\\n \\\"finalized_at\\\" TIMESTAMP,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"time_entry_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false},\"case_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"plan_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"amount\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false,\"default\":0},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"billable_seconds\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"included_seconds_applied\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"rate_snapshot\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"finalized_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_charges_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_charges_time_entry_id_idx\",\"columns\":[\"time_entry_id\"],\"unique\":true}],\"version\":\"80736ce6\"}},\"@happyvertical/smrt-support:SupportChargeCollection\":{\"name\":\"supportchargecollection\",\"className\":\"SupportChargeCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportChargeCollection\",\"collection\":\"supportcharges\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-settlement.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forTimeEntry\":{\"name\":\"forTimeEntry\",\"async\":true,\"parameters\":[{\"name\":\"timeEntryId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportCharge | null>\",\"isStatic\":false,\"isPublic\":true},\"forCase\":{\"name\":\"forCase\",\"async\":true,\"parameters\":[{\"name\":\"caseId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportCharge[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_charges\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportCharge\",\"exportName\":\"SupportChargeCollection\",\"collectionExportName\":\"SupportChargeCollectionCollection\",\"schema\":{\"tableName\":\"support_charges\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_charges\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_charges_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_charges_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"6853d84d\"}},\"@happyvertical/smrt-support:SupportCompensation\":{\"name\":\"supportcompensation\",\"className\":\"SupportCompensation\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportCompensation\",\"collection\":\"supportcompensations\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-settlement.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"timeEntryId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"ServiceTimeEntry\",\"_meta\":{\"required\":true}},\"specialistId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportSpecialist\"},\"compensationPlanId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"SupportCompensationPlan\"},\"amount\":{\"type\":\"decimal\",\"required\":false,\"default\":0},\"currency\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"payableSeconds\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"rateSnapshot\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"finalizedAt\":{\"type\":\"datetime\",\"required\":false},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getRateSnapshot\":{\"name\":\"getRateSnapshot\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setRateSnapshot\":{\"name\":\"setRateSnapshot\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_compensations\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"time_entry_id\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportCompensation\",\"collectionExportName\":\"SupportCompensationCollection\",\"validationRules\":[{\"field\":\"timeEntryId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"support_compensations\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_compensations\\\" (\\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 \\\"specialist_id\\\" UUID,\\n \\\"compensation_plan_id\\\" UUID,\\n \\\"amount\\\" REAL DEFAULT 0,\\n \\\"currency\\\" TEXT,\\n \\\"payable_seconds\\\" INTEGER DEFAULT 0,\\n \\\"rate_snapshot\\\" TEXT,\\n \\\"status\\\" TEXT,\\n \\\"finalized_at\\\" TIMESTAMP,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"time_entry_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false},\"specialist_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"compensation_plan_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false},\"amount\":{\"type\":\"REAL\",\"notNull\":false,\"unique\":false,\"default\":0},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"payable_seconds\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"rate_snapshot\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"finalized_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_compensations_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_compensations_time_entry_id_idx\",\"columns\":[\"time_entry_id\"],\"unique\":true}],\"version\":\"2ec22878\"}},\"@happyvertical/smrt-support:SupportCompensationCollection\":{\"name\":\"supportcompensationcollection\",\"className\":\"SupportCompensationCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportCompensationCollection\",\"collection\":\"supportcompensations\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-settlement.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forTimeEntry\":{\"name\":\"forTimeEntry\",\"async\":true,\"parameters\":[{\"name\":\"timeEntryId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportCompensation | null>\",\"isStatic\":false,\"isPublic\":true},\"forSpecialist\":{\"name\":\"forSpecialist\",\"async\":true,\"parameters\":[{\"name\":\"specialistId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportCompensation[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_compensations\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportCompensation\",\"exportName\":\"SupportCompensationCollection\",\"collectionExportName\":\"SupportCompensationCollectionCollection\",\"schema\":{\"tableName\":\"support_compensations\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_compensations\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_compensations_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_compensations_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"ec88df4e\"}},\"@happyvertical/smrt-support:SupportSpecialist\":{\"name\":\"supportspecialist\",\"className\":\"SupportSpecialist\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportSpecialist\",\"collection\":\"supportspecialists\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-specialist.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"profileId\":{\"type\":\"crossPackageRef\",\"required\":true,\"related\":\"@happyvertical/smrt-profiles:Profile\"},\"displayName\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"languages\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"timezone\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"maxConcurrentCases\":{\"type\":\"integer\",\"required\":false,\"default\":10},\"onCallPriority\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"notes\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getLanguages\":{\"name\":\"getLanguages\",\"async\":false,\"parameters\":[],\"returnType\":\"string[]\",\"isStatic\":false,\"isPublic\":true},\"setLanguages\":{\"name\":\"setLanguages\",\"async\":false,\"parameters\":[{\"name\":\"languages\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"isActive\":{\"name\":\"isActive\",\"async\":false,\"parameters\":[],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_specialists\",\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\",\"delete\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"tenant_id\",\"profile_id\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportSpecialist\",\"collectionExportName\":\"SupportSpecialistCollection\",\"validationRules\":[{\"field\":\"profileId\",\"rule\":\"required\",\"fieldType\":\"crossPackageRef\"}],\"schema\":{\"tableName\":\"support_specialists\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_specialists\\\" (\\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 \\\"profile_id\\\" UUID NOT NULL,\\n \\\"display_name\\\" TEXT,\\n \\\"status\\\" TEXT,\\n \\\"languages\\\" TEXT,\\n \\\"timezone\\\" TEXT,\\n \\\"max_concurrent_cases\\\" INTEGER DEFAULT 10,\\n \\\"on_call_priority\\\" INTEGER DEFAULT 0,\\n \\\"notes\\\" TEXT,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":true,\"unique\":false},\"display_name\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"languages\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"timezone\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"max_concurrent_cases\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":10},\"on_call_priority\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_specialists_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_specialists_tenant_id_profile_id_idx\",\"columns\":[\"tenant_id\",\"profile_id\"],\"unique\":true}],\"version\":\"ff5305d3\"}},\"@happyvertical/smrt-support:SupportSpecialistCollection\":{\"name\":\"supportspecialistcollection\",\"className\":\"SupportSpecialistCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportSpecialistCollection\",\"collection\":\"supportspecialists\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-specialist.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"findActive\":{\"name\":\"findActive\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<SupportSpecialist[]>\",\"isStatic\":false,\"isPublic\":true},\"byProfile\":{\"name\":\"byProfile\",\"async\":true,\"parameters\":[{\"name\":\"profileId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportSpecialist | null>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_specialists\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportSpecialist\",\"exportName\":\"SupportSpecialistCollection\",\"collectionExportName\":\"SupportSpecialistCollectionCollection\",\"schema\":{\"tableName\":\"support_specialists\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_specialists\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_specialists_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_specialists_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"65dfee96\"}},\"@happyvertical/smrt-support:SupportQualification\":{\"name\":\"supportqualification\",\"className\":\"SupportQualification\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportQualification\",\"collection\":\"supportqualifications\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-specialist.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"specialistId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"SupportSpecialist\",\"_meta\":{\"required\":true}},\"projectId\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"level\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"effectiveFrom\":{\"type\":\"datetime\",\"required\":false},\"effectiveTo\":{\"type\":\"datetime\",\"required\":false},\"grantedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\",\"_meta\":{\"nullable\":true}},\"notes\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"isEffectiveAt\":{\"name\":\"isEffectiveAt\",\"async\":false,\"parameters\":[{\"name\":\"at\",\"type\":\"Date\",\"optional\":true}],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_qualifications\",\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\",\"delete\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"specialist_id\",\"project_id\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportQualification\",\"collectionExportName\":\"SupportQualificationCollection\",\"validationRules\":[{\"field\":\"specialistId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"support_qualifications\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_qualifications\\\" (\\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 \\\"specialist_id\\\" UUID NOT NULL,\\n \\\"project_id\\\" TEXT,\\n \\\"level\\\" TEXT,\\n \\\"effective_from\\\" TIMESTAMP,\\n \\\"effective_to\\\" TIMESTAMP,\\n \\\"granted_by_profile_id\\\" UUID,\\n \\\"notes\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"specialist_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false},\"project_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"level\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"effective_from\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"effective_to\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"granted_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_qualifications_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_qualifications_specialist_id_project_id_idx\",\"columns\":[\"specialist_id\",\"project_id\"],\"unique\":true}],\"version\":\"7b4f8c26\"}},\"@happyvertical/smrt-support:SupportQualificationCollection\":{\"name\":\"supportqualificationcollection\",\"className\":\"SupportQualificationCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportQualificationCollection\",\"collection\":\"supportqualifications\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-specialist.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forSpecialist\":{\"name\":\"forSpecialist\",\"async\":true,\"parameters\":[{\"name\":\"specialistId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportQualification[]>\",\"isStatic\":false,\"isPublic\":true},\"forProject\":{\"name\":\"forProject\",\"async\":true,\"parameters\":[{\"name\":\"projectId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportQualification[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_qualifications\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportQualification\",\"exportName\":\"SupportQualificationCollection\",\"collectionExportName\":\"SupportQualificationCollectionCollection\",\"schema\":{\"tableName\":\"support_qualifications\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_qualifications\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_qualifications_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_qualifications_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"a0c2e59b\"}},\"@happyvertical/smrt-support:SupportAvailability\":{\"name\":\"supportavailability\",\"className\":\"SupportAvailability\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportAvailability\",\"collection\":\"supportavailabilities\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-specialist.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"specialistId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"SupportSpecialist\",\"_meta\":{\"required\":true}},\"kind\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"weekday\":{\"type\":\"integer\",\"required\":false,\"_meta\":{\"nullable\":true}},\"startMinute\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"endMinute\":{\"type\":\"integer\",\"required\":false,\"default\":0},\"startsAt\":{\"type\":\"datetime\",\"required\":false},\"endsAt\":{\"type\":\"datetime\",\"required\":false},\"notes\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"support_availability_windows\",\"api\":{\"include\":[\"list\",\"get\",\"create\",\"update\",\"delete\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportAvailability\",\"collectionExportName\":\"SupportAvailabilityCollection\",\"validationRules\":[{\"field\":\"specialistId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"support_availability_windows\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_availability_windows\\\" (\\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 \\\"specialist_id\\\" UUID NOT NULL,\\n \\\"kind\\\" TEXT,\\n \\\"weekday\\\" INTEGER,\\n \\\"start_minute\\\" INTEGER DEFAULT 0,\\n \\\"end_minute\\\" INTEGER DEFAULT 0,\\n \\\"starts_at\\\" TIMESTAMP,\\n \\\"ends_at\\\" TIMESTAMP,\\n \\\"notes\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"specialist_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false},\"kind\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"weekday\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"start_minute\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"end_minute\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false,\"default\":0},\"starts_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"ends_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"notes\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_availability_windows_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_availability_windows_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"3cb42cd3\"}},\"@happyvertical/smrt-support:SupportAvailabilityCollection\":{\"name\":\"supportavailabilitycollection\",\"className\":\"SupportAvailabilityCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportAvailabilityCollection\",\"collection\":\"supportavailabilities\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-specialist.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forSpecialist\":{\"name\":\"forSpecialist\",\"async\":true,\"parameters\":[{\"name\":\"specialistId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportAvailability[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_availability_windows\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportAvailability\",\"exportName\":\"SupportAvailabilityCollection\",\"collectionExportName\":\"SupportAvailabilityCollectionCollection\",\"schema\":{\"tableName\":\"support_availability_windows\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_availability_windows\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_availability_windows_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_availability_windows_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"acd97b79\"}},\"@happyvertical/smrt-support:SupportWorkLink\":{\"name\":\"supportworklink\",\"className\":\"SupportWorkLink\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportWorkLink\",\"collection\":\"supportworklinks\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-work-link.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"caseId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"SupportCase\",\"_meta\":{\"required\":true}},\"linkKind\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"targetType\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"targetId\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"targetLabel\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"externalUrl\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"lastStatusAt\":{\"type\":\"datetime\",\"required\":false},\"metadata\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getMetadata\":{\"name\":\"getMetadata\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setMetadata\":{\"name\":\"setMetadata\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_work_links\",\"api\":{\"include\":[\"list\",\"get\"]},\"mcp\":{\"include\":[\"list\",\"get\"]},\"cli\":{\"include\":[\"list\",\"get\"]},\"conflictColumns\":[\"case_id\",\"target_type\",\"target_id\"],\"tenantScoped\":{\"mode\":\"optional\"}},\"extends\":\"SmrtObject\",\"exportName\":\"SupportWorkLink\",\"collectionExportName\":\"SupportWorkLinkCollection\",\"validationRules\":[{\"field\":\"caseId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"}],\"schema\":{\"tableName\":\"support_work_links\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_work_links\\\" (\\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 \\\"case_id\\\" UUID NOT NULL,\\n \\\"link_kind\\\" TEXT,\\n \\\"target_type\\\" TEXT,\\n \\\"target_id\\\" TEXT,\\n \\\"target_label\\\" TEXT,\\n \\\"external_url\\\" TEXT,\\n \\\"status\\\" TEXT,\\n \\\"last_status_at\\\" TIMESTAMP,\\n \\\"metadata\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"case_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false},\"link_kind\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"target_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"target_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"target_label\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"external_url\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"last_status_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"metadata\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"support_work_links_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_work_links_case_id_target_type_idx\",\"columns\":[\"case_id\",\"target_type\",\"target_id\"],\"unique\":true}],\"version\":\"72accaad\"}},\"@happyvertical/smrt-support:SupportWorkLinkCollection\":{\"name\":\"supportworklinkcollection\",\"className\":\"SupportWorkLinkCollection\",\"qualifiedName\":\"@happyvertical/smrt-support:SupportWorkLinkCollection\",\"collection\":\"supportworklinks\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/support/src/models/support-work-link.ts\",\"packageName\":\"@happyvertical/smrt-support\",\"fields\":{},\"methods\":{\"forCase\":{\"name\":\"forCase\",\"async\":true,\"parameters\":[{\"name\":\"caseId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<SupportWorkLink[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"support_work_links\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"SupportWorkLink\",\"exportName\":\"SupportWorkLinkCollection\",\"collectionExportName\":\"SupportWorkLinkCollectionCollection\",\"schema\":{\"tableName\":\"support_work_links\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"support_work_links\\\" (\\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);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"support_work_links_id_idx\",\"columns\":[\"id\"]},{\"name\":\"support_work_links_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"d1147c3e\"}}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-chat\",\"@happyvertical/smrt-core\",\"@happyvertical/smrt-jobs\",\"@happyvertical/smrt-messages\",\"@happyvertical/smrt-tenancy\",\"@happyvertical/smrt-users\"]}"));
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/permissions.ts
|
|
23
|
+
var REASSIGN_CASE_PERMISSION = "support.reassign-case";
|
|
24
|
+
var APPROVE_TIME_ENTRY_PERMISSION = "support.approve-time-entry";
|
|
25
|
+
var MANAGE_PLANS_PERMISSION = "support.manage-plans";
|
|
26
|
+
var SUPPORT_PERMISSION_DEFS = [
|
|
27
|
+
{
|
|
28
|
+
slug: REASSIGN_CASE_PERMISSION,
|
|
29
|
+
category: "support",
|
|
30
|
+
name: "Reassign Support Case",
|
|
31
|
+
description: "Manually reassign a Support Case to a specialist, overriding automatic routing"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
slug: APPROVE_TIME_ENTRY_PERMISSION,
|
|
35
|
+
category: "support",
|
|
36
|
+
name: "Approve Service Time Entry",
|
|
37
|
+
description: "Approve or reject submitted Service Time Entries and finalize their charge/compensation snapshots"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
slug: MANAGE_PLANS_PERMISSION,
|
|
41
|
+
category: "support",
|
|
42
|
+
name: "Manage Support Plans",
|
|
43
|
+
description: "Create and edit Managed Support Plans and Support Compensation Plans"
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
function supportPrincipalFromPermissions(permissions, options = {}) {
|
|
47
|
+
const granted = new Set(permissions);
|
|
48
|
+
return {
|
|
49
|
+
id: options.id,
|
|
50
|
+
tenantId: options.tenantId,
|
|
51
|
+
can: (slug) => granted.has(slug)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
var supportPermissionsRegistered = false;
|
|
55
|
+
function registerSupportPermissions() {
|
|
56
|
+
return registerPermissionDefinitions(SUPPORT_PERMISSION_DEFS);
|
|
57
|
+
}
|
|
58
|
+
function ensureSupportPermissionsRegistered() {
|
|
59
|
+
if (supportPermissionsRegistered) return;
|
|
60
|
+
supportPermissionsRegistered = true;
|
|
61
|
+
registerSupportPermissions();
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/models/service-time-entry.ts
|
|
65
|
+
var __defProp$12 = Object.defineProperty;
|
|
66
|
+
var __getOwnPropDesc$12 = Object.getOwnPropertyDescriptor;
|
|
67
|
+
var __decorateClass$12 = (decorators, target, key, kind) => {
|
|
68
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$12(target, key) : target;
|
|
69
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
70
|
+
if (kind && result) __defProp$12(target, key, result);
|
|
71
|
+
return result;
|
|
72
|
+
};
|
|
73
|
+
var FROZEN_FIELDS = [
|
|
74
|
+
"caseId",
|
|
75
|
+
"workRefType",
|
|
76
|
+
"workRefId",
|
|
77
|
+
"specialistId",
|
|
78
|
+
"participantKind",
|
|
79
|
+
"participantProfileId",
|
|
80
|
+
"agentRef",
|
|
81
|
+
"source",
|
|
82
|
+
"description",
|
|
83
|
+
"startedAt",
|
|
84
|
+
"endedAt",
|
|
85
|
+
"durationSeconds",
|
|
86
|
+
"evidence"
|
|
87
|
+
];
|
|
88
|
+
var approvedSnapshot = /* @__PURE__ */ new WeakMap();
|
|
89
|
+
var loadedEntryStatus = /* @__PURE__ */ new WeakMap();
|
|
90
|
+
function normalizeFrozenValue(key, value) {
|
|
91
|
+
if (value === void 0 || value === null || value === "") return key === "durationSeconds" ? 0 : null;
|
|
92
|
+
if (key === "startedAt" || key === "endedAt") {
|
|
93
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
94
|
+
return Number.isNaN(date.getTime()) ? String(value) : date.toISOString();
|
|
95
|
+
}
|
|
96
|
+
if (key === "durationSeconds") return Number(value);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function serializeFrozenView(view) {
|
|
100
|
+
const normalized = {};
|
|
101
|
+
for (const key of FROZEN_FIELDS) normalized[key] = normalizeFrozenValue(key, view[key]);
|
|
102
|
+
return JSON.stringify(normalized);
|
|
103
|
+
}
|
|
104
|
+
function serializeFrozenFields(entry) {
|
|
105
|
+
const view = {};
|
|
106
|
+
for (const key of FROZEN_FIELDS) view[key] = entry[key];
|
|
107
|
+
return serializeFrozenView(view);
|
|
108
|
+
}
|
|
109
|
+
var ServiceTimeEntry = class extends SmrtObject {
|
|
110
|
+
tenantId = null;
|
|
111
|
+
caseId = null;
|
|
112
|
+
workRefType = null;
|
|
113
|
+
workRefId = null;
|
|
114
|
+
specialistId = null;
|
|
115
|
+
participantKind = "human";
|
|
116
|
+
participantProfileId = null;
|
|
117
|
+
agentRef = "";
|
|
118
|
+
source = "manual";
|
|
119
|
+
description = "";
|
|
120
|
+
/** Work period bounds (informational; `durationSeconds` is authoritative). */
|
|
121
|
+
startedAt = null;
|
|
122
|
+
endedAt = null;
|
|
123
|
+
/** Authoritative worked duration in seconds. */
|
|
124
|
+
durationSeconds = 0;
|
|
125
|
+
evidence = "[]";
|
|
126
|
+
status = "draft";
|
|
127
|
+
submittedAt = null;
|
|
128
|
+
submittedByProfileId = null;
|
|
129
|
+
approvedAt = null;
|
|
130
|
+
approvedByProfileId = null;
|
|
131
|
+
approvalPath = "";
|
|
132
|
+
rejectedAt = null;
|
|
133
|
+
rejectedByProfileId = null;
|
|
134
|
+
rejectionReason = "";
|
|
135
|
+
correctionOfId = null;
|
|
136
|
+
metadata = "{}";
|
|
137
|
+
getEvidence() {
|
|
138
|
+
const parsed = parseJsonField(this.evidence, []);
|
|
139
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
140
|
+
}
|
|
141
|
+
setEvidence(items) {
|
|
142
|
+
this.evidence = JSON.stringify(items ?? []);
|
|
143
|
+
}
|
|
144
|
+
getMetadata() {
|
|
145
|
+
return parseJsonField(this.metadata, {});
|
|
146
|
+
}
|
|
147
|
+
setMetadata(value) {
|
|
148
|
+
this.metadata = JSON.stringify(value ?? {});
|
|
149
|
+
}
|
|
150
|
+
/** Worked duration in decimal hours (UI convenience). */
|
|
151
|
+
durationHours() {
|
|
152
|
+
return this.durationSeconds / 3600;
|
|
153
|
+
}
|
|
154
|
+
async initialize() {
|
|
155
|
+
await super.initialize();
|
|
156
|
+
if (await this.isSaved()) {
|
|
157
|
+
loadedEntryStatus.set(this, this.status);
|
|
158
|
+
if (this.status === "approved" || this.status === "corrected") approvedSnapshot.set(this, serializeFrozenFields(this));
|
|
159
|
+
}
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
async save() {
|
|
163
|
+
const prior = await this.resolvePriorState();
|
|
164
|
+
this.assertStatusTransition(prior?.status);
|
|
165
|
+
this.assertApprovedImmutable(prior);
|
|
166
|
+
const result = await super.save();
|
|
167
|
+
loadedEntryStatus.set(this, this.status);
|
|
168
|
+
if (this.status === "approved" || this.status === "corrected") approvedSnapshot.set(this, serializeFrozenFields(this));
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
assertStatusTransition(prior) {
|
|
172
|
+
if (prior === void 0) return;
|
|
173
|
+
if (prior === this.status) return;
|
|
174
|
+
if (!(SERVICE_TIME_ENTRY_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`ServiceTimeEntry ${this.id}: illegal status transition '${prior}' \u2192 '${this.status}'.`);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Once approved, the work-defining fields are immutable — compare against
|
|
178
|
+
* the authoritative persisted row (not just the WeakMap), so an upsert via
|
|
179
|
+
* `create({ id, _skipLoad: true })` cannot bypass the freeze.
|
|
180
|
+
*/
|
|
181
|
+
assertApprovedImmutable(prior) {
|
|
182
|
+
if (!prior) return;
|
|
183
|
+
if (prior.status !== "approved" && prior.status !== "corrected") return;
|
|
184
|
+
const persisted = prior.frozen ?? approvedSnapshot.get(this) ?? null;
|
|
185
|
+
if (persisted === null) return;
|
|
186
|
+
if (serializeFrozenFields(this) !== persisted) throw new Error(`ServiceTimeEntry ${this.id}: approved entries are immutable \u2014 create an explicit correction instead of editing the approved record.`);
|
|
187
|
+
}
|
|
188
|
+
/** Authoritative prior state read from the DB row (S5 #1390 idiom). */
|
|
189
|
+
async resolvePriorState() {
|
|
190
|
+
if (this.id) try {
|
|
191
|
+
const row = await this.db.get(this.tableName, { id: this.id });
|
|
192
|
+
if (row && row.status != null) {
|
|
193
|
+
const status2 = row.status;
|
|
194
|
+
let frozen = null;
|
|
195
|
+
if (status2 === "approved" || status2 === "corrected") {
|
|
196
|
+
const view = {};
|
|
197
|
+
for (const key of FROZEN_FIELDS) view[key] = row[key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)];
|
|
198
|
+
frozen = serializeFrozenView(view);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
status: status2,
|
|
202
|
+
frozen
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
} catch {}
|
|
206
|
+
const status = loadedEntryStatus.get(this);
|
|
207
|
+
if (status === void 0) return void 0;
|
|
208
|
+
return {
|
|
209
|
+
status,
|
|
210
|
+
frozen: approvedSnapshot.get(this) ?? null
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
__decorateClass$12([tenantId({ nullable: true })], ServiceTimeEntry.prototype, "tenantId", 2);
|
|
215
|
+
__decorateClass$12([foreignKey("SupportCase")], ServiceTimeEntry.prototype, "caseId", 2);
|
|
216
|
+
__decorateClass$12([field({
|
|
217
|
+
type: "text",
|
|
218
|
+
nullable: true
|
|
219
|
+
})], ServiceTimeEntry.prototype, "workRefType", 2);
|
|
220
|
+
__decorateClass$12([field({
|
|
221
|
+
type: "text",
|
|
222
|
+
nullable: true
|
|
223
|
+
})], ServiceTimeEntry.prototype, "workRefId", 2);
|
|
224
|
+
__decorateClass$12([foreignKey("SupportSpecialist")], ServiceTimeEntry.prototype, "specialistId", 2);
|
|
225
|
+
__decorateClass$12([field({ type: "text" })], ServiceTimeEntry.prototype, "participantKind", 2);
|
|
226
|
+
__decorateClass$12([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], ServiceTimeEntry.prototype, "participantProfileId", 2);
|
|
227
|
+
__decorateClass$12([field({ type: "text" })], ServiceTimeEntry.prototype, "agentRef", 2);
|
|
228
|
+
__decorateClass$12([field({ type: "text" })], ServiceTimeEntry.prototype, "source", 2);
|
|
229
|
+
__decorateClass$12([field({ type: "text" })], ServiceTimeEntry.prototype, "description", 2);
|
|
230
|
+
__decorateClass$12([field({ type: "text" })], ServiceTimeEntry.prototype, "evidence", 2);
|
|
231
|
+
__decorateClass$12([field({ type: "text" })], ServiceTimeEntry.prototype, "status", 2);
|
|
232
|
+
__decorateClass$12([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], ServiceTimeEntry.prototype, "submittedByProfileId", 2);
|
|
233
|
+
__decorateClass$12([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], ServiceTimeEntry.prototype, "approvedByProfileId", 2);
|
|
234
|
+
__decorateClass$12([field({ type: "text" })], ServiceTimeEntry.prototype, "approvalPath", 2);
|
|
235
|
+
__decorateClass$12([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], ServiceTimeEntry.prototype, "rejectedByProfileId", 2);
|
|
236
|
+
__decorateClass$12([field({ type: "text" })], ServiceTimeEntry.prototype, "rejectionReason", 2);
|
|
237
|
+
__decorateClass$12([foreignKey("ServiceTimeEntry")], ServiceTimeEntry.prototype, "correctionOfId", 2);
|
|
238
|
+
__decorateClass$12([field({ type: "text" })], ServiceTimeEntry.prototype, "metadata", 2);
|
|
239
|
+
ServiceTimeEntry = __decorateClass$12([TenantScoped({ mode: "optional" }), smrt({
|
|
240
|
+
tableName: "service_time_entries",
|
|
241
|
+
api: { include: ["list", "get"] },
|
|
242
|
+
mcp: { include: ["list", "get"] },
|
|
243
|
+
cli: { include: ["list", "get"] }
|
|
244
|
+
})], ServiceTimeEntry);
|
|
245
|
+
var ServiceTimeEntryCollection = class extends SmrtCollection {
|
|
246
|
+
static _itemClass = ServiceTimeEntry;
|
|
247
|
+
async forCase(caseId) {
|
|
248
|
+
return this.list({
|
|
249
|
+
where: { caseId },
|
|
250
|
+
orderBy: "created_at ASC"
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async forSpecialist(specialistId) {
|
|
254
|
+
return this.list({
|
|
255
|
+
where: { specialistId },
|
|
256
|
+
orderBy: "created_at ASC"
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async pendingApproval() {
|
|
260
|
+
return this.list({
|
|
261
|
+
where: { status: "submitted" },
|
|
262
|
+
orderBy: "submitted_at ASC"
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/models/support-ai-run.ts
|
|
268
|
+
var __defProp$11 = Object.defineProperty;
|
|
269
|
+
var __getOwnPropDesc$11 = Object.getOwnPropertyDescriptor;
|
|
270
|
+
var __decorateClass$11 = (decorators, target, key, kind) => {
|
|
271
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$11(target, key) : target;
|
|
272
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
273
|
+
if (kind && result) __defProp$11(target, key, result);
|
|
274
|
+
return result;
|
|
275
|
+
};
|
|
276
|
+
var SupportAiRun = class extends SmrtObject {
|
|
277
|
+
tenantId = null;
|
|
278
|
+
caseId = "";
|
|
279
|
+
interactionId = null;
|
|
280
|
+
phase = "acknowledge";
|
|
281
|
+
outcome = "completed";
|
|
282
|
+
confidence = null;
|
|
283
|
+
classification = "{}";
|
|
284
|
+
responseInteractionId = null;
|
|
285
|
+
knowledgeRefs = "[]";
|
|
286
|
+
toolCalls = "[]";
|
|
287
|
+
error = "";
|
|
288
|
+
correlationId = "";
|
|
289
|
+
model = "";
|
|
290
|
+
startedAt = /* @__PURE__ */ new Date();
|
|
291
|
+
completedAt = null;
|
|
292
|
+
metadata = "{}";
|
|
293
|
+
getClassification() {
|
|
294
|
+
return parseJsonField(this.classification, {});
|
|
295
|
+
}
|
|
296
|
+
setClassification(value) {
|
|
297
|
+
this.classification = JSON.stringify(value ?? {});
|
|
298
|
+
}
|
|
299
|
+
getKnowledgeRefs() {
|
|
300
|
+
return parseJsonField(this.knowledgeRefs, []);
|
|
301
|
+
}
|
|
302
|
+
setKnowledgeRefs(refs) {
|
|
303
|
+
this.knowledgeRefs = JSON.stringify(refs ?? []);
|
|
304
|
+
}
|
|
305
|
+
getToolCalls() {
|
|
306
|
+
return parseJsonField(this.toolCalls, []);
|
|
307
|
+
}
|
|
308
|
+
setToolCalls(calls) {
|
|
309
|
+
this.toolCalls = JSON.stringify(calls ?? []);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
__decorateClass$11([tenantId({ nullable: true })], SupportAiRun.prototype, "tenantId", 2);
|
|
313
|
+
__decorateClass$11([foreignKey("SupportCase", { required: true })], SupportAiRun.prototype, "caseId", 2);
|
|
314
|
+
__decorateClass$11([foreignKey("SupportInteraction")], SupportAiRun.prototype, "interactionId", 2);
|
|
315
|
+
__decorateClass$11([field({ type: "text" })], SupportAiRun.prototype, "phase", 2);
|
|
316
|
+
__decorateClass$11([field({ type: "text" })], SupportAiRun.prototype, "outcome", 2);
|
|
317
|
+
__decorateClass$11([field({
|
|
318
|
+
type: "decimal",
|
|
319
|
+
nullable: true
|
|
320
|
+
})], SupportAiRun.prototype, "confidence", 2);
|
|
321
|
+
__decorateClass$11([field({ type: "text" })], SupportAiRun.prototype, "classification", 2);
|
|
322
|
+
__decorateClass$11([foreignKey("SupportInteraction")], SupportAiRun.prototype, "responseInteractionId", 2);
|
|
323
|
+
__decorateClass$11([field({ type: "text" })], SupportAiRun.prototype, "knowledgeRefs", 2);
|
|
324
|
+
__decorateClass$11([field({ type: "text" })], SupportAiRun.prototype, "toolCalls", 2);
|
|
325
|
+
__decorateClass$11([field({ type: "text" })], SupportAiRun.prototype, "error", 2);
|
|
326
|
+
__decorateClass$11([field({ type: "text" })], SupportAiRun.prototype, "correlationId", 2);
|
|
327
|
+
__decorateClass$11([field({ type: "text" })], SupportAiRun.prototype, "model", 2);
|
|
328
|
+
__decorateClass$11([field({ type: "text" })], SupportAiRun.prototype, "metadata", 2);
|
|
329
|
+
SupportAiRun = __decorateClass$11([TenantScoped({ mode: "optional" }), smrt({
|
|
330
|
+
tableName: "support_ai_runs",
|
|
331
|
+
api: { include: ["list", "get"] },
|
|
332
|
+
mcp: { include: ["list", "get"] },
|
|
333
|
+
cli: { include: ["list", "get"] }
|
|
334
|
+
})], SupportAiRun);
|
|
335
|
+
var SupportAiRunCollection = class extends SmrtCollection {
|
|
336
|
+
static _itemClass = SupportAiRun;
|
|
337
|
+
/** All AI runs for a case, oldest first. */
|
|
338
|
+
async forCase(caseId) {
|
|
339
|
+
return this.list({
|
|
340
|
+
where: { caseId },
|
|
341
|
+
orderBy: "started_at ASC"
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/models/support-case.ts
|
|
347
|
+
var __defProp$10 = Object.defineProperty;
|
|
348
|
+
var __getOwnPropDesc$10 = Object.getOwnPropertyDescriptor;
|
|
349
|
+
var __decorateClass$10 = (decorators, target, key, kind) => {
|
|
350
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$10(target, key) : target;
|
|
351
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
352
|
+
if (kind && result) __defProp$10(target, key, result);
|
|
353
|
+
return result;
|
|
354
|
+
};
|
|
355
|
+
var loadedCaseStatus = /* @__PURE__ */ new WeakMap();
|
|
356
|
+
function generateCaseNumber(now = /* @__PURE__ */ new Date()) {
|
|
357
|
+
return `SUP-${now.getTime().toString(36).toUpperCase()}-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;
|
|
358
|
+
}
|
|
359
|
+
var SupportCase = class extends SmrtObject {
|
|
360
|
+
tenantId = null;
|
|
361
|
+
caseNumber = "";
|
|
362
|
+
subject = "";
|
|
363
|
+
description = "";
|
|
364
|
+
status = "new";
|
|
365
|
+
priority = "normal";
|
|
366
|
+
severity = "";
|
|
367
|
+
category = "";
|
|
368
|
+
sensitive = false;
|
|
369
|
+
channelKind = "";
|
|
370
|
+
clientProfileId = null;
|
|
371
|
+
openedByProfileId = null;
|
|
372
|
+
projectId = null;
|
|
373
|
+
bindingId = null;
|
|
374
|
+
threadKey = "";
|
|
375
|
+
planId = null;
|
|
376
|
+
planSnapshot = "{}";
|
|
377
|
+
assignedSpecialistId = null;
|
|
378
|
+
assignedAt = null;
|
|
379
|
+
preferredSpecialistId = null;
|
|
380
|
+
/** Current escalation level (0 = never escalated). */
|
|
381
|
+
escalationLevel = 0;
|
|
382
|
+
escalatedAt = null;
|
|
383
|
+
/** First outbound acknowledgement of any kind (starts as `null`). */
|
|
384
|
+
acknowledgedAt = null;
|
|
385
|
+
/** First substantive outbound response (specialist or agent). */
|
|
386
|
+
firstRespondedAt = null;
|
|
387
|
+
/** Set when the client explicitly requested a human (FR-28b). */
|
|
388
|
+
humanRequestedAt = null;
|
|
389
|
+
resolvedAt = null;
|
|
390
|
+
resolvedByProfileId = null;
|
|
391
|
+
resolutionKind = "";
|
|
392
|
+
resolutionSummary = "";
|
|
393
|
+
closedAt = null;
|
|
394
|
+
/** Times this case was reopened; prior resolutions live in case events. */
|
|
395
|
+
reopenCount = 0;
|
|
396
|
+
lastReopenedAt = null;
|
|
397
|
+
aiEnabled = true;
|
|
398
|
+
metadata = "{}";
|
|
399
|
+
/** Whether the case is in an open (not resolved/closed) state. */
|
|
400
|
+
isOpen() {
|
|
401
|
+
return OPEN_SUPPORT_CASE_STATUSES.includes(this.status);
|
|
402
|
+
}
|
|
403
|
+
getMetadata() {
|
|
404
|
+
return parseJsonField(this.metadata, {});
|
|
405
|
+
}
|
|
406
|
+
setMetadata(value) {
|
|
407
|
+
this.metadata = JSON.stringify(value ?? {});
|
|
408
|
+
}
|
|
409
|
+
updateMetadata(patch) {
|
|
410
|
+
this.setMetadata({
|
|
411
|
+
...this.getMetadata(),
|
|
412
|
+
...patch
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
getPlanSnapshot() {
|
|
416
|
+
return parseJsonField(this.planSnapshot, {});
|
|
417
|
+
}
|
|
418
|
+
setPlanSnapshot(value) {
|
|
419
|
+
this.planSnapshot = JSON.stringify(value ?? {});
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Capture the status the row was loaded with so the save-time guard can
|
|
423
|
+
* reject illegal flips made via raw field assignment.
|
|
424
|
+
*/
|
|
425
|
+
async initialize() {
|
|
426
|
+
await super.initialize();
|
|
427
|
+
if (await this.isSaved()) loadedCaseStatus.set(this, this.status);
|
|
428
|
+
return this;
|
|
429
|
+
}
|
|
430
|
+
/** Validate the status transition before persisting, then save. */
|
|
431
|
+
async save() {
|
|
432
|
+
const prior = await this.resolvePriorStatus();
|
|
433
|
+
this.assertStatusTransition(prior);
|
|
434
|
+
const result = await super.save();
|
|
435
|
+
loadedCaseStatus.set(this, this.status);
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
assertStatusTransition(prior) {
|
|
439
|
+
if (prior === void 0) return;
|
|
440
|
+
if (prior === this.status) return;
|
|
441
|
+
if (!(SUPPORT_CASE_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`SupportCase ${this.caseNumber || this.id}: illegal status transition '${prior}' \u2192 '${this.status}'.`);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Resolve the AUTHORITATIVE prior status. The WeakMap only covers rows
|
|
445
|
+
* hydrated through {@link initialize}; a `create({ id, _skipLoad: true })`
|
|
446
|
+
* upsert would otherwise look brand-new and skip the guard, so when the
|
|
447
|
+
* instance carries an id, read the persisted row directly (S5 #1390 idiom).
|
|
448
|
+
*/
|
|
449
|
+
async resolvePriorStatus() {
|
|
450
|
+
if (this.id) try {
|
|
451
|
+
const row = await this.db.get(this.tableName, { id: this.id });
|
|
452
|
+
if (row && row.status != null) return row.status;
|
|
453
|
+
} catch {}
|
|
454
|
+
return loadedCaseStatus.get(this);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
__decorateClass$10([tenantId({ nullable: true })], SupportCase.prototype, "tenantId", 2);
|
|
458
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "caseNumber", 2);
|
|
459
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "subject", 2);
|
|
460
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "description", 2);
|
|
461
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "status", 2);
|
|
462
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "priority", 2);
|
|
463
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "severity", 2);
|
|
464
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "category", 2);
|
|
465
|
+
__decorateClass$10([field({ type: "boolean" })], SupportCase.prototype, "sensitive", 2);
|
|
466
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "channelKind", 2);
|
|
467
|
+
__decorateClass$10([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], SupportCase.prototype, "clientProfileId", 2);
|
|
468
|
+
__decorateClass$10([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], SupportCase.prototype, "openedByProfileId", 2);
|
|
469
|
+
__decorateClass$10([field({
|
|
470
|
+
type: "text",
|
|
471
|
+
nullable: true
|
|
472
|
+
})], SupportCase.prototype, "projectId", 2);
|
|
473
|
+
__decorateClass$10([foreignKey("SupportChannelBinding")], SupportCase.prototype, "bindingId", 2);
|
|
474
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "threadKey", 2);
|
|
475
|
+
__decorateClass$10([foreignKey("SupportPlan")], SupportCase.prototype, "planId", 2);
|
|
476
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "planSnapshot", 2);
|
|
477
|
+
__decorateClass$10([foreignKey("SupportSpecialist")], SupportCase.prototype, "assignedSpecialistId", 2);
|
|
478
|
+
__decorateClass$10([foreignKey("SupportSpecialist")], SupportCase.prototype, "preferredSpecialistId", 2);
|
|
479
|
+
__decorateClass$10([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], SupportCase.prototype, "resolvedByProfileId", 2);
|
|
480
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "resolutionKind", 2);
|
|
481
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "resolutionSummary", 2);
|
|
482
|
+
__decorateClass$10([field({ type: "boolean" })], SupportCase.prototype, "aiEnabled", 2);
|
|
483
|
+
__decorateClass$10([field({ type: "text" })], SupportCase.prototype, "metadata", 2);
|
|
484
|
+
SupportCase = __decorateClass$10([TenantScoped({ mode: "optional" }), smrt({
|
|
485
|
+
tableName: "support_cases",
|
|
486
|
+
api: { include: ["list", "get"] },
|
|
487
|
+
mcp: { include: ["list", "get"] },
|
|
488
|
+
cli: { include: ["list", "get"] }
|
|
489
|
+
})], SupportCase);
|
|
490
|
+
var SupportCaseCollection = class extends SmrtCollection {
|
|
491
|
+
static _itemClass = SupportCase;
|
|
492
|
+
/**
|
|
493
|
+
* Find the open case an inbound interaction should join, by conversation
|
|
494
|
+
* key (create-or-join dedup, FR-28). Returns the most recently updated open
|
|
495
|
+
* case for the key, or `null` when a new case should be created.
|
|
496
|
+
*/
|
|
497
|
+
async findOpenByThreadKey(threadKey, options = {}) {
|
|
498
|
+
const where = {
|
|
499
|
+
threadKey,
|
|
500
|
+
"status in": OPEN_SUPPORT_CASE_STATUSES
|
|
501
|
+
};
|
|
502
|
+
if (options.bindingId !== void 0 && options.bindingId !== null) where.bindingId = options.bindingId;
|
|
503
|
+
return (await this.list({
|
|
504
|
+
where,
|
|
505
|
+
orderBy: "updated_at DESC",
|
|
506
|
+
limit: 1
|
|
507
|
+
}))[0] ?? null;
|
|
508
|
+
}
|
|
509
|
+
/** Queue listing with common filters, newest first. */
|
|
510
|
+
async findQueue(options = {}) {
|
|
511
|
+
const where = {};
|
|
512
|
+
if (options.status !== void 0) if (Array.isArray(options.status)) where["status in"] = options.status;
|
|
513
|
+
else where.status = options.status;
|
|
514
|
+
else if (options.openOnly) where["status in"] = OPEN_SUPPORT_CASE_STATUSES;
|
|
515
|
+
if (options.assignedSpecialistId !== void 0) where.assignedSpecialistId = options.assignedSpecialistId;
|
|
516
|
+
if (options.clientProfileId !== void 0) where.clientProfileId = options.clientProfileId;
|
|
517
|
+
if (options.projectId !== void 0) where.projectId = options.projectId;
|
|
518
|
+
return this.list({
|
|
519
|
+
where,
|
|
520
|
+
orderBy: "updated_at DESC",
|
|
521
|
+
limit: options.limit ?? 50,
|
|
522
|
+
offset: options.offset ?? 0
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/models/support-case-event.ts
|
|
528
|
+
var __defProp$9 = Object.defineProperty;
|
|
529
|
+
var __getOwnPropDesc$9 = Object.getOwnPropertyDescriptor;
|
|
530
|
+
var __decorateClass$9 = (decorators, target, key, kind) => {
|
|
531
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$9(target, key) : target;
|
|
532
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
533
|
+
if (kind && result) __defProp$9(target, key, result);
|
|
534
|
+
return result;
|
|
535
|
+
};
|
|
536
|
+
var SupportCaseEvent = class extends SmrtObject {
|
|
537
|
+
tenantId = null;
|
|
538
|
+
caseId = "";
|
|
539
|
+
eventType = "note";
|
|
540
|
+
actorKind = "system";
|
|
541
|
+
actorProfileId = null;
|
|
542
|
+
occurredAt = /* @__PURE__ */ new Date();
|
|
543
|
+
summary = "";
|
|
544
|
+
payload = "{}";
|
|
545
|
+
getPayload() {
|
|
546
|
+
return parseJsonField(this.payload, {});
|
|
547
|
+
}
|
|
548
|
+
setPayload(value) {
|
|
549
|
+
this.payload = JSON.stringify(value ?? {});
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
__decorateClass$9([tenantId({ nullable: true })], SupportCaseEvent.prototype, "tenantId", 2);
|
|
553
|
+
__decorateClass$9([foreignKey("SupportCase", { required: true })], SupportCaseEvent.prototype, "caseId", 2);
|
|
554
|
+
__decorateClass$9([field({ type: "text" })], SupportCaseEvent.prototype, "eventType", 2);
|
|
555
|
+
__decorateClass$9([field({ type: "text" })], SupportCaseEvent.prototype, "actorKind", 2);
|
|
556
|
+
__decorateClass$9([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], SupportCaseEvent.prototype, "actorProfileId", 2);
|
|
557
|
+
__decorateClass$9([field({ type: "text" })], SupportCaseEvent.prototype, "summary", 2);
|
|
558
|
+
__decorateClass$9([field({ type: "text" })], SupportCaseEvent.prototype, "payload", 2);
|
|
559
|
+
SupportCaseEvent = __decorateClass$9([TenantScoped({ mode: "optional" }), smrt({
|
|
560
|
+
tableName: "support_case_events",
|
|
561
|
+
api: { include: ["list", "get"] },
|
|
562
|
+
mcp: { include: ["list", "get"] },
|
|
563
|
+
cli: { include: ["list", "get"] }
|
|
564
|
+
})], SupportCaseEvent);
|
|
565
|
+
var SupportCaseEventCollection = class extends SmrtCollection {
|
|
566
|
+
static _itemClass = SupportCaseEvent;
|
|
567
|
+
/** All events for a case, oldest first. */
|
|
568
|
+
async forCase(caseId, options = {}) {
|
|
569
|
+
const where = { caseId };
|
|
570
|
+
if (options.eventType !== void 0) where.eventType = options.eventType;
|
|
571
|
+
return this.list({
|
|
572
|
+
where,
|
|
573
|
+
orderBy: "occurred_at ASC"
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/models/support-channel-binding.ts
|
|
579
|
+
var __defProp$8 = Object.defineProperty;
|
|
580
|
+
var __getOwnPropDesc$8 = Object.getOwnPropertyDescriptor;
|
|
581
|
+
var __decorateClass$8 = (decorators, target, key, kind) => {
|
|
582
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$8(target, key) : target;
|
|
583
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
584
|
+
if (kind && result) __defProp$8(target, key, result);
|
|
585
|
+
return result;
|
|
586
|
+
};
|
|
587
|
+
var SupportChannelBinding = class extends SmrtObject {
|
|
588
|
+
tenantId = null;
|
|
589
|
+
/** Operator-facing binding name (inherited `name` is the display label). */
|
|
590
|
+
name = "";
|
|
591
|
+
bindingKind = "chat_room";
|
|
592
|
+
channelKind = "chat";
|
|
593
|
+
targetType = "";
|
|
594
|
+
targetId = "";
|
|
595
|
+
clientProfileId = null;
|
|
596
|
+
projectId = null;
|
|
597
|
+
planId = null;
|
|
598
|
+
enabled = true;
|
|
599
|
+
selfAddresses = "[]";
|
|
600
|
+
metadata = "{}";
|
|
601
|
+
getSelfAddresses() {
|
|
602
|
+
return parseStringArrayField(this.selfAddresses).map((address) => address.toLowerCase());
|
|
603
|
+
}
|
|
604
|
+
setSelfAddresses(addresses) {
|
|
605
|
+
this.selfAddresses = JSON.stringify(addresses ?? []);
|
|
606
|
+
}
|
|
607
|
+
getMetadata() {
|
|
608
|
+
return parseJsonField(this.metadata, {});
|
|
609
|
+
}
|
|
610
|
+
setMetadata(value) {
|
|
611
|
+
this.metadata = JSON.stringify(value ?? {});
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
__decorateClass$8([tenantId({ nullable: true })], SupportChannelBinding.prototype, "tenantId", 2);
|
|
615
|
+
__decorateClass$8([field({ type: "text" })], SupportChannelBinding.prototype, "bindingKind", 2);
|
|
616
|
+
__decorateClass$8([field({ type: "text" })], SupportChannelBinding.prototype, "channelKind", 2);
|
|
617
|
+
__decorateClass$8([field({ type: "text" })], SupportChannelBinding.prototype, "targetType", 2);
|
|
618
|
+
__decorateClass$8([field({ type: "text" })], SupportChannelBinding.prototype, "targetId", 2);
|
|
619
|
+
__decorateClass$8([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], SupportChannelBinding.prototype, "clientProfileId", 2);
|
|
620
|
+
__decorateClass$8([field({
|
|
621
|
+
type: "text",
|
|
622
|
+
nullable: true
|
|
623
|
+
})], SupportChannelBinding.prototype, "projectId", 2);
|
|
624
|
+
__decorateClass$8([foreignKey("SupportPlan")], SupportChannelBinding.prototype, "planId", 2);
|
|
625
|
+
__decorateClass$8([field({ type: "boolean" })], SupportChannelBinding.prototype, "enabled", 2);
|
|
626
|
+
__decorateClass$8([field({ type: "text" })], SupportChannelBinding.prototype, "selfAddresses", 2);
|
|
627
|
+
__decorateClass$8([field({ type: "text" })], SupportChannelBinding.prototype, "metadata", 2);
|
|
628
|
+
SupportChannelBinding = __decorateClass$8([TenantScoped({ mode: "optional" }), smrt({
|
|
629
|
+
tableName: "support_channel_bindings",
|
|
630
|
+
api: { include: [
|
|
631
|
+
"list",
|
|
632
|
+
"get",
|
|
633
|
+
"create",
|
|
634
|
+
"update",
|
|
635
|
+
"delete"
|
|
636
|
+
] },
|
|
637
|
+
mcp: { include: ["list", "get"] },
|
|
638
|
+
cli: { include: ["list", "get"] },
|
|
639
|
+
conflictColumns: ["target_type", "target_id"]
|
|
640
|
+
})], SupportChannelBinding);
|
|
641
|
+
var SupportChannelBindingCollection = class extends SmrtCollection {
|
|
642
|
+
static _itemClass = SupportChannelBinding;
|
|
643
|
+
/** Find the enabled binding for a transport container, if any. */
|
|
644
|
+
async findForTarget(targetType, targetId) {
|
|
645
|
+
return (await this.list({
|
|
646
|
+
where: {
|
|
647
|
+
targetType,
|
|
648
|
+
targetId,
|
|
649
|
+
enabled: true
|
|
650
|
+
},
|
|
651
|
+
limit: 1
|
|
652
|
+
}))[0] ?? null;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/models/support-compensation-plan.ts
|
|
657
|
+
var __defProp$7 = Object.defineProperty;
|
|
658
|
+
var __getOwnPropDesc$7 = Object.getOwnPropertyDescriptor;
|
|
659
|
+
var __decorateClass$7 = (decorators, target, key, kind) => {
|
|
660
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$7(target, key) : target;
|
|
661
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
662
|
+
if (kind && result) __defProp$7(target, key, result);
|
|
663
|
+
return result;
|
|
664
|
+
};
|
|
665
|
+
var SupportCompensationPlan = class extends SmrtObject {
|
|
666
|
+
tenantId = null;
|
|
667
|
+
specialistId = null;
|
|
668
|
+
/** Display name (inherited `name`; uniqueness comes from conflictColumns). */
|
|
669
|
+
name = "";
|
|
670
|
+
/** Hourly earning rate for delivered support time. */
|
|
671
|
+
hourlyRate = 0;
|
|
672
|
+
currency = "USD";
|
|
673
|
+
/** Effective-dating: null bounds mean open-ended. */
|
|
674
|
+
effectiveFrom = null;
|
|
675
|
+
effectiveTo = null;
|
|
676
|
+
status = "active";
|
|
677
|
+
terms = "{}";
|
|
678
|
+
getTerms() {
|
|
679
|
+
return parseJsonField(this.terms, {});
|
|
680
|
+
}
|
|
681
|
+
setTerms(value) {
|
|
682
|
+
this.terms = JSON.stringify(value ?? {});
|
|
683
|
+
}
|
|
684
|
+
/** Whether the plan is effective at the given instant. */
|
|
685
|
+
isEffectiveAt(at = /* @__PURE__ */ new Date()) {
|
|
686
|
+
if (this.status !== "active") return false;
|
|
687
|
+
if (this.effectiveFrom && at < this.effectiveFrom) return false;
|
|
688
|
+
if (this.effectiveTo && at >= this.effectiveTo) return false;
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
__decorateClass$7([tenantId({ nullable: true })], SupportCompensationPlan.prototype, "tenantId", 2);
|
|
693
|
+
__decorateClass$7([foreignKey("SupportSpecialist")], SupportCompensationPlan.prototype, "specialistId", 2);
|
|
694
|
+
__decorateClass$7([field({ type: "text" })], SupportCompensationPlan.prototype, "currency", 2);
|
|
695
|
+
__decorateClass$7([field({ type: "text" })], SupportCompensationPlan.prototype, "status", 2);
|
|
696
|
+
__decorateClass$7([field({ type: "text" })], SupportCompensationPlan.prototype, "terms", 2);
|
|
697
|
+
SupportCompensationPlan = __decorateClass$7([TenantScoped({ mode: "optional" }), smrt({
|
|
698
|
+
tableName: "support_compensation_plans",
|
|
699
|
+
api: { include: ["list", "get"] },
|
|
700
|
+
mcp: { include: ["list", "get"] },
|
|
701
|
+
cli: { include: ["list", "get"] },
|
|
702
|
+
conflictColumns: [
|
|
703
|
+
"tenant_id",
|
|
704
|
+
"specialist_id",
|
|
705
|
+
"effective_from"
|
|
706
|
+
]
|
|
707
|
+
})], SupportCompensationPlan);
|
|
708
|
+
var SupportCompensationPlanCollection = class extends SmrtCollection {
|
|
709
|
+
static _itemClass = SupportCompensationPlan;
|
|
710
|
+
/**
|
|
711
|
+
* Resolve the plan compensating a specialist at a work instant:
|
|
712
|
+
* specialist-specific first, then the tenant default. Ties (overlapping
|
|
713
|
+
* effective ranges) prefer an exact tenant match over a global
|
|
714
|
+
* (NULL-tenant) plan, then the latest `effectiveFrom`.
|
|
715
|
+
*
|
|
716
|
+
* Pass `options.tenantId` (the work's tenant) whenever resolution may run
|
|
717
|
+
* without an ambient tenant context (system/job paths list across
|
|
718
|
+
* tenants): candidates are then limited to that tenant's plans plus
|
|
719
|
+
* global NULL-tenant plans, so another tenant's default can never price
|
|
720
|
+
* this tenant's work.
|
|
721
|
+
*/
|
|
722
|
+
async resolveForSpecialist(specialistId, at = /* @__PURE__ */ new Date(), options = {}) {
|
|
723
|
+
const candidates = await this.list({ where: { status: "active" } });
|
|
724
|
+
const tenantId2 = options.tenantId;
|
|
725
|
+
const effective = candidates.filter((plan) => plan.isEffectiveAt(at) && (tenantId2 === void 0 || plan.tenantId === null || plan.tenantId === tenantId2));
|
|
726
|
+
const pick = (plans) => plans.sort((a, b) => {
|
|
727
|
+
if (tenantId2 !== void 0) {
|
|
728
|
+
const aExact = a.tenantId === tenantId2 ? 1 : 0;
|
|
729
|
+
const bExact = b.tenantId === tenantId2 ? 1 : 0;
|
|
730
|
+
if (aExact !== bExact) return bExact - aExact;
|
|
731
|
+
}
|
|
732
|
+
return (b.effectiveFrom?.getTime() ?? 0) - (a.effectiveFrom?.getTime() ?? 0);
|
|
733
|
+
})[0] ?? null;
|
|
734
|
+
const specific = pick(effective.filter((plan) => plan.specialistId === specialistId));
|
|
735
|
+
if (specific) return specific;
|
|
736
|
+
return pick(effective.filter((plan) => plan.specialistId === null));
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/models/support-interaction.ts
|
|
741
|
+
var __defProp$6 = Object.defineProperty;
|
|
742
|
+
var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
|
|
743
|
+
var __decorateClass$6 = (decorators, target, key, kind) => {
|
|
744
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
|
|
745
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
746
|
+
if (kind && result) __defProp$6(target, key, result);
|
|
747
|
+
return result;
|
|
748
|
+
};
|
|
749
|
+
var SupportInteraction = class extends SmrtObject {
|
|
750
|
+
tenantId = null;
|
|
751
|
+
caseId = "";
|
|
752
|
+
direction = "inbound";
|
|
753
|
+
channelKind = "chat";
|
|
754
|
+
actorKind = "client";
|
|
755
|
+
authorProfileId = null;
|
|
756
|
+
/** When the exchange happened on its channel (not when it was ingested). */
|
|
757
|
+
occurredAt = /* @__PURE__ */ new Date();
|
|
758
|
+
body = "";
|
|
759
|
+
sourceType = null;
|
|
760
|
+
sourceId = null;
|
|
761
|
+
sourceKey = "";
|
|
762
|
+
rfcMessageId = "";
|
|
763
|
+
metadata = "{}";
|
|
764
|
+
getMetadata() {
|
|
765
|
+
return parseJsonField(this.metadata, {});
|
|
766
|
+
}
|
|
767
|
+
setMetadata(value) {
|
|
768
|
+
this.metadata = JSON.stringify(value ?? {});
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
__decorateClass$6([tenantId({ nullable: true })], SupportInteraction.prototype, "tenantId", 2);
|
|
772
|
+
__decorateClass$6([foreignKey("SupportCase", { required: true })], SupportInteraction.prototype, "caseId", 2);
|
|
773
|
+
__decorateClass$6([field({ type: "text" })], SupportInteraction.prototype, "direction", 2);
|
|
774
|
+
__decorateClass$6([field({ type: "text" })], SupportInteraction.prototype, "channelKind", 2);
|
|
775
|
+
__decorateClass$6([field({ type: "text" })], SupportInteraction.prototype, "actorKind", 2);
|
|
776
|
+
__decorateClass$6([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], SupportInteraction.prototype, "authorProfileId", 2);
|
|
777
|
+
__decorateClass$6([field({ type: "text" })], SupportInteraction.prototype, "body", 2);
|
|
778
|
+
__decorateClass$6([field({
|
|
779
|
+
type: "text",
|
|
780
|
+
nullable: true
|
|
781
|
+
})], SupportInteraction.prototype, "sourceType", 2);
|
|
782
|
+
__decorateClass$6([field({
|
|
783
|
+
type: "text",
|
|
784
|
+
nullable: true
|
|
785
|
+
})], SupportInteraction.prototype, "sourceId", 2);
|
|
786
|
+
__decorateClass$6([field({ type: "text" })], SupportInteraction.prototype, "sourceKey", 2);
|
|
787
|
+
__decorateClass$6([field({ type: "text" })], SupportInteraction.prototype, "rfcMessageId", 2);
|
|
788
|
+
__decorateClass$6([field({ type: "text" })], SupportInteraction.prototype, "metadata", 2);
|
|
789
|
+
SupportInteraction = __decorateClass$6([TenantScoped({ mode: "optional" }), smrt({
|
|
790
|
+
tableName: "support_interactions",
|
|
791
|
+
api: { include: ["list", "get"] },
|
|
792
|
+
mcp: { include: ["list", "get"] },
|
|
793
|
+
cli: { include: ["list", "get"] },
|
|
794
|
+
conflictColumns: ["source_key"]
|
|
795
|
+
})], SupportInteraction);
|
|
796
|
+
var SupportInteractionCollection = class extends SmrtCollection {
|
|
797
|
+
static _itemClass = SupportInteraction;
|
|
798
|
+
/** All interactions for a case in channel chronology (oldest first). */
|
|
799
|
+
async forCase(caseId) {
|
|
800
|
+
return this.list({
|
|
801
|
+
where: { caseId },
|
|
802
|
+
orderBy: "occurred_at ASC"
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
/** Look up an interaction by its idempotency key. */
|
|
806
|
+
async bySourceKey(sourceKey) {
|
|
807
|
+
return (await this.list({
|
|
808
|
+
where: { sourceKey },
|
|
809
|
+
limit: 1
|
|
810
|
+
}))[0] ?? null;
|
|
811
|
+
}
|
|
812
|
+
/** Keyed lookup of an email interaction by its RFC 822 Message-ID. */
|
|
813
|
+
async byRfcMessageId(rfcMessageId) {
|
|
814
|
+
if (!rfcMessageId) return null;
|
|
815
|
+
return (await this.list({
|
|
816
|
+
where: { rfcMessageId },
|
|
817
|
+
limit: 1
|
|
818
|
+
}))[0] ?? null;
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/models/support-plan.ts
|
|
823
|
+
var __defProp$5 = Object.defineProperty;
|
|
824
|
+
var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
|
|
825
|
+
var __decorateClass$5 = (decorators, target, key, kind) => {
|
|
826
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
|
|
827
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
828
|
+
if (kind && result) __defProp$5(target, key, result);
|
|
829
|
+
return result;
|
|
830
|
+
};
|
|
831
|
+
var DEFAULT_TARGET_MINUTES = {
|
|
832
|
+
acknowledgementMinutes: 60,
|
|
833
|
+
responseMinutes: 240,
|
|
834
|
+
updateMinutes: null,
|
|
835
|
+
resolutionMinutes: null
|
|
836
|
+
};
|
|
837
|
+
var SupportPlan = class extends SmrtObject {
|
|
838
|
+
tenantId = null;
|
|
839
|
+
planKey = "";
|
|
840
|
+
/** Display name (inherited `name` field). */
|
|
841
|
+
name = "";
|
|
842
|
+
description = "";
|
|
843
|
+
status = "active";
|
|
844
|
+
channels = "[\"chat\",\"email\"]";
|
|
845
|
+
timezone = "UTC";
|
|
846
|
+
coverage = "[]";
|
|
847
|
+
holidays = "[]";
|
|
848
|
+
severityDefinitions = "{}";
|
|
849
|
+
targets = "{}";
|
|
850
|
+
pauseStatuses = "[\"waiting_on_client\"]";
|
|
851
|
+
escalationPolicy = "[]";
|
|
852
|
+
/** Recurring availability/retainer fee (0.0 = pure hourly support). */
|
|
853
|
+
availabilityFeeAmount = 0;
|
|
854
|
+
currency = "USD";
|
|
855
|
+
/** Included support minutes per period (0 = none included). */
|
|
856
|
+
includedMinutes = 0;
|
|
857
|
+
/** Metered hourly rate for time beyond the included minutes. */
|
|
858
|
+
overageHourlyRate = 0;
|
|
859
|
+
/** Premium hourly rate for on-call work (0.0 = use overage rate). */
|
|
860
|
+
onCallHourlyRate = 0;
|
|
861
|
+
timeApprovalPolicy = "{}";
|
|
862
|
+
subscriptionPlanKey = "";
|
|
863
|
+
metadata = "{}";
|
|
864
|
+
getChannels() {
|
|
865
|
+
return parseStringArrayField(this.channels);
|
|
866
|
+
}
|
|
867
|
+
setChannels(channels) {
|
|
868
|
+
this.channels = JSON.stringify(channels ?? []);
|
|
869
|
+
}
|
|
870
|
+
getCoverage() {
|
|
871
|
+
const parsed = parseJsonField(this.coverage, []);
|
|
872
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
873
|
+
}
|
|
874
|
+
setCoverage(windows) {
|
|
875
|
+
this.coverage = JSON.stringify(windows ?? []);
|
|
876
|
+
}
|
|
877
|
+
getHolidays() {
|
|
878
|
+
return parseStringArrayField(this.holidays);
|
|
879
|
+
}
|
|
880
|
+
setHolidays(dates) {
|
|
881
|
+
this.holidays = JSON.stringify(dates ?? []);
|
|
882
|
+
}
|
|
883
|
+
getSeverityDefinitions() {
|
|
884
|
+
const parsed = parseJsonField(this.severityDefinitions, {});
|
|
885
|
+
return Object.keys(parsed).length > 0 ? parsed : { ...DEFAULT_SEVERITY_DEFINITIONS };
|
|
886
|
+
}
|
|
887
|
+
setSeverityDefinitions(defs) {
|
|
888
|
+
this.severityDefinitions = JSON.stringify(defs ?? {});
|
|
889
|
+
}
|
|
890
|
+
getTargets() {
|
|
891
|
+
return parseJsonField(this.targets, {});
|
|
892
|
+
}
|
|
893
|
+
setTargets(targets) {
|
|
894
|
+
this.targets = JSON.stringify(targets ?? {});
|
|
895
|
+
}
|
|
896
|
+
/** Effective target minutes for a severity, with defaults applied. */
|
|
897
|
+
targetsForSeverity(severity) {
|
|
898
|
+
const configured = this.getTargets()[severity] ?? {};
|
|
899
|
+
return {
|
|
900
|
+
...DEFAULT_TARGET_MINUTES,
|
|
901
|
+
...configured
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
getPauseStatuses() {
|
|
905
|
+
return parseStringArrayField(this.pauseStatuses);
|
|
906
|
+
}
|
|
907
|
+
setPauseStatuses(statuses) {
|
|
908
|
+
this.pauseStatuses = JSON.stringify(statuses ?? []);
|
|
909
|
+
}
|
|
910
|
+
getEscalationPolicy() {
|
|
911
|
+
const parsed = parseJsonField(this.escalationPolicy, []);
|
|
912
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
913
|
+
}
|
|
914
|
+
setEscalationPolicy(steps) {
|
|
915
|
+
this.escalationPolicy = JSON.stringify(steps ?? []);
|
|
916
|
+
}
|
|
917
|
+
getTimeApprovalPolicy() {
|
|
918
|
+
return {
|
|
919
|
+
mode: "operator",
|
|
920
|
+
...parseJsonField(this.timeApprovalPolicy, {})
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
setTimeApprovalPolicy(policy) {
|
|
924
|
+
this.timeApprovalPolicy = JSON.stringify(policy ?? {});
|
|
925
|
+
}
|
|
926
|
+
getMetadata() {
|
|
927
|
+
return parseJsonField(this.metadata, {});
|
|
928
|
+
}
|
|
929
|
+
setMetadata(value) {
|
|
930
|
+
this.metadata = JSON.stringify(value ?? {});
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* The plan terms captured onto a case at apply time (`planSnapshot`), so
|
|
934
|
+
* later plan edits never rewrite the terms a case was handled under.
|
|
935
|
+
*/
|
|
936
|
+
snapshotTerms() {
|
|
937
|
+
return {
|
|
938
|
+
planId: this.id,
|
|
939
|
+
planKey: this.planKey,
|
|
940
|
+
name: this.name,
|
|
941
|
+
timezone: this.timezone,
|
|
942
|
+
channels: this.getChannels(),
|
|
943
|
+
coverage: this.getCoverage(),
|
|
944
|
+
holidays: this.getHolidays(),
|
|
945
|
+
severityDefinitions: this.getSeverityDefinitions(),
|
|
946
|
+
targets: this.getTargets(),
|
|
947
|
+
pauseStatuses: this.getPauseStatuses(),
|
|
948
|
+
escalationPolicy: this.getEscalationPolicy(),
|
|
949
|
+
availabilityFeeAmount: this.availabilityFeeAmount,
|
|
950
|
+
currency: this.currency,
|
|
951
|
+
includedMinutes: this.includedMinutes,
|
|
952
|
+
overageHourlyRate: this.overageHourlyRate,
|
|
953
|
+
onCallHourlyRate: this.onCallHourlyRate,
|
|
954
|
+
timeApprovalPolicy: this.getTimeApprovalPolicy(),
|
|
955
|
+
subscriptionPlanKey: this.subscriptionPlanKey,
|
|
956
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
__decorateClass$5([tenantId({ nullable: true })], SupportPlan.prototype, "tenantId", 2);
|
|
961
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "planKey", 2);
|
|
962
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "description", 2);
|
|
963
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "status", 2);
|
|
964
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "channels", 2);
|
|
965
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "timezone", 2);
|
|
966
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "coverage", 2);
|
|
967
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "holidays", 2);
|
|
968
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "severityDefinitions", 2);
|
|
969
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "targets", 2);
|
|
970
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "pauseStatuses", 2);
|
|
971
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "escalationPolicy", 2);
|
|
972
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "currency", 2);
|
|
973
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "timeApprovalPolicy", 2);
|
|
974
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "subscriptionPlanKey", 2);
|
|
975
|
+
__decorateClass$5([field({ type: "text" })], SupportPlan.prototype, "metadata", 2);
|
|
976
|
+
SupportPlan = __decorateClass$5([TenantScoped({ mode: "optional" }), smrt({
|
|
977
|
+
tableName: "support_plans",
|
|
978
|
+
api: { include: ["list", "get"] },
|
|
979
|
+
mcp: { include: ["list", "get"] },
|
|
980
|
+
cli: { include: ["list", "get"] },
|
|
981
|
+
conflictColumns: ["tenant_id", "plan_key"]
|
|
982
|
+
})], SupportPlan);
|
|
983
|
+
var SupportPlanCollection = class extends SmrtCollection {
|
|
984
|
+
static _itemClass = SupportPlan;
|
|
985
|
+
async byPlanKey(planKey) {
|
|
986
|
+
return (await this.list({
|
|
987
|
+
where: { planKey },
|
|
988
|
+
limit: 1
|
|
989
|
+
}))[0] ?? null;
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
//#endregion
|
|
993
|
+
//#region src/models/support-policy.ts
|
|
994
|
+
var __defProp$4 = Object.defineProperty;
|
|
995
|
+
var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
|
|
996
|
+
var __decorateClass$4 = (decorators, target, key, kind) => {
|
|
997
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
|
|
998
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
999
|
+
if (kind && result) __defProp$4(target, key, result);
|
|
1000
|
+
return result;
|
|
1001
|
+
};
|
|
1002
|
+
var DEFAULT_SUPPORT_POLICY = {
|
|
1003
|
+
autoAcknowledge: true,
|
|
1004
|
+
autoClassify: true,
|
|
1005
|
+
autoAnswer: true,
|
|
1006
|
+
autoTroubleshoot: false,
|
|
1007
|
+
autoResolve: false,
|
|
1008
|
+
autoResolveMaxSeverity: "",
|
|
1009
|
+
confidenceThreshold: .7,
|
|
1010
|
+
maxAutoAttempts: 1,
|
|
1011
|
+
autoSendEmailReplies: false,
|
|
1012
|
+
sensitiveCategories: [],
|
|
1013
|
+
allowedTools: []
|
|
1014
|
+
};
|
|
1015
|
+
var SupportPolicy = class extends SmrtObject {
|
|
1016
|
+
tenantId = null;
|
|
1017
|
+
/** Policy name, unique per tenant (`conflictColumns`). */
|
|
1018
|
+
name = "";
|
|
1019
|
+
planId = null;
|
|
1020
|
+
projectId = null;
|
|
1021
|
+
enabled = true;
|
|
1022
|
+
autoAcknowledge = true;
|
|
1023
|
+
autoClassify = true;
|
|
1024
|
+
autoAnswer = true;
|
|
1025
|
+
autoTroubleshoot = false;
|
|
1026
|
+
autoResolve = false;
|
|
1027
|
+
autoResolveMaxSeverity = "";
|
|
1028
|
+
/**
|
|
1029
|
+
* Confidence floor for automated answers/resolutions; below it the workflow
|
|
1030
|
+
* triggers a `low_confidence` Human Handoff.
|
|
1031
|
+
*/
|
|
1032
|
+
confidenceThreshold = .7;
|
|
1033
|
+
/** Max automated resolution attempts before a `failed_resolution` handoff. */
|
|
1034
|
+
maxAutoAttempts = 1;
|
|
1035
|
+
autoSendEmailReplies = false;
|
|
1036
|
+
sensitiveCategories = "[]";
|
|
1037
|
+
allowedTools = "[]";
|
|
1038
|
+
metadata = "{}";
|
|
1039
|
+
getSensitiveCategories() {
|
|
1040
|
+
return parseStringArrayField(this.sensitiveCategories);
|
|
1041
|
+
}
|
|
1042
|
+
setSensitiveCategories(categories) {
|
|
1043
|
+
this.sensitiveCategories = JSON.stringify(categories ?? []);
|
|
1044
|
+
}
|
|
1045
|
+
getAllowedTools() {
|
|
1046
|
+
return parseStringArrayField(this.allowedTools);
|
|
1047
|
+
}
|
|
1048
|
+
setAllowedTools(tools) {
|
|
1049
|
+
this.allowedTools = JSON.stringify(tools ?? []);
|
|
1050
|
+
}
|
|
1051
|
+
getMetadata() {
|
|
1052
|
+
return parseJsonField(this.metadata, {});
|
|
1053
|
+
}
|
|
1054
|
+
setMetadata(value) {
|
|
1055
|
+
this.metadata = JSON.stringify(value ?? {});
|
|
1056
|
+
}
|
|
1057
|
+
/** Specificity rank for resolution: exact scope wins over partial. */
|
|
1058
|
+
scopeRank() {
|
|
1059
|
+
let rank = 0;
|
|
1060
|
+
if (this.planId) rank += 2;
|
|
1061
|
+
if (this.projectId) rank += 1;
|
|
1062
|
+
return rank;
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
__decorateClass$4([tenantId({ nullable: true })], SupportPolicy.prototype, "tenantId", 2);
|
|
1066
|
+
__decorateClass$4([foreignKey("SupportPlan")], SupportPolicy.prototype, "planId", 2);
|
|
1067
|
+
__decorateClass$4([field({
|
|
1068
|
+
type: "text",
|
|
1069
|
+
nullable: true
|
|
1070
|
+
})], SupportPolicy.prototype, "projectId", 2);
|
|
1071
|
+
__decorateClass$4([field({ type: "boolean" })], SupportPolicy.prototype, "enabled", 2);
|
|
1072
|
+
__decorateClass$4([field({ type: "boolean" })], SupportPolicy.prototype, "autoAcknowledge", 2);
|
|
1073
|
+
__decorateClass$4([field({ type: "boolean" })], SupportPolicy.prototype, "autoClassify", 2);
|
|
1074
|
+
__decorateClass$4([field({ type: "boolean" })], SupportPolicy.prototype, "autoAnswer", 2);
|
|
1075
|
+
__decorateClass$4([field({ type: "boolean" })], SupportPolicy.prototype, "autoTroubleshoot", 2);
|
|
1076
|
+
__decorateClass$4([field({ type: "boolean" })], SupportPolicy.prototype, "autoResolve", 2);
|
|
1077
|
+
__decorateClass$4([field({ type: "text" })], SupportPolicy.prototype, "autoResolveMaxSeverity", 2);
|
|
1078
|
+
__decorateClass$4([field({ type: "boolean" })], SupportPolicy.prototype, "autoSendEmailReplies", 2);
|
|
1079
|
+
__decorateClass$4([field({ type: "text" })], SupportPolicy.prototype, "sensitiveCategories", 2);
|
|
1080
|
+
__decorateClass$4([field({ type: "text" })], SupportPolicy.prototype, "allowedTools", 2);
|
|
1081
|
+
__decorateClass$4([field({ type: "text" })], SupportPolicy.prototype, "metadata", 2);
|
|
1082
|
+
SupportPolicy = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
|
|
1083
|
+
tableName: "support_policies",
|
|
1084
|
+
api: { include: [
|
|
1085
|
+
"list",
|
|
1086
|
+
"get",
|
|
1087
|
+
"create",
|
|
1088
|
+
"update",
|
|
1089
|
+
"delete"
|
|
1090
|
+
] },
|
|
1091
|
+
mcp: { include: ["list", "get"] },
|
|
1092
|
+
cli: { include: ["list", "get"] },
|
|
1093
|
+
conflictColumns: ["tenant_id", "name"]
|
|
1094
|
+
})], SupportPolicy);
|
|
1095
|
+
var SupportPolicyCollection = class extends SmrtCollection {
|
|
1096
|
+
static _itemClass = SupportPolicy;
|
|
1097
|
+
/**
|
|
1098
|
+
* Resolve the effective policy for a case scope, most-specific-wins.
|
|
1099
|
+
* Returns `null` when no enabled policy matches (callers then apply
|
|
1100
|
+
* {@link DEFAULT_SUPPORT_POLICY}).
|
|
1101
|
+
*/
|
|
1102
|
+
async resolveForScope(options) {
|
|
1103
|
+
const policies = await this.list({ where: { enabled: true } });
|
|
1104
|
+
const planId = options.planId ?? null;
|
|
1105
|
+
const projectId = options.projectId ?? null;
|
|
1106
|
+
const applicable = policies.filter((policy) => {
|
|
1107
|
+
if (policy.planId && policy.planId !== planId) return false;
|
|
1108
|
+
if (policy.projectId && policy.projectId !== projectId) return false;
|
|
1109
|
+
return true;
|
|
1110
|
+
});
|
|
1111
|
+
applicable.sort((a, b) => b.scopeRank() - a.scopeRank());
|
|
1112
|
+
return applicable[0] ?? null;
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
//#endregion
|
|
1116
|
+
//#region src/models/support-service-target.ts
|
|
1117
|
+
var __defProp$3 = Object.defineProperty;
|
|
1118
|
+
var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
|
|
1119
|
+
var __decorateClass$3 = (decorators, target, key, kind) => {
|
|
1120
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
|
|
1121
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
1122
|
+
if (kind && result) __defProp$3(target, key, result);
|
|
1123
|
+
return result;
|
|
1124
|
+
};
|
|
1125
|
+
var SupportServiceTarget = class extends SmrtObject {
|
|
1126
|
+
tenantId = null;
|
|
1127
|
+
caseId = "";
|
|
1128
|
+
targetType = "acknowledgement";
|
|
1129
|
+
/**
|
|
1130
|
+
* Recurrence cycle for repeating clocks (`update` restarts after each
|
|
1131
|
+
* update). One-shot clocks stay at cycle 0.
|
|
1132
|
+
*/
|
|
1133
|
+
cycle = 0;
|
|
1134
|
+
severity = "";
|
|
1135
|
+
/** Configured clock length in covered minutes. */
|
|
1136
|
+
baseMinutes = 0;
|
|
1137
|
+
/** When the clock started running. */
|
|
1138
|
+
startedAt = /* @__PURE__ */ new Date();
|
|
1139
|
+
/** Current deadline, in wall-clock time (recomputed on pause/resume). */
|
|
1140
|
+
dueAt = /* @__PURE__ */ new Date();
|
|
1141
|
+
status = "pending";
|
|
1142
|
+
/** Set while the clock is paused (plan pause rules). */
|
|
1143
|
+
pausedAt = null;
|
|
1144
|
+
/** Accumulated paused time, excluded from the clock. */
|
|
1145
|
+
pausedTotalSeconds = 0;
|
|
1146
|
+
satisfiedAt = null;
|
|
1147
|
+
breachedAt = null;
|
|
1148
|
+
cancelledAt = null;
|
|
1149
|
+
escalationJobId = "";
|
|
1150
|
+
metadata = "{}";
|
|
1151
|
+
getMetadata() {
|
|
1152
|
+
return parseJsonField(this.metadata, {});
|
|
1153
|
+
}
|
|
1154
|
+
setMetadata(value) {
|
|
1155
|
+
this.metadata = JSON.stringify(value ?? {});
|
|
1156
|
+
}
|
|
1157
|
+
isRunning() {
|
|
1158
|
+
return this.status === "pending";
|
|
1159
|
+
}
|
|
1160
|
+
async checkAndEscalate(args, _context) {
|
|
1161
|
+
const rawAt = args?.at;
|
|
1162
|
+
const at = rawAt instanceof Date ? rawAt : typeof rawAt === "string" && !Number.isNaN(new Date(rawAt).getTime()) ? new Date(rawAt) : /* @__PURE__ */ new Date();
|
|
1163
|
+
if (!this.id) return { outcome: "missing" };
|
|
1164
|
+
const fresh = await (await SupportServiceTargetCollection.create({ db: this.db })).get({ id: this.id });
|
|
1165
|
+
if (!fresh) return { outcome: "missing" };
|
|
1166
|
+
if (fresh.status === "pending") {
|
|
1167
|
+
if (fresh.dueAt.getTime() > at.getTime()) return { outcome: "rescheduled" };
|
|
1168
|
+
fresh.breachedAt = at;
|
|
1169
|
+
fresh.status = "breached";
|
|
1170
|
+
await fresh.save();
|
|
1171
|
+
const { ServiceTargetEngine } = await Promise.resolve().then(() => service_target_engine_exports);
|
|
1172
|
+
await (await ServiceTargetEngine.create({ db: this.db })).escalateForBreach(fresh, { at });
|
|
1173
|
+
return { outcome: "breached" };
|
|
1174
|
+
}
|
|
1175
|
+
if (fresh.status === "breached") {
|
|
1176
|
+
const { ServiceTargetEngine } = await Promise.resolve().then(() => service_target_engine_exports);
|
|
1177
|
+
return { outcome: await (await ServiceTargetEngine.create({ db: this.db })).continueEscalation(fresh, at) ? "escalated" : "noop" };
|
|
1178
|
+
}
|
|
1179
|
+
return { outcome: "noop" };
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
__decorateClass$3([tenantId({ nullable: true })], SupportServiceTarget.prototype, "tenantId", 2);
|
|
1183
|
+
__decorateClass$3([foreignKey("SupportCase", { required: true })], SupportServiceTarget.prototype, "caseId", 2);
|
|
1184
|
+
__decorateClass$3([field({ type: "text" })], SupportServiceTarget.prototype, "targetType", 2);
|
|
1185
|
+
__decorateClass$3([field({ type: "text" })], SupportServiceTarget.prototype, "severity", 2);
|
|
1186
|
+
__decorateClass$3([field({ type: "text" })], SupportServiceTarget.prototype, "status", 2);
|
|
1187
|
+
__decorateClass$3([field({ type: "text" })], SupportServiceTarget.prototype, "escalationJobId", 2);
|
|
1188
|
+
__decorateClass$3([field({ type: "text" })], SupportServiceTarget.prototype, "metadata", 2);
|
|
1189
|
+
__decorateClass$3([backgroundEligible()], SupportServiceTarget.prototype, "checkAndEscalate", 1);
|
|
1190
|
+
SupportServiceTarget = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
|
|
1191
|
+
tableName: "support_service_targets",
|
|
1192
|
+
api: { include: ["list", "get"] },
|
|
1193
|
+
mcp: { include: ["list", "get"] },
|
|
1194
|
+
cli: { include: ["list", "get"] },
|
|
1195
|
+
conflictColumns: [
|
|
1196
|
+
"case_id",
|
|
1197
|
+
"target_type",
|
|
1198
|
+
"cycle"
|
|
1199
|
+
]
|
|
1200
|
+
})], SupportServiceTarget);
|
|
1201
|
+
var SupportServiceTargetCollection = class extends SmrtCollection {
|
|
1202
|
+
static _itemClass = SupportServiceTarget;
|
|
1203
|
+
async forCase(caseId) {
|
|
1204
|
+
return this.list({
|
|
1205
|
+
where: { caseId },
|
|
1206
|
+
orderBy: "created_at ASC"
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
/** Active clock of a type on a case (highest cycle wins). */
|
|
1210
|
+
async activeTarget(caseId, targetType) {
|
|
1211
|
+
return (await this.list({
|
|
1212
|
+
where: {
|
|
1213
|
+
caseId,
|
|
1214
|
+
targetType,
|
|
1215
|
+
"status in": ["pending", "paused"]
|
|
1216
|
+
},
|
|
1217
|
+
orderBy: "cycle DESC",
|
|
1218
|
+
limit: 1
|
|
1219
|
+
}))[0] ?? null;
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
var SupportEscalation = class extends SmrtObject {
|
|
1223
|
+
tenantId = null;
|
|
1224
|
+
caseId = "";
|
|
1225
|
+
/** Escalation level applied (1-based, follows the plan's policy steps). */
|
|
1226
|
+
level = 1;
|
|
1227
|
+
reason = "target_breach";
|
|
1228
|
+
targetId = null;
|
|
1229
|
+
targetType = "";
|
|
1230
|
+
action = "notify";
|
|
1231
|
+
fromSpecialistId = null;
|
|
1232
|
+
toSpecialistId = null;
|
|
1233
|
+
notifiedProfileIds = "[]";
|
|
1234
|
+
occurredAt = /* @__PURE__ */ new Date();
|
|
1235
|
+
note = "";
|
|
1236
|
+
getNotifiedProfileIds() {
|
|
1237
|
+
return parseStringArrayField(this.notifiedProfileIds);
|
|
1238
|
+
}
|
|
1239
|
+
setNotifiedProfileIds(ids) {
|
|
1240
|
+
this.notifiedProfileIds = JSON.stringify(ids ?? []);
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
__decorateClass$3([tenantId({ nullable: true })], SupportEscalation.prototype, "tenantId", 2);
|
|
1244
|
+
__decorateClass$3([foreignKey("SupportCase", { required: true })], SupportEscalation.prototype, "caseId", 2);
|
|
1245
|
+
__decorateClass$3([field({ type: "text" })], SupportEscalation.prototype, "reason", 2);
|
|
1246
|
+
__decorateClass$3([foreignKey("SupportServiceTarget")], SupportEscalation.prototype, "targetId", 2);
|
|
1247
|
+
__decorateClass$3([field({ type: "text" })], SupportEscalation.prototype, "targetType", 2);
|
|
1248
|
+
__decorateClass$3([field({ type: "text" })], SupportEscalation.prototype, "action", 2);
|
|
1249
|
+
__decorateClass$3([foreignKey("SupportSpecialist")], SupportEscalation.prototype, "fromSpecialistId", 2);
|
|
1250
|
+
__decorateClass$3([foreignKey("SupportSpecialist")], SupportEscalation.prototype, "toSpecialistId", 2);
|
|
1251
|
+
__decorateClass$3([field({ type: "text" })], SupportEscalation.prototype, "notifiedProfileIds", 2);
|
|
1252
|
+
__decorateClass$3([field({ type: "text" })], SupportEscalation.prototype, "note", 2);
|
|
1253
|
+
SupportEscalation = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
|
|
1254
|
+
tableName: "support_escalations",
|
|
1255
|
+
api: { include: ["list", "get"] },
|
|
1256
|
+
mcp: { include: ["list", "get"] },
|
|
1257
|
+
cli: { include: ["list", "get"] }
|
|
1258
|
+
})], SupportEscalation);
|
|
1259
|
+
var SupportEscalationCollection = class extends SmrtCollection {
|
|
1260
|
+
static _itemClass = SupportEscalation;
|
|
1261
|
+
async forCase(caseId) {
|
|
1262
|
+
return this.list({
|
|
1263
|
+
where: { caseId },
|
|
1264
|
+
orderBy: "occurred_at ASC"
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
//#endregion
|
|
1269
|
+
//#region src/models/support-settlement.ts
|
|
1270
|
+
var __defProp$2 = Object.defineProperty;
|
|
1271
|
+
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
1272
|
+
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
1273
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
1274
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
1275
|
+
if (kind && result) __defProp$2(target, key, result);
|
|
1276
|
+
return result;
|
|
1277
|
+
};
|
|
1278
|
+
var SupportCharge = class extends SmrtObject {
|
|
1279
|
+
tenantId = null;
|
|
1280
|
+
timeEntryId = "";
|
|
1281
|
+
caseId = null;
|
|
1282
|
+
planId = null;
|
|
1283
|
+
/** Client charge amount for the billable time. */
|
|
1284
|
+
amount = 0;
|
|
1285
|
+
currency = "USD";
|
|
1286
|
+
/** Seconds priced at the metered rate (after included-time consumption). */
|
|
1287
|
+
billableSeconds = 0;
|
|
1288
|
+
/** Seconds absorbed by the plan's included support time. */
|
|
1289
|
+
includedSecondsApplied = 0;
|
|
1290
|
+
rateSnapshot = "{}";
|
|
1291
|
+
status = "pending";
|
|
1292
|
+
finalizedAt = null;
|
|
1293
|
+
metadata = "{}";
|
|
1294
|
+
getRateSnapshot() {
|
|
1295
|
+
return parseJsonField(this.rateSnapshot, {});
|
|
1296
|
+
}
|
|
1297
|
+
setRateSnapshot(value) {
|
|
1298
|
+
this.rateSnapshot = JSON.stringify(value ?? {});
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
__decorateClass$2([tenantId({ nullable: true })], SupportCharge.prototype, "tenantId", 2);
|
|
1302
|
+
__decorateClass$2([foreignKey("ServiceTimeEntry", { required: true })], SupportCharge.prototype, "timeEntryId", 2);
|
|
1303
|
+
__decorateClass$2([foreignKey("SupportCase")], SupportCharge.prototype, "caseId", 2);
|
|
1304
|
+
__decorateClass$2([foreignKey("SupportPlan")], SupportCharge.prototype, "planId", 2);
|
|
1305
|
+
__decorateClass$2([field({ type: "text" })], SupportCharge.prototype, "currency", 2);
|
|
1306
|
+
__decorateClass$2([field({ type: "text" })], SupportCharge.prototype, "rateSnapshot", 2);
|
|
1307
|
+
__decorateClass$2([field({ type: "text" })], SupportCharge.prototype, "status", 2);
|
|
1308
|
+
__decorateClass$2([field({ type: "text" })], SupportCharge.prototype, "metadata", 2);
|
|
1309
|
+
SupportCharge = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
|
|
1310
|
+
tableName: "support_charges",
|
|
1311
|
+
api: { include: ["list", "get"] },
|
|
1312
|
+
mcp: { include: ["list", "get"] },
|
|
1313
|
+
cli: { include: ["list", "get"] },
|
|
1314
|
+
conflictColumns: ["time_entry_id"]
|
|
1315
|
+
})], SupportCharge);
|
|
1316
|
+
var SupportChargeCollection = class extends SmrtCollection {
|
|
1317
|
+
static _itemClass = SupportCharge;
|
|
1318
|
+
async forTimeEntry(timeEntryId) {
|
|
1319
|
+
return (await this.list({
|
|
1320
|
+
where: { timeEntryId },
|
|
1321
|
+
limit: 1
|
|
1322
|
+
}))[0] ?? null;
|
|
1323
|
+
}
|
|
1324
|
+
async forCase(caseId) {
|
|
1325
|
+
return this.list({
|
|
1326
|
+
where: { caseId },
|
|
1327
|
+
orderBy: "created_at ASC"
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
var SupportCompensation = class extends SmrtObject {
|
|
1332
|
+
tenantId = null;
|
|
1333
|
+
timeEntryId = "";
|
|
1334
|
+
specialistId = null;
|
|
1335
|
+
compensationPlanId = null;
|
|
1336
|
+
/** Provider earning amount for the payable time. */
|
|
1337
|
+
amount = 0;
|
|
1338
|
+
currency = "USD";
|
|
1339
|
+
/** Seconds compensated. */
|
|
1340
|
+
payableSeconds = 0;
|
|
1341
|
+
rateSnapshot = "{}";
|
|
1342
|
+
status = "pending";
|
|
1343
|
+
finalizedAt = null;
|
|
1344
|
+
metadata = "{}";
|
|
1345
|
+
getRateSnapshot() {
|
|
1346
|
+
return parseJsonField(this.rateSnapshot, {});
|
|
1347
|
+
}
|
|
1348
|
+
setRateSnapshot(value) {
|
|
1349
|
+
this.rateSnapshot = JSON.stringify(value ?? {});
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
__decorateClass$2([tenantId({ nullable: true })], SupportCompensation.prototype, "tenantId", 2);
|
|
1353
|
+
__decorateClass$2([foreignKey("ServiceTimeEntry", { required: true })], SupportCompensation.prototype, "timeEntryId", 2);
|
|
1354
|
+
__decorateClass$2([foreignKey("SupportSpecialist")], SupportCompensation.prototype, "specialistId", 2);
|
|
1355
|
+
__decorateClass$2([foreignKey("SupportCompensationPlan")], SupportCompensation.prototype, "compensationPlanId", 2);
|
|
1356
|
+
__decorateClass$2([field({ type: "text" })], SupportCompensation.prototype, "currency", 2);
|
|
1357
|
+
__decorateClass$2([field({ type: "text" })], SupportCompensation.prototype, "rateSnapshot", 2);
|
|
1358
|
+
__decorateClass$2([field({ type: "text" })], SupportCompensation.prototype, "status", 2);
|
|
1359
|
+
__decorateClass$2([field({ type: "text" })], SupportCompensation.prototype, "metadata", 2);
|
|
1360
|
+
SupportCompensation = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
|
|
1361
|
+
tableName: "support_compensations",
|
|
1362
|
+
api: { include: ["list", "get"] },
|
|
1363
|
+
mcp: { include: ["list", "get"] },
|
|
1364
|
+
cli: { include: ["list", "get"] },
|
|
1365
|
+
conflictColumns: ["time_entry_id"]
|
|
1366
|
+
})], SupportCompensation);
|
|
1367
|
+
var SupportCompensationCollection = class extends SmrtCollection {
|
|
1368
|
+
static _itemClass = SupportCompensation;
|
|
1369
|
+
async forTimeEntry(timeEntryId) {
|
|
1370
|
+
return (await this.list({
|
|
1371
|
+
where: { timeEntryId },
|
|
1372
|
+
limit: 1
|
|
1373
|
+
}))[0] ?? null;
|
|
1374
|
+
}
|
|
1375
|
+
async forSpecialist(specialistId) {
|
|
1376
|
+
return this.list({
|
|
1377
|
+
where: { specialistId },
|
|
1378
|
+
orderBy: "created_at ASC"
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
//#endregion
|
|
1383
|
+
//#region src/models/support-specialist.ts
|
|
1384
|
+
var __defProp$1 = Object.defineProperty;
|
|
1385
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
1386
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
1387
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
1388
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
1389
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
1390
|
+
return result;
|
|
1391
|
+
};
|
|
1392
|
+
var SupportSpecialist = class extends SmrtObject {
|
|
1393
|
+
tenantId = null;
|
|
1394
|
+
profileId = "";
|
|
1395
|
+
displayName = "";
|
|
1396
|
+
status = "active";
|
|
1397
|
+
languages = "[\"en\"]";
|
|
1398
|
+
timezone = "UTC";
|
|
1399
|
+
/** Routing workload cap: open cases beyond this exclude the specialist. */
|
|
1400
|
+
maxConcurrentCases = 10;
|
|
1401
|
+
/** Tie-break priority for on-call ranking (higher = preferred). */
|
|
1402
|
+
onCallPriority = 0;
|
|
1403
|
+
notes = "";
|
|
1404
|
+
metadata = "{}";
|
|
1405
|
+
getLanguages() {
|
|
1406
|
+
return parseStringArrayField(this.languages);
|
|
1407
|
+
}
|
|
1408
|
+
setLanguages(languages) {
|
|
1409
|
+
this.languages = JSON.stringify(languages ?? []);
|
|
1410
|
+
}
|
|
1411
|
+
getMetadata() {
|
|
1412
|
+
return parseJsonField(this.metadata, {});
|
|
1413
|
+
}
|
|
1414
|
+
setMetadata(value) {
|
|
1415
|
+
this.metadata = JSON.stringify(value ?? {});
|
|
1416
|
+
}
|
|
1417
|
+
isActive() {
|
|
1418
|
+
return this.status === "active";
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
__decorateClass$1([tenantId({ nullable: true })], SupportSpecialist.prototype, "tenantId", 2);
|
|
1422
|
+
__decorateClass$1([crossPackageRef("@happyvertical/smrt-profiles:Profile", { required: true })], SupportSpecialist.prototype, "profileId", 2);
|
|
1423
|
+
__decorateClass$1([field({ type: "text" })], SupportSpecialist.prototype, "displayName", 2);
|
|
1424
|
+
__decorateClass$1([field({ type: "text" })], SupportSpecialist.prototype, "status", 2);
|
|
1425
|
+
__decorateClass$1([field({ type: "text" })], SupportSpecialist.prototype, "languages", 2);
|
|
1426
|
+
__decorateClass$1([field({ type: "text" })], SupportSpecialist.prototype, "timezone", 2);
|
|
1427
|
+
__decorateClass$1([field({ type: "text" })], SupportSpecialist.prototype, "notes", 2);
|
|
1428
|
+
__decorateClass$1([field({ type: "text" })], SupportSpecialist.prototype, "metadata", 2);
|
|
1429
|
+
SupportSpecialist = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
|
|
1430
|
+
tableName: "support_specialists",
|
|
1431
|
+
api: { include: [
|
|
1432
|
+
"list",
|
|
1433
|
+
"get",
|
|
1434
|
+
"create",
|
|
1435
|
+
"update",
|
|
1436
|
+
"delete"
|
|
1437
|
+
] },
|
|
1438
|
+
mcp: { include: ["list", "get"] },
|
|
1439
|
+
cli: { include: ["list", "get"] },
|
|
1440
|
+
conflictColumns: ["tenant_id", "profile_id"]
|
|
1441
|
+
})], SupportSpecialist);
|
|
1442
|
+
var SupportSpecialistCollection = class extends SmrtCollection {
|
|
1443
|
+
static _itemClass = SupportSpecialist;
|
|
1444
|
+
async findActive() {
|
|
1445
|
+
return this.list({ where: { status: "active" } });
|
|
1446
|
+
}
|
|
1447
|
+
async byProfile(profileId) {
|
|
1448
|
+
return (await this.list({
|
|
1449
|
+
where: { profileId },
|
|
1450
|
+
limit: 1
|
|
1451
|
+
}))[0] ?? null;
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
var SupportQualification = class extends SmrtObject {
|
|
1455
|
+
tenantId = null;
|
|
1456
|
+
specialistId = "";
|
|
1457
|
+
projectId = "";
|
|
1458
|
+
level = "qualified";
|
|
1459
|
+
/** Effective-dating: null bounds mean open-ended. */
|
|
1460
|
+
effectiveFrom = null;
|
|
1461
|
+
effectiveTo = null;
|
|
1462
|
+
grantedByProfileId = null;
|
|
1463
|
+
notes = "";
|
|
1464
|
+
/** Whether the qualification is effective at the given instant. */
|
|
1465
|
+
isEffectiveAt(at = /* @__PURE__ */ new Date()) {
|
|
1466
|
+
if (this.effectiveFrom && at < this.effectiveFrom) return false;
|
|
1467
|
+
if (this.effectiveTo && at >= this.effectiveTo) return false;
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
__decorateClass$1([tenantId({ nullable: true })], SupportQualification.prototype, "tenantId", 2);
|
|
1472
|
+
__decorateClass$1([foreignKey("SupportSpecialist", { required: true })], SupportQualification.prototype, "specialistId", 2);
|
|
1473
|
+
__decorateClass$1([field({ type: "text" })], SupportQualification.prototype, "projectId", 2);
|
|
1474
|
+
__decorateClass$1([field({ type: "text" })], SupportQualification.prototype, "level", 2);
|
|
1475
|
+
__decorateClass$1([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], SupportQualification.prototype, "grantedByProfileId", 2);
|
|
1476
|
+
__decorateClass$1([field({ type: "text" })], SupportQualification.prototype, "notes", 2);
|
|
1477
|
+
SupportQualification = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
|
|
1478
|
+
tableName: "support_qualifications",
|
|
1479
|
+
api: { include: [
|
|
1480
|
+
"list",
|
|
1481
|
+
"get",
|
|
1482
|
+
"create",
|
|
1483
|
+
"update",
|
|
1484
|
+
"delete"
|
|
1485
|
+
] },
|
|
1486
|
+
mcp: { include: ["list", "get"] },
|
|
1487
|
+
cli: { include: ["list", "get"] },
|
|
1488
|
+
conflictColumns: ["specialist_id", "project_id"]
|
|
1489
|
+
})], SupportQualification);
|
|
1490
|
+
var SupportQualificationCollection = class extends SmrtCollection {
|
|
1491
|
+
static _itemClass = SupportQualification;
|
|
1492
|
+
async forSpecialist(specialistId) {
|
|
1493
|
+
return this.list({ where: { specialistId } });
|
|
1494
|
+
}
|
|
1495
|
+
async forProject(projectId) {
|
|
1496
|
+
return this.list({ where: { projectId } });
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
var SupportAvailability = class extends SmrtObject {
|
|
1500
|
+
tenantId = null;
|
|
1501
|
+
specialistId = "";
|
|
1502
|
+
kind = "weekly";
|
|
1503
|
+
weekday = null;
|
|
1504
|
+
/** Weekly windows: minutes from midnight, inclusive start. */
|
|
1505
|
+
startMinute = 0;
|
|
1506
|
+
/** Weekly windows: minutes from midnight, exclusive end. */
|
|
1507
|
+
endMinute = 0;
|
|
1508
|
+
/** Span kinds (`on_call`, `time_off`): absolute start. */
|
|
1509
|
+
startsAt = null;
|
|
1510
|
+
/** Span kinds (`on_call`, `time_off`): absolute end. */
|
|
1511
|
+
endsAt = null;
|
|
1512
|
+
notes = "";
|
|
1513
|
+
};
|
|
1514
|
+
__decorateClass$1([tenantId({ nullable: true })], SupportAvailability.prototype, "tenantId", 2);
|
|
1515
|
+
__decorateClass$1([foreignKey("SupportSpecialist", { required: true })], SupportAvailability.prototype, "specialistId", 2);
|
|
1516
|
+
__decorateClass$1([field({ type: "text" })], SupportAvailability.prototype, "kind", 2);
|
|
1517
|
+
__decorateClass$1([field({
|
|
1518
|
+
type: "integer",
|
|
1519
|
+
nullable: true
|
|
1520
|
+
})], SupportAvailability.prototype, "weekday", 2);
|
|
1521
|
+
__decorateClass$1([field({ type: "text" })], SupportAvailability.prototype, "notes", 2);
|
|
1522
|
+
SupportAvailability = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
|
|
1523
|
+
tableName: "support_availability_windows",
|
|
1524
|
+
api: { include: [
|
|
1525
|
+
"list",
|
|
1526
|
+
"get",
|
|
1527
|
+
"create",
|
|
1528
|
+
"update",
|
|
1529
|
+
"delete"
|
|
1530
|
+
] },
|
|
1531
|
+
mcp: { include: ["list", "get"] },
|
|
1532
|
+
cli: { include: ["list", "get"] }
|
|
1533
|
+
})], SupportAvailability);
|
|
1534
|
+
var SupportAvailabilityCollection = class extends SmrtCollection {
|
|
1535
|
+
static _itemClass = SupportAvailability;
|
|
1536
|
+
async forSpecialist(specialistId) {
|
|
1537
|
+
return this.list({ where: { specialistId } });
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
//#endregion
|
|
1541
|
+
//#region src/models/support-work-link.ts
|
|
1542
|
+
var __defProp = Object.defineProperty;
|
|
1543
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
1544
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
1545
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
1546
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
1547
|
+
if (kind && result) __defProp(target, key, result);
|
|
1548
|
+
return result;
|
|
1549
|
+
};
|
|
1550
|
+
var SupportWorkLink = class extends SmrtObject {
|
|
1551
|
+
tenantId = null;
|
|
1552
|
+
caseId = "";
|
|
1553
|
+
linkKind = "support_work_item";
|
|
1554
|
+
targetType = "";
|
|
1555
|
+
targetId = "";
|
|
1556
|
+
targetLabel = "";
|
|
1557
|
+
externalUrl = "";
|
|
1558
|
+
status = "open";
|
|
1559
|
+
lastStatusAt = null;
|
|
1560
|
+
metadata = "{}";
|
|
1561
|
+
getMetadata() {
|
|
1562
|
+
return parseJsonField(this.metadata, {});
|
|
1563
|
+
}
|
|
1564
|
+
setMetadata(value) {
|
|
1565
|
+
this.metadata = JSON.stringify(value ?? {});
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
__decorateClass([tenantId({ nullable: true })], SupportWorkLink.prototype, "tenantId", 2);
|
|
1569
|
+
__decorateClass([foreignKey("SupportCase", { required: true })], SupportWorkLink.prototype, "caseId", 2);
|
|
1570
|
+
__decorateClass([field({ type: "text" })], SupportWorkLink.prototype, "linkKind", 2);
|
|
1571
|
+
__decorateClass([field({ type: "text" })], SupportWorkLink.prototype, "targetType", 2);
|
|
1572
|
+
__decorateClass([field({ type: "text" })], SupportWorkLink.prototype, "targetId", 2);
|
|
1573
|
+
__decorateClass([field({ type: "text" })], SupportWorkLink.prototype, "targetLabel", 2);
|
|
1574
|
+
__decorateClass([field({ type: "text" })], SupportWorkLink.prototype, "externalUrl", 2);
|
|
1575
|
+
__decorateClass([field({ type: "text" })], SupportWorkLink.prototype, "status", 2);
|
|
1576
|
+
__decorateClass([field({ type: "text" })], SupportWorkLink.prototype, "metadata", 2);
|
|
1577
|
+
SupportWorkLink = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
|
|
1578
|
+
tableName: "support_work_links",
|
|
1579
|
+
api: { include: ["list", "get"] },
|
|
1580
|
+
mcp: { include: ["list", "get"] },
|
|
1581
|
+
cli: { include: ["list", "get"] },
|
|
1582
|
+
conflictColumns: [
|
|
1583
|
+
"case_id",
|
|
1584
|
+
"target_type",
|
|
1585
|
+
"target_id"
|
|
1586
|
+
]
|
|
1587
|
+
})], SupportWorkLink);
|
|
1588
|
+
var SupportWorkLinkCollection = class extends SmrtCollection {
|
|
1589
|
+
static _itemClass = SupportWorkLink;
|
|
1590
|
+
async forCase(caseId) {
|
|
1591
|
+
return this.list({
|
|
1592
|
+
where: { caseId },
|
|
1593
|
+
orderBy: "created_at ASC"
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
//#endregion
|
|
1598
|
+
//#region src/services/coverage-calendar.ts
|
|
1599
|
+
var MAX_COVERAGE_SCAN_DAYS = 400;
|
|
1600
|
+
var MINUTE_MS$1 = 6e4;
|
|
1601
|
+
var DAY_MS = 864e5;
|
|
1602
|
+
var MINUTES_PER_DAY = 1440;
|
|
1603
|
+
var WEEKDAY_INDEX = {
|
|
1604
|
+
Sun: 0,
|
|
1605
|
+
Mon: 1,
|
|
1606
|
+
Tue: 2,
|
|
1607
|
+
Wed: 3,
|
|
1608
|
+
Thu: 4,
|
|
1609
|
+
Fri: 5,
|
|
1610
|
+
Sat: 6
|
|
1611
|
+
};
|
|
1612
|
+
var formatterCache = /* @__PURE__ */ new Map();
|
|
1613
|
+
function getFormatter(timeZone) {
|
|
1614
|
+
const cached = formatterCache.get(timeZone);
|
|
1615
|
+
if (cached) return cached;
|
|
1616
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
1617
|
+
timeZone,
|
|
1618
|
+
hourCycle: "h23",
|
|
1619
|
+
weekday: "short",
|
|
1620
|
+
year: "numeric",
|
|
1621
|
+
month: "2-digit",
|
|
1622
|
+
day: "2-digit",
|
|
1623
|
+
hour: "2-digit",
|
|
1624
|
+
minute: "2-digit"
|
|
1625
|
+
});
|
|
1626
|
+
formatterCache.set(timeZone, formatter);
|
|
1627
|
+
return formatter;
|
|
1628
|
+
}
|
|
1629
|
+
function zonedParts(date, timeZone) {
|
|
1630
|
+
const parts = getFormatter(timeZone).formatToParts(date);
|
|
1631
|
+
const byType = {};
|
|
1632
|
+
for (const part of parts) byType[part.type] = part.value;
|
|
1633
|
+
const year = Number(byType.year);
|
|
1634
|
+
const month = Number(byType.month);
|
|
1635
|
+
const day = Number(byType.day);
|
|
1636
|
+
const hour = Number(byType.hour) % 24;
|
|
1637
|
+
const minute = Number(byType.minute);
|
|
1638
|
+
return {
|
|
1639
|
+
year,
|
|
1640
|
+
month,
|
|
1641
|
+
day,
|
|
1642
|
+
weekday: WEEKDAY_INDEX[byType.weekday ?? ""] ?? 0,
|
|
1643
|
+
minuteOfDay: hour * 60 + minute,
|
|
1644
|
+
dateKey: `${byType.year}-${byType.month}-${byType.day}`
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
function wallClockMs(parts) {
|
|
1648
|
+
return Date.UTC(parts.year, parts.month - 1, parts.day) + parts.minuteOfDay * MINUTE_MS$1;
|
|
1649
|
+
}
|
|
1650
|
+
function wallTimeToUtcMs(year, month, day, minuteOfDay, timeZone) {
|
|
1651
|
+
const targetWallMs = Date.UTC(year, month - 1, day) + minuteOfDay * MINUTE_MS$1;
|
|
1652
|
+
let guessMs = targetWallMs;
|
|
1653
|
+
for (let i = 0; i < 4; i++) {
|
|
1654
|
+
const wallMs = wallClockMs(zonedParts(new Date(guessMs), timeZone));
|
|
1655
|
+
if (wallMs === targetWallMs) return guessMs;
|
|
1656
|
+
guessMs += targetWallMs - wallMs;
|
|
1657
|
+
}
|
|
1658
|
+
const finalWallMs = wallClockMs(zonedParts(new Date(guessMs), timeZone));
|
|
1659
|
+
if (finalWallMs < targetWallMs) guessMs += targetWallMs - finalWallMs;
|
|
1660
|
+
return guessMs;
|
|
1661
|
+
}
|
|
1662
|
+
function normalizeWindowMinutes(windows) {
|
|
1663
|
+
const clamped = windows.map((window) => ({
|
|
1664
|
+
startMinute: Math.max(0, Math.min(window.startMinute, MINUTES_PER_DAY)),
|
|
1665
|
+
endMinute: Math.max(0, Math.min(window.endMinute, MINUTES_PER_DAY))
|
|
1666
|
+
})).filter((window) => window.endMinute > window.startMinute).sort((a, b) => a.startMinute - b.startMinute);
|
|
1667
|
+
const merged = [];
|
|
1668
|
+
for (const window of clamped) {
|
|
1669
|
+
const last = merged[merged.length - 1];
|
|
1670
|
+
if (last && window.startMinute <= last.endMinute) last.endMinute = Math.max(last.endMinute, window.endMinute);
|
|
1671
|
+
else merged.push({ ...window });
|
|
1672
|
+
}
|
|
1673
|
+
return merged;
|
|
1674
|
+
}
|
|
1675
|
+
function coveredSpansForDay(calendar, parts) {
|
|
1676
|
+
if (calendar.holidays.includes(parts.dateKey)) return [];
|
|
1677
|
+
const weekday = new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay();
|
|
1678
|
+
return normalizeWindowMinutes(calendar.windows.length === 0 ? [{
|
|
1679
|
+
startMinute: 0,
|
|
1680
|
+
endMinute: MINUTES_PER_DAY
|
|
1681
|
+
}] : calendar.windows.filter((window) => window.weekday === weekday)).map((window) => ({
|
|
1682
|
+
startMs: wallTimeToUtcMs(parts.year, parts.month, parts.day, window.startMinute, calendar.timezone),
|
|
1683
|
+
endMs: wallTimeToUtcMs(parts.year, parts.month, parts.day, window.endMinute, calendar.timezone)
|
|
1684
|
+
}));
|
|
1685
|
+
}
|
|
1686
|
+
function nextZoneMidnightMs(calendar, parts) {
|
|
1687
|
+
const nextDate = new Date(Date.UTC(parts.year, parts.month - 1, parts.day) + DAY_MS);
|
|
1688
|
+
return wallTimeToUtcMs(nextDate.getUTCFullYear(), nextDate.getUTCMonth() + 1, nextDate.getUTCDate(), 0, calendar.timezone);
|
|
1689
|
+
}
|
|
1690
|
+
function isAlwaysCovered(calendar) {
|
|
1691
|
+
return calendar.windows.length === 0 && calendar.holidays.length === 0;
|
|
1692
|
+
}
|
|
1693
|
+
function coveredMinutesBetween(calendar, from, to) {
|
|
1694
|
+
const fromMs = from.getTime();
|
|
1695
|
+
const toMs = to.getTime();
|
|
1696
|
+
if (toMs <= fromMs) return 0;
|
|
1697
|
+
if (isAlwaysCovered(calendar)) return (toMs - fromMs) / MINUTE_MS$1;
|
|
1698
|
+
let totalMs = 0;
|
|
1699
|
+
let cursorMs = fromMs;
|
|
1700
|
+
while (cursorMs < toMs) {
|
|
1701
|
+
const parts = zonedParts(new Date(cursorMs), calendar.timezone);
|
|
1702
|
+
for (const span of coveredSpansForDay(calendar, parts)) {
|
|
1703
|
+
const start = Math.max(span.startMs, cursorMs);
|
|
1704
|
+
const end = Math.min(span.endMs, toMs);
|
|
1705
|
+
if (end > start) totalMs += end - start;
|
|
1706
|
+
}
|
|
1707
|
+
const nextMidnightMs = nextZoneMidnightMs(calendar, parts);
|
|
1708
|
+
if (nextMidnightMs <= cursorMs) throw new Error(`coveredMinutesBetween: calendar for timezone '${calendar.timezone}' failed to advance past ` + new Date(cursorMs).toISOString());
|
|
1709
|
+
cursorMs = nextMidnightMs;
|
|
1710
|
+
}
|
|
1711
|
+
return totalMs / MINUTE_MS$1;
|
|
1712
|
+
}
|
|
1713
|
+
function addCoveredMinutes(calendar, from, minutes) {
|
|
1714
|
+
if (minutes <= 0) return new Date(from.getTime());
|
|
1715
|
+
if (isAlwaysCovered(calendar)) return new Date(from.getTime() + minutes * MINUTE_MS$1);
|
|
1716
|
+
let remainingMs = minutes * MINUTE_MS$1;
|
|
1717
|
+
let cursorMs = from.getTime();
|
|
1718
|
+
for (let dayScan = 0; dayScan <= 400; dayScan++) {
|
|
1719
|
+
const parts = zonedParts(new Date(cursorMs), calendar.timezone);
|
|
1720
|
+
for (const span of coveredSpansForDay(calendar, parts)) {
|
|
1721
|
+
const start = Math.max(span.startMs, cursorMs);
|
|
1722
|
+
if (span.endMs <= start) continue;
|
|
1723
|
+
const spanMs = span.endMs - start;
|
|
1724
|
+
if (spanMs >= remainingMs) return new Date(start + remainingMs);
|
|
1725
|
+
remainingMs -= spanMs;
|
|
1726
|
+
}
|
|
1727
|
+
const nextMidnightMs = nextZoneMidnightMs(calendar, parts);
|
|
1728
|
+
if (nextMidnightMs <= cursorMs) throw new Error(`addCoveredMinutes: calendar for timezone '${calendar.timezone}' failed to advance past ` + new Date(cursorMs).toISOString());
|
|
1729
|
+
cursorMs = nextMidnightMs;
|
|
1730
|
+
}
|
|
1731
|
+
throw new Error(`addCoveredMinutes: no coverage found within 400 days after ${from.toISOString()} for ${minutes} covered minute(s) \u2014 the coverage calendar appears to never cover any time (windows: ${JSON.stringify(calendar.windows)}, holidays: ${calendar.holidays.length}, timezone: '${calendar.timezone}').`);
|
|
1732
|
+
}
|
|
1733
|
+
//#endregion
|
|
1734
|
+
//#region src/services/support-case-service.ts
|
|
1735
|
+
var SupportCaseService = class SupportCaseService {
|
|
1736
|
+
cases;
|
|
1737
|
+
interactions;
|
|
1738
|
+
events;
|
|
1739
|
+
workLinks;
|
|
1740
|
+
plans;
|
|
1741
|
+
constructor(collections) {
|
|
1742
|
+
this.cases = collections.cases;
|
|
1743
|
+
this.interactions = collections.interactions;
|
|
1744
|
+
this.events = collections.events;
|
|
1745
|
+
this.workLinks = collections.workLinks;
|
|
1746
|
+
this.plans = collections.plans;
|
|
1747
|
+
}
|
|
1748
|
+
static async create(options) {
|
|
1749
|
+
const [cases, interactions, events, workLinks, plans] = await Promise.all([
|
|
1750
|
+
SupportCaseCollection.create(options),
|
|
1751
|
+
SupportInteractionCollection.create(options),
|
|
1752
|
+
SupportCaseEventCollection.create(options),
|
|
1753
|
+
SupportWorkLinkCollection.create(options),
|
|
1754
|
+
SupportPlanCollection.create(options)
|
|
1755
|
+
]);
|
|
1756
|
+
return new SupportCaseService({
|
|
1757
|
+
cases,
|
|
1758
|
+
interactions,
|
|
1759
|
+
events,
|
|
1760
|
+
workLinks,
|
|
1761
|
+
plans
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
/** Load a case or throw a descriptive error. */
|
|
1765
|
+
async getCase(caseId) {
|
|
1766
|
+
const found = await this.cases.get({ id: caseId });
|
|
1767
|
+
if (!found) throw new Error(`SupportCase not found: ${caseId}`);
|
|
1768
|
+
return found;
|
|
1769
|
+
}
|
|
1770
|
+
/** The persisted id of a saved case (all facade entry points save first). */
|
|
1771
|
+
caseIdOf(supportCase) {
|
|
1772
|
+
if (!supportCase.id) throw new Error("SupportCase has no id — was it saved?");
|
|
1773
|
+
return supportCase.id;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Open a new Support Case. Applies the plan snapshot when a plan is given
|
|
1777
|
+
* and writes the `created` audit event.
|
|
1778
|
+
*/
|
|
1779
|
+
async openCase(input) {
|
|
1780
|
+
let plan = null;
|
|
1781
|
+
if (input.planId) {
|
|
1782
|
+
plan = await this.plans.get({ id: input.planId });
|
|
1783
|
+
if (!plan) throw new Error(`SupportPlan not found: ${input.planId}`);
|
|
1784
|
+
if (plan.tenantId && plan.tenantId !== (input.tenantId ?? null)) throw new Error(`SupportPlan '${plan.planKey || plan.id}' belongs to tenant '${plan.tenantId}' and cannot govern a case in tenant '${input.tenantId ?? null}'.`);
|
|
1785
|
+
}
|
|
1786
|
+
const supportCase = await this.cases.create({
|
|
1787
|
+
tenantId: input.tenantId ?? null,
|
|
1788
|
+
caseNumber: generateCaseNumber(),
|
|
1789
|
+
subject: input.subject,
|
|
1790
|
+
description: input.description ?? "",
|
|
1791
|
+
status: "new",
|
|
1792
|
+
priority: input.priority ?? "normal",
|
|
1793
|
+
severity: input.severity ?? "",
|
|
1794
|
+
channelKind: input.channelKind ?? "",
|
|
1795
|
+
clientProfileId: input.clientProfileId ?? null,
|
|
1796
|
+
openedByProfileId: input.openedByProfileId ?? null,
|
|
1797
|
+
projectId: input.projectId ?? null,
|
|
1798
|
+
bindingId: input.bindingId ?? null,
|
|
1799
|
+
threadKey: input.threadKey ?? "",
|
|
1800
|
+
planId: null,
|
|
1801
|
+
preferredSpecialistId: input.preferredSpecialistId ?? null,
|
|
1802
|
+
metadata: JSON.stringify(input.metadata ?? {})
|
|
1803
|
+
});
|
|
1804
|
+
if (plan) await this.applyPlan(supportCase, plan);
|
|
1805
|
+
await this.recordEvent(supportCase, "created", {
|
|
1806
|
+
actorKind: "system",
|
|
1807
|
+
summary: `Case ${supportCase.caseNumber} opened`,
|
|
1808
|
+
payload: {
|
|
1809
|
+
channelKind: supportCase.channelKind,
|
|
1810
|
+
clientProfileId: supportCase.clientProfileId,
|
|
1811
|
+
projectId: supportCase.projectId
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
return supportCase;
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Capture the plan terms onto the case (`planSnapshot`) so later plan edits
|
|
1818
|
+
* never rewrite what this case was handled under. A tenant-owned plan only
|
|
1819
|
+
* applies to that tenant's cases (global NULL-tenant template plans apply
|
|
1820
|
+
* anywhere) — a foreign plan would snapshot another tenant's pricing,
|
|
1821
|
+
* approval, and escalation terms.
|
|
1822
|
+
*/
|
|
1823
|
+
async applyPlan(supportCase, plan) {
|
|
1824
|
+
if (plan.tenantId && plan.tenantId !== supportCase.tenantId) throw new Error(`SupportPlan '${plan.planKey || plan.id}' belongs to tenant '${plan.tenantId}' and cannot govern a case in tenant '${supportCase.tenantId}'.`);
|
|
1825
|
+
supportCase.planId = plan.id ?? null;
|
|
1826
|
+
supportCase.setPlanSnapshot(plan.snapshotTerms());
|
|
1827
|
+
await supportCase.save();
|
|
1828
|
+
await this.recordEvent(supportCase, "plan_applied", {
|
|
1829
|
+
actorKind: "system",
|
|
1830
|
+
summary: `Plan '${plan.name || plan.planKey}' applied`,
|
|
1831
|
+
payload: {
|
|
1832
|
+
planId: plan.id,
|
|
1833
|
+
planKey: plan.planKey
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Record a channel interaction on a case (idempotent on `sourceKey`).
|
|
1839
|
+
* Inbound client activity un-parks `waiting_on_client` cases; the first
|
|
1840
|
+
* outbound reply stamps `acknowledgedAt`/`firstRespondedAt`.
|
|
1841
|
+
*/
|
|
1842
|
+
async recordInteraction(caseRef, input) {
|
|
1843
|
+
const supportCase = typeof caseRef === "string" ? await this.getCase(caseRef) : caseRef;
|
|
1844
|
+
const sourceKey = input.sourceKey ?? `manual:${crypto.randomUUID()}`;
|
|
1845
|
+
const existing = await this.interactions.bySourceKey(sourceKey);
|
|
1846
|
+
if (existing) return existing;
|
|
1847
|
+
const interaction = await this.interactions.create({
|
|
1848
|
+
tenantId: supportCase.tenantId,
|
|
1849
|
+
caseId: this.caseIdOf(supportCase),
|
|
1850
|
+
direction: input.direction,
|
|
1851
|
+
channelKind: input.channelKind,
|
|
1852
|
+
actorKind: input.actorKind,
|
|
1853
|
+
authorProfileId: input.authorProfileId ?? null,
|
|
1854
|
+
occurredAt: input.occurredAt ?? /* @__PURE__ */ new Date(),
|
|
1855
|
+
body: input.body,
|
|
1856
|
+
sourceType: input.sourceType ?? null,
|
|
1857
|
+
sourceId: input.sourceId ?? null,
|
|
1858
|
+
sourceKey,
|
|
1859
|
+
rfcMessageId: input.rfcMessageId ?? "",
|
|
1860
|
+
metadata: JSON.stringify(input.metadata ?? {})
|
|
1861
|
+
});
|
|
1862
|
+
let caseDirty = false;
|
|
1863
|
+
if (input.direction === "outbound" && input.metadata?.draft !== true) {
|
|
1864
|
+
if (!supportCase.acknowledgedAt) {
|
|
1865
|
+
supportCase.acknowledgedAt = interaction.occurredAt;
|
|
1866
|
+
caseDirty = true;
|
|
1867
|
+
}
|
|
1868
|
+
if (!supportCase.firstRespondedAt && (input.actorKind === "specialist" || input.actorKind === "agent") && input.metadata?.acknowledgement !== true) {
|
|
1869
|
+
supportCase.firstRespondedAt = interaction.occurredAt;
|
|
1870
|
+
caseDirty = true;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
if (input.direction === "inbound" && supportCase.status === "waiting_on_client") {
|
|
1874
|
+
supportCase.status = "in_progress";
|
|
1875
|
+
caseDirty = true;
|
|
1876
|
+
}
|
|
1877
|
+
if (caseDirty) await supportCase.save();
|
|
1878
|
+
await this.recordEvent(supportCase, "interaction", {
|
|
1879
|
+
actorKind: input.actorKind,
|
|
1880
|
+
actorProfileId: input.authorProfileId ?? null,
|
|
1881
|
+
summary: input.direction === "inbound" ? `Inbound ${input.channelKind} interaction` : `${input.direction} ${input.channelKind} interaction`,
|
|
1882
|
+
payload: {
|
|
1883
|
+
interactionId: interaction.id,
|
|
1884
|
+
direction: input.direction
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
return interaction;
|
|
1888
|
+
}
|
|
1889
|
+
/** Transition a case's lifecycle status (guarded by the model). */
|
|
1890
|
+
async transition(caseRef, to, actor) {
|
|
1891
|
+
const supportCase = typeof caseRef === "string" ? await this.getCase(caseRef) : caseRef;
|
|
1892
|
+
const from = supportCase.status;
|
|
1893
|
+
if (from === to) return supportCase;
|
|
1894
|
+
supportCase.status = to;
|
|
1895
|
+
await supportCase.save();
|
|
1896
|
+
await this.recordEvent(supportCase, "transition", {
|
|
1897
|
+
actorKind: actor.actorKind,
|
|
1898
|
+
actorProfileId: actor.actorProfileId ?? null,
|
|
1899
|
+
summary: `Status ${from} \u2192 ${to}`,
|
|
1900
|
+
payload: {
|
|
1901
|
+
from,
|
|
1902
|
+
to,
|
|
1903
|
+
reason: actor.reason ?? null
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
return supportCase;
|
|
1907
|
+
}
|
|
1908
|
+
/** Assign (or reassign) a case to a specialist. */
|
|
1909
|
+
async assign(caseRef, input) {
|
|
1910
|
+
const supportCase = typeof caseRef === "string" ? await this.getCase(caseRef) : caseRef;
|
|
1911
|
+
const previousSpecialistId = supportCase.assignedSpecialistId;
|
|
1912
|
+
supportCase.assignedSpecialistId = input.specialistId;
|
|
1913
|
+
supportCase.assignedAt = /* @__PURE__ */ new Date();
|
|
1914
|
+
if (supportCase.status === "new" || supportCase.status === "triaged") supportCase.status = "assigned";
|
|
1915
|
+
await supportCase.save();
|
|
1916
|
+
await this.recordEvent(supportCase, "assignment", {
|
|
1917
|
+
actorKind: input.actorKind,
|
|
1918
|
+
actorProfileId: input.actorProfileId ?? null,
|
|
1919
|
+
summary: previousSpecialistId ? "Case reassigned" : "Case assigned",
|
|
1920
|
+
payload: {
|
|
1921
|
+
specialistId: input.specialistId,
|
|
1922
|
+
previousSpecialistId,
|
|
1923
|
+
rationale: input.rationale ?? null
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
return supportCase;
|
|
1927
|
+
}
|
|
1928
|
+
/** Resolve a case with a resolution summary. */
|
|
1929
|
+
async resolve(caseRef, input) {
|
|
1930
|
+
const supportCase = typeof caseRef === "string" ? await this.getCase(caseRef) : caseRef;
|
|
1931
|
+
const from = supportCase.status;
|
|
1932
|
+
supportCase.status = "resolved";
|
|
1933
|
+
supportCase.resolvedAt = /* @__PURE__ */ new Date();
|
|
1934
|
+
supportCase.resolvedByProfileId = input.actorProfileId ?? null;
|
|
1935
|
+
supportCase.resolutionKind = input.resolutionKind ?? "human";
|
|
1936
|
+
supportCase.resolutionSummary = input.summary;
|
|
1937
|
+
await supportCase.save();
|
|
1938
|
+
await this.recordEvent(supportCase, "transition", {
|
|
1939
|
+
actorKind: input.actorKind,
|
|
1940
|
+
actorProfileId: input.actorProfileId ?? null,
|
|
1941
|
+
summary: `Status ${from} \u2192 resolved`,
|
|
1942
|
+
payload: {
|
|
1943
|
+
from,
|
|
1944
|
+
to: "resolved",
|
|
1945
|
+
resolutionKind: supportCase.resolutionKind,
|
|
1946
|
+
resolutionSummary: input.summary
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
return supportCase;
|
|
1950
|
+
}
|
|
1951
|
+
/** Close a resolved case. */
|
|
1952
|
+
async close(caseRef, actor) {
|
|
1953
|
+
const supportCase = typeof caseRef === "string" ? await this.getCase(caseRef) : caseRef;
|
|
1954
|
+
supportCase.closedAt = /* @__PURE__ */ new Date();
|
|
1955
|
+
return this.transition(supportCase, "closed", actor);
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Reopen a resolved/closed case, preserving the prior resolution in the
|
|
1959
|
+
* reopen event (FR-29b) and bumping `reopenCount`.
|
|
1960
|
+
*/
|
|
1961
|
+
async reopen(caseRef, actor) {
|
|
1962
|
+
const supportCase = typeof caseRef === "string" ? await this.getCase(caseRef) : caseRef;
|
|
1963
|
+
if (supportCase.status !== "resolved" && supportCase.status !== "closed") throw new Error(`SupportCase ${supportCase.caseNumber || supportCase.id}: only resolved or closed cases can be reopened (status is '${supportCase.status}').`);
|
|
1964
|
+
const to = actor.to ?? "triaged";
|
|
1965
|
+
if (to === "resolved" || to === "closed") throw new Error(`SupportCase: a reopen must land in an active status (got '${to}').`);
|
|
1966
|
+
const priorResolution = {
|
|
1967
|
+
resolvedAt: supportCase.resolvedAt?.toISOString() ?? null,
|
|
1968
|
+
resolvedByProfileId: supportCase.resolvedByProfileId,
|
|
1969
|
+
resolutionKind: supportCase.resolutionKind,
|
|
1970
|
+
resolutionSummary: supportCase.resolutionSummary,
|
|
1971
|
+
closedAt: supportCase.closedAt?.toISOString() ?? null,
|
|
1972
|
+
reopenIndex: supportCase.reopenCount + 1
|
|
1973
|
+
};
|
|
1974
|
+
const from = supportCase.status;
|
|
1975
|
+
supportCase.status = to;
|
|
1976
|
+
supportCase.reopenCount += 1;
|
|
1977
|
+
supportCase.lastReopenedAt = /* @__PURE__ */ new Date();
|
|
1978
|
+
supportCase.resolvedAt = null;
|
|
1979
|
+
supportCase.resolvedByProfileId = null;
|
|
1980
|
+
supportCase.resolutionKind = "";
|
|
1981
|
+
supportCase.resolutionSummary = "";
|
|
1982
|
+
supportCase.closedAt = null;
|
|
1983
|
+
await supportCase.save();
|
|
1984
|
+
await this.recordEvent(supportCase, "reopened", {
|
|
1985
|
+
actorKind: actor.actorKind,
|
|
1986
|
+
actorProfileId: actor.actorProfileId ?? null,
|
|
1987
|
+
summary: `Case reopened (from ${from})`,
|
|
1988
|
+
payload: {
|
|
1989
|
+
from,
|
|
1990
|
+
to: supportCase.status,
|
|
1991
|
+
reason: actor.reason ?? null,
|
|
1992
|
+
priorResolution
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
return supportCase;
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Record that the Client explicitly requested a human (FR-28b). The actual
|
|
1999
|
+
* handoff (routing + context transfer) is performed by the handoff service;
|
|
2000
|
+
* this stamps the request and its audit event.
|
|
2001
|
+
*/
|
|
2002
|
+
async requestHuman(caseRef, input) {
|
|
2003
|
+
const supportCase = typeof caseRef === "string" ? await this.getCase(caseRef) : caseRef;
|
|
2004
|
+
if (!supportCase.humanRequestedAt) {
|
|
2005
|
+
supportCase.humanRequestedAt = /* @__PURE__ */ new Date();
|
|
2006
|
+
await supportCase.save();
|
|
2007
|
+
}
|
|
2008
|
+
await this.recordEvent(supportCase, "human_requested", {
|
|
2009
|
+
actorKind: "client",
|
|
2010
|
+
actorProfileId: input.byProfileId ?? null,
|
|
2011
|
+
summary: "Client requested a human",
|
|
2012
|
+
payload: { note: input.note ?? null }
|
|
2013
|
+
});
|
|
2014
|
+
return supportCase;
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Link separately tracked work to the case (Support Work Item or a
|
|
2018
|
+
* Delivery Handoff's Development Work Item). Support keeps the Case as the
|
|
2019
|
+
* canonical client record; the linked item's execution belongs to its
|
|
2020
|
+
* owning domain (FR-29a).
|
|
2021
|
+
*/
|
|
2022
|
+
async linkWork(caseRef, input) {
|
|
2023
|
+
const supportCase = typeof caseRef === "string" ? await this.getCase(caseRef) : caseRef;
|
|
2024
|
+
const link = await this.workLinks.create({
|
|
2025
|
+
tenantId: supportCase.tenantId,
|
|
2026
|
+
caseId: this.caseIdOf(supportCase),
|
|
2027
|
+
linkKind: input.linkKind,
|
|
2028
|
+
targetType: input.targetType,
|
|
2029
|
+
targetId: input.targetId,
|
|
2030
|
+
targetLabel: input.targetLabel ?? "",
|
|
2031
|
+
externalUrl: input.externalUrl ?? "",
|
|
2032
|
+
metadata: JSON.stringify(input.metadata ?? {})
|
|
2033
|
+
});
|
|
2034
|
+
await this.recordEvent(supportCase, "work_linked", {
|
|
2035
|
+
actorKind: input.actorKind,
|
|
2036
|
+
actorProfileId: input.actorProfileId ?? null,
|
|
2037
|
+
summary: input.linkKind === "development_work_item" ? `Delivery Handoff \u2192 ${input.targetLabel || input.targetId}` : `Support work item linked: ${input.targetLabel || input.targetId}`,
|
|
2038
|
+
payload: {
|
|
2039
|
+
linkId: link.id,
|
|
2040
|
+
linkKind: input.linkKind,
|
|
2041
|
+
targetType: input.targetType,
|
|
2042
|
+
targetId: input.targetId
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
return link;
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Record a status echo reported back from the domain owning a linked work
|
|
2049
|
+
* item (Delivery Operations reports progress; Support never drives it).
|
|
2050
|
+
*/
|
|
2051
|
+
async recordWorkStatus(linkId, input) {
|
|
2052
|
+
const link = await this.workLinks.get({ id: linkId });
|
|
2053
|
+
if (!link) throw new Error(`SupportWorkLink not found: ${linkId}`);
|
|
2054
|
+
link.status = input.status;
|
|
2055
|
+
link.lastStatusAt = /* @__PURE__ */ new Date();
|
|
2056
|
+
await link.save();
|
|
2057
|
+
const supportCase = await this.getCase(link.caseId);
|
|
2058
|
+
await this.recordEvent(supportCase, "work_linked", {
|
|
2059
|
+
actorKind: "system",
|
|
2060
|
+
summary: `Linked work '${link.targetLabel || link.targetId}' \u2192 ${input.status}`,
|
|
2061
|
+
payload: {
|
|
2062
|
+
linkId: link.id,
|
|
2063
|
+
status: input.status,
|
|
2064
|
+
note: input.note ?? null
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
return link;
|
|
2068
|
+
}
|
|
2069
|
+
/** Append an audit event to a case. */
|
|
2070
|
+
async recordEvent(caseRef, eventType, input) {
|
|
2071
|
+
const supportCase = typeof caseRef === "string" ? await this.getCase(caseRef) : caseRef;
|
|
2072
|
+
return this.events.create({
|
|
2073
|
+
tenantId: supportCase.tenantId,
|
|
2074
|
+
caseId: this.caseIdOf(supportCase),
|
|
2075
|
+
eventType,
|
|
2076
|
+
actorKind: input.actorKind,
|
|
2077
|
+
actorProfileId: input.actorProfileId ?? null,
|
|
2078
|
+
occurredAt: input.occurredAt ?? /* @__PURE__ */ new Date(),
|
|
2079
|
+
summary: input.summary,
|
|
2080
|
+
payload: JSON.stringify(input.payload ?? {})
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* The merged case timeline: interactions and audit events in one
|
|
2085
|
+
* chronological stream (the queue/detail UI's data shape, and the context
|
|
2086
|
+
* package a Human Handoff transfers).
|
|
2087
|
+
*/
|
|
2088
|
+
async getTimeline(caseId) {
|
|
2089
|
+
const [interactions, events] = await Promise.all([this.interactions.forCase(caseId), this.events.forCase(caseId)]);
|
|
2090
|
+
return [...interactions.map((interaction) => ({
|
|
2091
|
+
kind: "interaction",
|
|
2092
|
+
occurredAt: interaction.occurredAt,
|
|
2093
|
+
interaction
|
|
2094
|
+
})), ...events.map((event) => ({
|
|
2095
|
+
kind: "event",
|
|
2096
|
+
occurredAt: event.occurredAt,
|
|
2097
|
+
event
|
|
2098
|
+
}))].sort((a, b) => a.occurredAt.getTime() - b.occurredAt.getTime());
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
//#endregion
|
|
2102
|
+
//#region src/services/human-handoff-service.ts
|
|
2103
|
+
var HumanHandoffService = class HumanHandoffService {
|
|
2104
|
+
caseService;
|
|
2105
|
+
aiRuns;
|
|
2106
|
+
assignSpecialist;
|
|
2107
|
+
constructor(deps) {
|
|
2108
|
+
this.caseService = deps.caseService;
|
|
2109
|
+
this.aiRuns = deps.aiRuns;
|
|
2110
|
+
this.assignSpecialist = deps.assignSpecialist;
|
|
2111
|
+
}
|
|
2112
|
+
static async create(options) {
|
|
2113
|
+
const [caseService, aiRuns] = await Promise.all([SupportCaseService.create(options), SupportAiRunCollection.create(options)]);
|
|
2114
|
+
return new HumanHandoffService({
|
|
2115
|
+
caseService,
|
|
2116
|
+
aiRuns,
|
|
2117
|
+
assignSpecialist: options.assignSpecialist
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Assemble the lossless context package for a case: current state, the
|
|
2122
|
+
* merged interaction/event timeline, and every prior AI run (FR-28b).
|
|
2123
|
+
*/
|
|
2124
|
+
async buildContextPackage(caseId) {
|
|
2125
|
+
const supportCase = await this.caseService.getCase(caseId);
|
|
2126
|
+
const [timeline, runs] = await Promise.all([this.caseService.getTimeline(caseId), this.aiRuns.forCase(caseId)]);
|
|
2127
|
+
return {
|
|
2128
|
+
caseNumber: supportCase.caseNumber,
|
|
2129
|
+
subject: supportCase.subject,
|
|
2130
|
+
status: supportCase.status,
|
|
2131
|
+
severity: supportCase.severity,
|
|
2132
|
+
category: supportCase.category,
|
|
2133
|
+
clientProfileId: supportCase.clientProfileId,
|
|
2134
|
+
projectId: supportCase.projectId,
|
|
2135
|
+
planId: supportCase.planId,
|
|
2136
|
+
timeline: timeline.map((item) => compactTimelineItem(item)),
|
|
2137
|
+
aiRuns: runs.map((run) => compactAiRun(run))
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Hand the case to a human: enforce the no-repeat guarantee, stamp
|
|
2142
|
+
* `metadata.activeHandoff`, record the `handoff` audit event carrying the
|
|
2143
|
+
* full context package, then route via the `assignSpecialist` seam (or
|
|
2144
|
+
* queue the case as `triaged` when unrouted).
|
|
2145
|
+
*/
|
|
2146
|
+
async handoff(caseRef, input) {
|
|
2147
|
+
const supportCase = typeof caseRef === "string" ? await this.caseService.getCase(caseRef) : caseRef;
|
|
2148
|
+
const caseId = this.caseIdOf(supportCase);
|
|
2149
|
+
if (this.isHandoffActive(supportCase)) {
|
|
2150
|
+
await this.caseService.recordEvent(supportCase, "handoff", {
|
|
2151
|
+
actorKind: "system",
|
|
2152
|
+
summary: `Human handoff (${input.trigger}) deduped \u2014 one already active`,
|
|
2153
|
+
payload: {
|
|
2154
|
+
trigger: input.trigger,
|
|
2155
|
+
deduped: true
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
return {
|
|
2159
|
+
supportCase,
|
|
2160
|
+
alreadyActive: true
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
supportCase.updateMetadata({ activeHandoff: {
|
|
2164
|
+
trigger: input.trigger,
|
|
2165
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2166
|
+
} });
|
|
2167
|
+
await supportCase.save();
|
|
2168
|
+
if (input.trigger === "client_request") await this.caseService.requestHuman(supportCase, {
|
|
2169
|
+
byProfileId: input.requestedByProfileId ?? null,
|
|
2170
|
+
note: input.note
|
|
2171
|
+
});
|
|
2172
|
+
const contextPackage = await this.buildContextPackage(caseId);
|
|
2173
|
+
await this.caseService.recordEvent(supportCase, "handoff", {
|
|
2174
|
+
actorKind: "system",
|
|
2175
|
+
summary: `Human handoff triggered (${input.trigger})`,
|
|
2176
|
+
payload: {
|
|
2177
|
+
trigger: input.trigger,
|
|
2178
|
+
note: input.note ?? null,
|
|
2179
|
+
contextPackage
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
let routed = null;
|
|
2183
|
+
if (this.assignSpecialist) routed = await this.assignSpecialist(supportCase, { trigger: input.trigger });
|
|
2184
|
+
if (routed) await this.caseService.assign(supportCase, {
|
|
2185
|
+
actorKind: "system",
|
|
2186
|
+
specialistId: routed.specialistId,
|
|
2187
|
+
rationale: routed.rationale
|
|
2188
|
+
});
|
|
2189
|
+
else if (supportCase.status === "new") await this.caseService.transition(supportCase, "triaged", {
|
|
2190
|
+
actorKind: "system",
|
|
2191
|
+
reason: "human handoff queued"
|
|
2192
|
+
});
|
|
2193
|
+
return {
|
|
2194
|
+
supportCase,
|
|
2195
|
+
alreadyActive: false
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* Clear the active-handoff flag so a later trigger can hand off again.
|
|
2200
|
+
* Apps call this when the handoff concludes (assignment accepted or the
|
|
2201
|
+
* case resolves); a resolve-then-reopen also invalidates the flag
|
|
2202
|
+
* automatically inside {@link handoff}.
|
|
2203
|
+
*/
|
|
2204
|
+
async releaseHandoff(caseRef) {
|
|
2205
|
+
const supportCase = typeof caseRef === "string" ? await this.caseService.getCase(caseRef) : caseRef;
|
|
2206
|
+
if (supportCase.getMetadata().activeHandoff) {
|
|
2207
|
+
supportCase.updateMetadata({ activeHandoff: null });
|
|
2208
|
+
await supportCase.save();
|
|
2209
|
+
}
|
|
2210
|
+
return supportCase;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Whether a handoff is still pending on the case. A flag survives only
|
|
2214
|
+
* while the case stays open; a resolve-and-reopen since the flag was set
|
|
2215
|
+
* makes it stale (the new conversation may hand off afresh).
|
|
2216
|
+
*/
|
|
2217
|
+
isHandoffActive(supportCase) {
|
|
2218
|
+
const active = supportCase.getMetadata().activeHandoff;
|
|
2219
|
+
if (!active || typeof active !== "object") return false;
|
|
2220
|
+
if (!supportCase.isOpen()) return false;
|
|
2221
|
+
const at = Date.parse(String(active.at ?? ""));
|
|
2222
|
+
if (Number.isFinite(at) && supportCase.lastReopenedAt && supportCase.lastReopenedAt.getTime() > at) return false;
|
|
2223
|
+
return true;
|
|
2224
|
+
}
|
|
2225
|
+
/** The persisted id of a saved case. */
|
|
2226
|
+
caseIdOf(supportCase) {
|
|
2227
|
+
if (!supportCase.id) throw new Error("SupportCase has no id — was it saved?");
|
|
2228
|
+
return supportCase.id;
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
function compactTimelineItem(item) {
|
|
2232
|
+
if (item.kind === "interaction" && item.interaction) return {
|
|
2233
|
+
kind: "interaction",
|
|
2234
|
+
occurredAt: item.occurredAt.toISOString(),
|
|
2235
|
+
actorKind: item.interaction.actorKind,
|
|
2236
|
+
summary: `${item.interaction.direction} ${item.interaction.channelKind}`,
|
|
2237
|
+
body: item.interaction.body
|
|
2238
|
+
};
|
|
2239
|
+
return {
|
|
2240
|
+
kind: "event",
|
|
2241
|
+
occurredAt: item.occurredAt.toISOString(),
|
|
2242
|
+
actorKind: item.event?.actorKind ?? "system",
|
|
2243
|
+
summary: item.event?.summary ?? ""
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
function compactAiRun(run) {
|
|
2247
|
+
return {
|
|
2248
|
+
phase: run.phase,
|
|
2249
|
+
outcome: run.outcome,
|
|
2250
|
+
confidence: run.confidence,
|
|
2251
|
+
classification: run.getClassification(),
|
|
2252
|
+
error: run.error || null
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
//#endregion
|
|
2256
|
+
//#region src/services/keyed-mutex.ts
|
|
2257
|
+
var KeyedMutex = class {
|
|
2258
|
+
tails = /* @__PURE__ */ new Map();
|
|
2259
|
+
/** Run `fn` exclusively for `key`, after every earlier holder of it. */
|
|
2260
|
+
async run(key, fn) {
|
|
2261
|
+
const turn = (this.tails.get(key) ?? Promise.resolve()).then(fn, fn);
|
|
2262
|
+
const settled = turn.then(() => void 0, () => void 0);
|
|
2263
|
+
this.tails.set(key, settled);
|
|
2264
|
+
try {
|
|
2265
|
+
return await turn;
|
|
2266
|
+
} finally {
|
|
2267
|
+
if (this.tails.get(key) === settled) this.tails.delete(key);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
//#endregion
|
|
2272
|
+
//#region src/services/support-routing-service.ts
|
|
2273
|
+
var ROUTING_WEIGHTS = {
|
|
2274
|
+
/** Case/plan preferred specialist (FR-30) — considered first. */
|
|
2275
|
+
PREFERRED_SPECIALIST: 100,
|
|
2276
|
+
/** Effective `expert` Project Support Qualification. */
|
|
2277
|
+
QUALIFICATION_EXPERT: 30,
|
|
2278
|
+
/** Effective `qualified` Project Support Qualification. */
|
|
2279
|
+
QUALIFICATION_QUALIFIED: 20,
|
|
2280
|
+
/** Effective `trainee` Project Support Qualification. */
|
|
2281
|
+
QUALIFICATION_TRAINEE: 5,
|
|
2282
|
+
/** An active on-call span at the routing instant. */
|
|
2283
|
+
ON_CALL: 25,
|
|
2284
|
+
/** A weekly availability window covering the routing instant. */
|
|
2285
|
+
WEEKLY_AVAILABLE: 15,
|
|
2286
|
+
/** Maximum workload-headroom bonus (scaled by free capacity). */
|
|
2287
|
+
WORKLOAD_HEADROOM_MAX: 10,
|
|
2288
|
+
/** Case language ∈ specialist languages. */
|
|
2289
|
+
LANGUAGE_MATCH: 10
|
|
2290
|
+
};
|
|
2291
|
+
var QUALIFICATION_WEIGHTS = {
|
|
2292
|
+
expert: ROUTING_WEIGHTS.QUALIFICATION_EXPERT,
|
|
2293
|
+
qualified: ROUTING_WEIGHTS.QUALIFICATION_QUALIFIED,
|
|
2294
|
+
trainee: ROUTING_WEIGHTS.QUALIFICATION_TRAINEE
|
|
2295
|
+
};
|
|
2296
|
+
var QUALIFICATION_RANK = {
|
|
2297
|
+
expert: 3,
|
|
2298
|
+
qualified: 2,
|
|
2299
|
+
trainee: 1
|
|
2300
|
+
};
|
|
2301
|
+
var ReassignDeniedError = class extends Error {
|
|
2302
|
+
constructor(message) {
|
|
2303
|
+
super(message);
|
|
2304
|
+
this.name = "ReassignDeniedError";
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
function compactRanking(ranking, limit) {
|
|
2308
|
+
return ranking.slice(0, limit).map((entry) => ({
|
|
2309
|
+
specialistId: entry.specialistId,
|
|
2310
|
+
displayName: entry.displayName,
|
|
2311
|
+
score: entry.score,
|
|
2312
|
+
eligible: entry.eligible
|
|
2313
|
+
}));
|
|
2314
|
+
}
|
|
2315
|
+
function spanActiveAt(window, at) {
|
|
2316
|
+
if (!window.startsAt) return false;
|
|
2317
|
+
if (window.startsAt.getTime() > at.getTime()) return false;
|
|
2318
|
+
return window.endsAt === null || at.getTime() < window.endsAt.getTime();
|
|
2319
|
+
}
|
|
2320
|
+
var SupportRoutingService = class SupportRoutingService {
|
|
2321
|
+
caseService;
|
|
2322
|
+
specialists;
|
|
2323
|
+
qualifications;
|
|
2324
|
+
availabilities;
|
|
2325
|
+
constructor(collections) {
|
|
2326
|
+
this.caseService = collections.caseService;
|
|
2327
|
+
this.specialists = collections.specialists;
|
|
2328
|
+
this.qualifications = collections.qualifications;
|
|
2329
|
+
this.availabilities = collections.availabilities;
|
|
2330
|
+
}
|
|
2331
|
+
static async create(options) {
|
|
2332
|
+
const { caseService, ...smrtOptions } = options;
|
|
2333
|
+
const [service, specialists, qualifications, availabilities] = await Promise.all([
|
|
2334
|
+
caseService ?? SupportCaseService.create(smrtOptions),
|
|
2335
|
+
SupportSpecialistCollection.create(smrtOptions),
|
|
2336
|
+
SupportQualificationCollection.create(smrtOptions),
|
|
2337
|
+
SupportAvailabilityCollection.create(smrtOptions)
|
|
2338
|
+
]);
|
|
2339
|
+
return new SupportRoutingService({
|
|
2340
|
+
caseService: service,
|
|
2341
|
+
specialists,
|
|
2342
|
+
qualifications,
|
|
2343
|
+
availabilities
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
async resolveCase(caseRef) {
|
|
2347
|
+
return typeof caseRef === "string" ? this.caseService.getCase(caseRef) : caseRef;
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Rank every Support Specialist for a case at an instant.
|
|
2351
|
+
*
|
|
2352
|
+
* Hard eligibility filters — each recorded as a factor:
|
|
2353
|
+
* - `status` must be `active`;
|
|
2354
|
+
* - when the case carries a `projectId`, an *effective* Project Support
|
|
2355
|
+
* Qualification for that project (`projectQualification` is the level,
|
|
2356
|
+
* or `'expired'`/`'none'` on failure);
|
|
2357
|
+
* - open assigned cases below `maxConcurrentCases` (`openCases`,
|
|
2358
|
+
* `workloadExceeded`);
|
|
2359
|
+
* - available at `at`: a weekly window in the specialist's own timezone or
|
|
2360
|
+
* an active on-call span, and not inside a time-off span
|
|
2361
|
+
* (`weeklyAvailable`, `onCall`, `timeOff`).
|
|
2362
|
+
*
|
|
2363
|
+
* Ineligible specialists are returned too (`eligible: false`) so the UI
|
|
2364
|
+
* can show the failing factor. Sorted by score descending, then display
|
|
2365
|
+
* name, then id.
|
|
2366
|
+
*/
|
|
2367
|
+
async rankSpecialists(caseRef, opts = {}) {
|
|
2368
|
+
const supportCase = await this.resolveCase(caseRef);
|
|
2369
|
+
const at = opts.at ?? /* @__PURE__ */ new Date();
|
|
2370
|
+
const excluded = new Set(opts.exclude ?? []);
|
|
2371
|
+
const specialists = await this.specialists.list({});
|
|
2372
|
+
const ranked = [];
|
|
2373
|
+
for (const specialist of specialists) ranked.push(await this.evaluateSpecialist(supportCase, specialist, at, excluded));
|
|
2374
|
+
return ranked.sort((a, b) => b.score - a.score || a.displayName.localeCompare(b.displayName) || a.specialistId.localeCompare(b.specialistId));
|
|
2375
|
+
}
|
|
2376
|
+
async evaluateSpecialist(supportCase, specialist, at, excluded) {
|
|
2377
|
+
const specialistId = specialist.id ?? "";
|
|
2378
|
+
const factors = {};
|
|
2379
|
+
let eligible = true;
|
|
2380
|
+
let score = 0;
|
|
2381
|
+
factors.status = specialist.status;
|
|
2382
|
+
if (!specialist.isActive()) eligible = false;
|
|
2383
|
+
if (specialist.tenantId && supportCase.tenantId && specialist.tenantId !== supportCase.tenantId) {
|
|
2384
|
+
factors.tenantMismatch = true;
|
|
2385
|
+
eligible = false;
|
|
2386
|
+
}
|
|
2387
|
+
if (excluded.has(specialistId)) {
|
|
2388
|
+
factors.excluded = true;
|
|
2389
|
+
eligible = false;
|
|
2390
|
+
}
|
|
2391
|
+
if (supportCase.projectId) {
|
|
2392
|
+
const qualifications = (await this.qualifications.forSpecialist(specialistId)).filter((q) => q.projectId === supportCase.projectId);
|
|
2393
|
+
const effective = qualifications.filter((q) => q.isEffectiveAt(at));
|
|
2394
|
+
if (effective.length > 0) {
|
|
2395
|
+
const level = effective.map((q) => q.level).sort((a, b) => QUALIFICATION_RANK[b] - QUALIFICATION_RANK[a])[0];
|
|
2396
|
+
factors.projectQualification = level;
|
|
2397
|
+
score += QUALIFICATION_WEIGHTS[level];
|
|
2398
|
+
} else {
|
|
2399
|
+
factors.projectQualification = qualifications.length > 0 ? "expired" : "none";
|
|
2400
|
+
eligible = false;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
const openCases = await this.caseService.cases.findQueue({
|
|
2404
|
+
assignedSpecialistId: specialistId,
|
|
2405
|
+
openOnly: true,
|
|
2406
|
+
limit: 1e3
|
|
2407
|
+
});
|
|
2408
|
+
factors.openCases = openCases.length;
|
|
2409
|
+
factors.maxConcurrentCases = specialist.maxConcurrentCases;
|
|
2410
|
+
if (openCases.length >= specialist.maxConcurrentCases) {
|
|
2411
|
+
factors.workloadExceeded = true;
|
|
2412
|
+
eligible = false;
|
|
2413
|
+
} else if (specialist.maxConcurrentCases > 0) score += Math.round(ROUTING_WEIGHTS.WORKLOAD_HEADROOM_MAX * (specialist.maxConcurrentCases - openCases.length) / specialist.maxConcurrentCases);
|
|
2414
|
+
const windows = await this.availabilities.forSpecialist(specialistId);
|
|
2415
|
+
const wall = zonedParts(at, specialist.timezone || "UTC");
|
|
2416
|
+
const weeklyAvailable = windows.some((window) => window.kind === "weekly" && window.weekday === wall.weekday && wall.minuteOfDay >= window.startMinute && wall.minuteOfDay < window.endMinute);
|
|
2417
|
+
const onCall = windows.some((window) => window.kind === "on_call" && spanActiveAt(window, at));
|
|
2418
|
+
const timeOff = windows.some((window) => window.kind === "time_off" && spanActiveAt(window, at));
|
|
2419
|
+
factors.weeklyAvailable = weeklyAvailable;
|
|
2420
|
+
factors.onCall = onCall;
|
|
2421
|
+
factors.timeOff = timeOff;
|
|
2422
|
+
if (timeOff || !weeklyAvailable && !onCall) eligible = false;
|
|
2423
|
+
if (onCall) score += ROUTING_WEIGHTS.ON_CALL;
|
|
2424
|
+
if (weeklyAvailable) score += ROUTING_WEIGHTS.WEEKLY_AVAILABLE;
|
|
2425
|
+
if (supportCase.preferredSpecialistId && supportCase.preferredSpecialistId === specialistId) {
|
|
2426
|
+
factors.preferred = true;
|
|
2427
|
+
score += ROUTING_WEIGHTS.PREFERRED_SPECIALIST;
|
|
2428
|
+
}
|
|
2429
|
+
const language = supportCase.getMetadata().language;
|
|
2430
|
+
if (typeof language === "string" && language.length > 0 && specialist.getLanguages().includes(language)) {
|
|
2431
|
+
factors.languageMatch = true;
|
|
2432
|
+
score += ROUTING_WEIGHTS.LANGUAGE_MATCH;
|
|
2433
|
+
}
|
|
2434
|
+
if (specialist.onCallPriority !== 0) {
|
|
2435
|
+
factors.onCallPriority = specialist.onCallPriority;
|
|
2436
|
+
score += specialist.onCallPriority;
|
|
2437
|
+
}
|
|
2438
|
+
return {
|
|
2439
|
+
specialistId,
|
|
2440
|
+
displayName: specialist.displayName || specialistId,
|
|
2441
|
+
score,
|
|
2442
|
+
eligible,
|
|
2443
|
+
factors
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Assign the top eligible ranked specialist, recording the rationale
|
|
2448
|
+
* (winning factors plus the compact top of the ranking) on the assignment
|
|
2449
|
+
* event. When no one is eligible, records an unrouted `note` event with
|
|
2450
|
+
* the full ranking and returns `assigned: false`.
|
|
2451
|
+
*/
|
|
2452
|
+
async autoAssign(caseRef, opts = {}) {
|
|
2453
|
+
const supportCase = await this.resolveCase(caseRef);
|
|
2454
|
+
const ranking = await this.rankSpecialists(supportCase, {
|
|
2455
|
+
at: opts.at,
|
|
2456
|
+
exclude: opts.exclude
|
|
2457
|
+
});
|
|
2458
|
+
const top = ranking.find((entry) => entry.eligible);
|
|
2459
|
+
if (!top) {
|
|
2460
|
+
await this.caseService.recordEvent(supportCase, "note", {
|
|
2461
|
+
actorKind: opts.actorKind ?? "system",
|
|
2462
|
+
occurredAt: opts.at,
|
|
2463
|
+
summary: "Routing found no eligible specialist",
|
|
2464
|
+
payload: {
|
|
2465
|
+
unrouted: true,
|
|
2466
|
+
ranking: compactRanking(ranking, ranking.length)
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
return {
|
|
2470
|
+
assigned: false,
|
|
2471
|
+
ranking
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
await this.caseService.assign(supportCase, {
|
|
2475
|
+
actorKind: opts.actorKind ?? "system",
|
|
2476
|
+
specialistId: top.specialistId,
|
|
2477
|
+
rationale: {
|
|
2478
|
+
factors: top.factors,
|
|
2479
|
+
ranking: compactRanking(ranking, 3)
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
return {
|
|
2483
|
+
assigned: true,
|
|
2484
|
+
specialistId: top.specialistId,
|
|
2485
|
+
ranking
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Manual reassignment override (FR-30). Requires the
|
|
2490
|
+
* `support.reassign-case` permission split on the acting principal and
|
|
2491
|
+
* refuses cross-tenant acts; the assignment event carries
|
|
2492
|
+
* `{ manual: true, note }` as its rationale.
|
|
2493
|
+
*/
|
|
2494
|
+
async reassign(caseRef, input) {
|
|
2495
|
+
if (!input.principal.can("support.reassign-case")) throw new ReassignDeniedError(`Reassignment denied: principal lacks '${REASSIGN_CASE_PERMISSION}'.`);
|
|
2496
|
+
const supportCase = await this.resolveCase(caseRef);
|
|
2497
|
+
if (input.principal.tenantId && supportCase.tenantId && input.principal.tenantId !== supportCase.tenantId) throw new ReassignDeniedError("Reassignment denied: principal tenant does not match the case tenant.");
|
|
2498
|
+
const specialist = await this.specialists.get({ id: input.specialistId });
|
|
2499
|
+
if (!specialist) throw new ReassignDeniedError(`Reassignment denied: unknown specialist '${input.specialistId}'.`);
|
|
2500
|
+
if (specialist.tenantId && supportCase.tenantId && specialist.tenantId !== supportCase.tenantId) throw new ReassignDeniedError("Reassignment denied: specialist tenant does not match the case tenant.");
|
|
2501
|
+
return this.caseService.assign(supportCase, {
|
|
2502
|
+
actorKind: "specialist",
|
|
2503
|
+
actorProfileId: input.principal.id ?? null,
|
|
2504
|
+
specialistId: input.specialistId,
|
|
2505
|
+
rationale: {
|
|
2506
|
+
manual: true,
|
|
2507
|
+
note: input.note ?? null
|
|
2508
|
+
}
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
};
|
|
2512
|
+
//#endregion
|
|
2513
|
+
//#region src/services/service-target-engine.ts
|
|
2514
|
+
var service_target_engine_exports = /* @__PURE__ */ __exportAll({
|
|
2515
|
+
DEFAULT_SEVERITY_KEY: () => DEFAULT_SEVERITY_KEY,
|
|
2516
|
+
ESCALATION_JOB_PRIORITY: () => 75,
|
|
2517
|
+
SUPPORT_JOB_QUEUE: () => SUPPORT_JOB_QUEUE,
|
|
2518
|
+
ServiceTargetEngine: () => ServiceTargetEngine,
|
|
2519
|
+
default: () => ServiceTargetEngine
|
|
2520
|
+
});
|
|
2521
|
+
var SUPPORT_JOB_QUEUE = "support";
|
|
2522
|
+
var ESCALATION_JOB_PRIORITY = 75;
|
|
2523
|
+
var DEFAULT_SEVERITY_KEY = "sev3";
|
|
2524
|
+
var DEFAULT_PAUSE_STATUSES = ["waiting_on_client"];
|
|
2525
|
+
var MINUTE_MS = 6e4;
|
|
2526
|
+
var TARGET_TYPES = [
|
|
2527
|
+
"acknowledgement",
|
|
2528
|
+
"response",
|
|
2529
|
+
"update",
|
|
2530
|
+
"resolution"
|
|
2531
|
+
];
|
|
2532
|
+
function targetMinutesFor(minutes, targetType) {
|
|
2533
|
+
switch (targetType) {
|
|
2534
|
+
case "acknowledgement": return minutes.acknowledgementMinutes;
|
|
2535
|
+
case "response": return minutes.responseMinutes;
|
|
2536
|
+
case "update": return minutes.updateMinutes;
|
|
2537
|
+
case "resolution": return minutes.resolutionMinutes;
|
|
2538
|
+
default: return null;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
function asStringArray(value) {
|
|
2542
|
+
if (!Array.isArray(value)) return null;
|
|
2543
|
+
return value.filter((entry) => typeof entry === "string");
|
|
2544
|
+
}
|
|
2545
|
+
function asFiniteNumber(value) {
|
|
2546
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2547
|
+
}
|
|
2548
|
+
function parseIsoDate(value) {
|
|
2549
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
2550
|
+
if (typeof value === "string" && value) {
|
|
2551
|
+
const parsed = new Date(value);
|
|
2552
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
2553
|
+
}
|
|
2554
|
+
return null;
|
|
2555
|
+
}
|
|
2556
|
+
var ServiceTargetEngine = class ServiceTargetEngine {
|
|
2557
|
+
caseService;
|
|
2558
|
+
targets;
|
|
2559
|
+
escalations;
|
|
2560
|
+
jobs;
|
|
2561
|
+
scheduleJobs;
|
|
2562
|
+
options;
|
|
2563
|
+
routing;
|
|
2564
|
+
constructor(deps) {
|
|
2565
|
+
this.options = deps.options;
|
|
2566
|
+
this.caseService = deps.caseService;
|
|
2567
|
+
this.targets = deps.targets;
|
|
2568
|
+
this.escalations = deps.escalations;
|
|
2569
|
+
this.jobs = deps.jobs;
|
|
2570
|
+
this.scheduleJobs = deps.scheduleJobs;
|
|
2571
|
+
}
|
|
2572
|
+
static async create(options) {
|
|
2573
|
+
const { caseService, scheduleJobs, ...smrtOptions } = options;
|
|
2574
|
+
const [service, targets, escalations, jobs] = await Promise.all([
|
|
2575
|
+
caseService ?? SupportCaseService.create(smrtOptions),
|
|
2576
|
+
SupportServiceTargetCollection.create(smrtOptions),
|
|
2577
|
+
SupportEscalationCollection.create(smrtOptions),
|
|
2578
|
+
SmrtJobCollection.create(smrtOptions)
|
|
2579
|
+
]);
|
|
2580
|
+
return new ServiceTargetEngine({
|
|
2581
|
+
options: smrtOptions,
|
|
2582
|
+
caseService: service,
|
|
2583
|
+
targets,
|
|
2584
|
+
escalations,
|
|
2585
|
+
jobs,
|
|
2586
|
+
scheduleJobs: scheduleJobs ?? true
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
async resolveCase(caseRef) {
|
|
2590
|
+
return typeof caseRef === "string" ? this.caseService.getCase(caseRef) : caseRef;
|
|
2591
|
+
}
|
|
2592
|
+
requireId(entity, label) {
|
|
2593
|
+
if (!entity.id) throw new Error(`${label} has no id \u2014 was it saved?`);
|
|
2594
|
+
return entity.id;
|
|
2595
|
+
}
|
|
2596
|
+
async getRoutingService() {
|
|
2597
|
+
if (!this.routing) this.routing = await SupportRoutingService.create({
|
|
2598
|
+
...this.options,
|
|
2599
|
+
caseService: this.caseService
|
|
2600
|
+
});
|
|
2601
|
+
return this.routing;
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Resolve the plan terms governing a case's clocks: the frozen
|
|
2605
|
+
* `planSnapshot` when present, else the loaded plan, else conservative
|
|
2606
|
+
* defaults (24×7 calendar, default target minutes, pause on
|
|
2607
|
+
* `waiting_on_client`, empty escalation policy).
|
|
2608
|
+
*/
|
|
2609
|
+
async resolvePlanTerms(supportCase) {
|
|
2610
|
+
const snapshot = supportCase.getPlanSnapshot();
|
|
2611
|
+
if (Object.keys(snapshot).length > 0) {
|
|
2612
|
+
const calendar = {
|
|
2613
|
+
windows: Array.isArray(snapshot.coverage) ? snapshot.coverage : [],
|
|
2614
|
+
holidays: asStringArray(snapshot.holidays) ?? [],
|
|
2615
|
+
timezone: typeof snapshot.timezone === "string" && snapshot.timezone ? snapshot.timezone : "UTC"
|
|
2616
|
+
};
|
|
2617
|
+
const targets = snapshot.targets && typeof snapshot.targets === "object" ? snapshot.targets : {};
|
|
2618
|
+
return {
|
|
2619
|
+
calendar,
|
|
2620
|
+
pauseStatuses: asStringArray(snapshot.pauseStatuses) ?? DEFAULT_PAUSE_STATUSES,
|
|
2621
|
+
escalationPolicy: Array.isArray(snapshot.escalationPolicy) ? snapshot.escalationPolicy : [],
|
|
2622
|
+
targetsForSeverity: (severity) => ({
|
|
2623
|
+
...DEFAULT_TARGET_MINUTES,
|
|
2624
|
+
...targets[severity] ?? {}
|
|
2625
|
+
})
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
if (supportCase.planId) {
|
|
2629
|
+
const plan = await this.caseService.plans.get({ id: supportCase.planId });
|
|
2630
|
+
if (plan) return {
|
|
2631
|
+
calendar: {
|
|
2632
|
+
windows: plan.getCoverage(),
|
|
2633
|
+
holidays: plan.getHolidays(),
|
|
2634
|
+
timezone: plan.timezone || "UTC"
|
|
2635
|
+
},
|
|
2636
|
+
pauseStatuses: plan.getPauseStatuses(),
|
|
2637
|
+
escalationPolicy: plan.getEscalationPolicy(),
|
|
2638
|
+
targetsForSeverity: (severity) => plan.targetsForSeverity(severity)
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
return {
|
|
2642
|
+
calendar: {
|
|
2643
|
+
windows: [],
|
|
2644
|
+
holidays: [],
|
|
2645
|
+
timezone: "UTC"
|
|
2646
|
+
},
|
|
2647
|
+
pauseStatuses: [...DEFAULT_PAUSE_STATUSES],
|
|
2648
|
+
escalationPolicy: [],
|
|
2649
|
+
targetsForSeverity: () => ({ ...DEFAULT_TARGET_MINUTES })
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* Start the case's Service Target clocks (idempotent: target types that
|
|
2654
|
+
* already have an active clock are skipped). One clock per non-null
|
|
2655
|
+
* configured minutes for the case severity — the `update` clock only when
|
|
2656
|
+
* the plan configures `updateMinutes`. A first start creates cycle 0;
|
|
2657
|
+
* starting again after clocks settled (e.g. a reopen) creates the next
|
|
2658
|
+
* cycle, preserving the settled rows.
|
|
2659
|
+
*/
|
|
2660
|
+
async startTargetsForCase(caseRef, opts = {}) {
|
|
2661
|
+
const supportCase = await this.resolveCase(caseRef);
|
|
2662
|
+
return this.startTargets(supportCase, opts.at ?? /* @__PURE__ */ new Date());
|
|
2663
|
+
}
|
|
2664
|
+
async startTargets(supportCase, at) {
|
|
2665
|
+
const caseId = this.requireId(supportCase, "SupportCase");
|
|
2666
|
+
const terms = await this.resolvePlanTerms(supportCase);
|
|
2667
|
+
const severity = supportCase.severity || "sev3";
|
|
2668
|
+
const minutes = terms.targetsForSeverity(severity);
|
|
2669
|
+
const existing = await this.targets.forCase(caseId);
|
|
2670
|
+
const created = [];
|
|
2671
|
+
for (const targetType of TARGET_TYPES) {
|
|
2672
|
+
const baseMinutes = targetMinutesFor(minutes, targetType);
|
|
2673
|
+
if (baseMinutes === null) continue;
|
|
2674
|
+
if (await this.targets.activeTarget(caseId, targetType)) continue;
|
|
2675
|
+
const prior = existing.filter((target) => target.targetType === targetType);
|
|
2676
|
+
const cycle = prior.length > 0 ? Math.max(...prior.map((target) => target.cycle)) + 1 : 0;
|
|
2677
|
+
created.push(await this.startTarget(supportCase, terms, {
|
|
2678
|
+
targetType,
|
|
2679
|
+
cycle,
|
|
2680
|
+
severity,
|
|
2681
|
+
baseMinutes,
|
|
2682
|
+
at
|
|
2683
|
+
}));
|
|
2684
|
+
}
|
|
2685
|
+
return created;
|
|
2686
|
+
}
|
|
2687
|
+
async startTarget(supportCase, terms, input) {
|
|
2688
|
+
const dueAt = addCoveredMinutes(terms.calendar, input.at, input.baseMinutes);
|
|
2689
|
+
const target = await this.targets.create({
|
|
2690
|
+
tenantId: supportCase.tenantId,
|
|
2691
|
+
caseId: this.requireId(supportCase, "SupportCase"),
|
|
2692
|
+
targetType: input.targetType,
|
|
2693
|
+
cycle: input.cycle,
|
|
2694
|
+
severity: input.severity,
|
|
2695
|
+
baseMinutes: input.baseMinutes,
|
|
2696
|
+
startedAt: input.at,
|
|
2697
|
+
dueAt,
|
|
2698
|
+
status: "pending",
|
|
2699
|
+
metadata: JSON.stringify({ consumedMinutes: 0 })
|
|
2700
|
+
});
|
|
2701
|
+
await this.scheduleEscalationJob(target, dueAt);
|
|
2702
|
+
await this.caseService.recordEvent(supportCase, "target_scheduled", {
|
|
2703
|
+
actorKind: "system",
|
|
2704
|
+
occurredAt: input.at,
|
|
2705
|
+
summary: `${input.targetType} target scheduled (cycle ${input.cycle}, due ${dueAt.toISOString()})`,
|
|
2706
|
+
payload: {
|
|
2707
|
+
targetId: target.id,
|
|
2708
|
+
targetType: input.targetType,
|
|
2709
|
+
cycle: input.cycle,
|
|
2710
|
+
severity: input.severity,
|
|
2711
|
+
baseMinutes: input.baseMinutes,
|
|
2712
|
+
dueAt: dueAt.toISOString()
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
return target;
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Enqueue the one-shot escalation job for a clock at `runAt` and record
|
|
2719
|
+
* its id on the target (`escalationJobId`) so satisfying can cancel it.
|
|
2720
|
+
*/
|
|
2721
|
+
async scheduleEscalationJob(target, runAt) {
|
|
2722
|
+
if (!this.scheduleJobs) return;
|
|
2723
|
+
target.escalationJobId = (await this.jobs.enqueueJob({
|
|
2724
|
+
tenantId: target.tenantId ?? null,
|
|
2725
|
+
queue: "support",
|
|
2726
|
+
objectType: "SupportServiceTarget",
|
|
2727
|
+
objectId: this.requireId(target, "SupportServiceTarget"),
|
|
2728
|
+
method: "checkAndEscalate",
|
|
2729
|
+
args: {},
|
|
2730
|
+
runAt,
|
|
2731
|
+
priority: 75
|
|
2732
|
+
}, { tenantJobCap: 0 })).id ?? "";
|
|
2733
|
+
await target.save();
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Cancel a clock's pending escalation job, tolerating jobs that already
|
|
2737
|
+
* ran, were cancelled, or are missing (at-least-once semantics).
|
|
2738
|
+
*/
|
|
2739
|
+
async cancelEscalationJob(target) {
|
|
2740
|
+
if (!target.escalationJobId) return;
|
|
2741
|
+
try {
|
|
2742
|
+
const job = await this.jobs.get({ id: target.escalationJobId });
|
|
2743
|
+
if (job && (job.status === "pending" || job.status === "running")) await job.cancel();
|
|
2744
|
+
} catch {}
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* React to a recorded interaction. Outbound specialist/agent interactions
|
|
2748
|
+
* satisfy the acknowledgement clock (first outbound of any kind), the
|
|
2749
|
+
* response clock (first substantive outbound — the same trigger), and the
|
|
2750
|
+
* active update-cycle clock (which then restarts at the next cycle from
|
|
2751
|
+
* the interaction instant). Inbound client interactions never satisfy
|
|
2752
|
+
* clocks.
|
|
2753
|
+
*/
|
|
2754
|
+
async onInteractionRecorded(caseRef, interaction) {
|
|
2755
|
+
if (interaction.direction !== "outbound") return [];
|
|
2756
|
+
if (interaction.actorKind !== "specialist" && interaction.actorKind !== "agent") return [];
|
|
2757
|
+
const supportCase = await this.resolveCase(caseRef);
|
|
2758
|
+
const caseId = this.requireId(supportCase, "SupportCase");
|
|
2759
|
+
const at = interaction.occurredAt ?? /* @__PURE__ */ new Date();
|
|
2760
|
+
const satisfied = [];
|
|
2761
|
+
const interactionMeta = interaction.getMetadata();
|
|
2762
|
+
if (interactionMeta.draft === true) return [];
|
|
2763
|
+
const ackOnly = interactionMeta.acknowledgement === true;
|
|
2764
|
+
const satisfiableTypes = ackOnly ? ["acknowledgement"] : ["acknowledgement", "response"];
|
|
2765
|
+
for (const targetType of satisfiableTypes) {
|
|
2766
|
+
const target = await this.targets.activeTarget(caseId, targetType);
|
|
2767
|
+
if (target) {
|
|
2768
|
+
await this.satisfyTarget(supportCase, target, at);
|
|
2769
|
+
satisfied.push(target);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
const update = ackOnly ? null : await this.targets.activeTarget(caseId, "update");
|
|
2773
|
+
if (update) {
|
|
2774
|
+
await this.satisfyTarget(supportCase, update, at);
|
|
2775
|
+
satisfied.push(update);
|
|
2776
|
+
const terms = await this.resolvePlanTerms(supportCase);
|
|
2777
|
+
const severity = supportCase.severity || "sev3";
|
|
2778
|
+
const updateMinutes = terms.targetsForSeverity(severity).updateMinutes;
|
|
2779
|
+
if (updateMinutes !== null) await this.startTarget(supportCase, terms, {
|
|
2780
|
+
targetType: "update",
|
|
2781
|
+
cycle: update.cycle + 1,
|
|
2782
|
+
severity,
|
|
2783
|
+
baseMinutes: updateMinutes,
|
|
2784
|
+
at
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
return satisfied;
|
|
2788
|
+
}
|
|
2789
|
+
async satisfyTarget(supportCase, target, at) {
|
|
2790
|
+
if (target.pausedAt) {
|
|
2791
|
+
target.pausedTotalSeconds += Math.max(0, Math.round((at.getTime() - target.pausedAt.getTime()) / 1e3));
|
|
2792
|
+
target.pausedAt = null;
|
|
2793
|
+
}
|
|
2794
|
+
target.status = "satisfied";
|
|
2795
|
+
target.satisfiedAt = at;
|
|
2796
|
+
await target.save();
|
|
2797
|
+
await this.cancelEscalationJob(target);
|
|
2798
|
+
await this.caseService.recordEvent(supportCase, "target_satisfied", {
|
|
2799
|
+
actorKind: "system",
|
|
2800
|
+
occurredAt: at,
|
|
2801
|
+
summary: `${target.targetType} target satisfied`,
|
|
2802
|
+
payload: {
|
|
2803
|
+
targetId: target.id,
|
|
2804
|
+
targetType: target.targetType,
|
|
2805
|
+
cycle: target.cycle
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* React to a case lifecycle transition:
|
|
2811
|
+
* - to `resolved` — satisfy the resolution clock, cancel every other
|
|
2812
|
+
* active clock (and its job);
|
|
2813
|
+
* - to `closed` — cancel all remaining active clocks;
|
|
2814
|
+
* - reopen (`resolved`/`closed` → an open status) — start fresh clocks on
|
|
2815
|
+
* the next cycle for every type;
|
|
2816
|
+
* - entering a plan pause status — freeze pending clocks (FR-29b: only
|
|
2817
|
+
* when the plan says so);
|
|
2818
|
+
* - leaving a pause status — resume paused clocks with `dueAt` recomputed
|
|
2819
|
+
* from the remaining covered minutes and a fresh escalation job.
|
|
2820
|
+
*/
|
|
2821
|
+
async onCaseTransition(caseRef, from, to, opts = {}) {
|
|
2822
|
+
const supportCase = await this.resolveCase(caseRef);
|
|
2823
|
+
const at = opts.at ?? /* @__PURE__ */ new Date();
|
|
2824
|
+
if (to === "resolved" || to === "closed") {
|
|
2825
|
+
await this.settleTargetsForTerminal(supportCase, to, at);
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
if ((from === "resolved" || from === "closed") && OPEN_SUPPORT_CASE_STATUSES.includes(to)) {
|
|
2829
|
+
await this.startTargets(supportCase, at);
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
const terms = await this.resolvePlanTerms(supportCase);
|
|
2833
|
+
const wasPaused = terms.pauseStatuses.includes(from);
|
|
2834
|
+
const nowPaused = terms.pauseStatuses.includes(to);
|
|
2835
|
+
if (!wasPaused && nowPaused) await this.pauseTargets(supportCase, terms, at);
|
|
2836
|
+
else if (wasPaused && !nowPaused) await this.resumeTargets(supportCase, terms, at);
|
|
2837
|
+
}
|
|
2838
|
+
async settleTargetsForTerminal(supportCase, to, at) {
|
|
2839
|
+
const caseId = this.requireId(supportCase, "SupportCase");
|
|
2840
|
+
if (to === "resolved") {
|
|
2841
|
+
const resolution = await this.targets.activeTarget(caseId, "resolution");
|
|
2842
|
+
if (resolution) await this.satisfyTarget(supportCase, resolution, at);
|
|
2843
|
+
}
|
|
2844
|
+
const remaining = await this.targets.list({ where: {
|
|
2845
|
+
caseId,
|
|
2846
|
+
"status in": ["pending", "paused"]
|
|
2847
|
+
} });
|
|
2848
|
+
for (const target of remaining) {
|
|
2849
|
+
if (target.pausedAt) {
|
|
2850
|
+
target.pausedTotalSeconds += Math.max(0, Math.round((at.getTime() - target.pausedAt.getTime()) / 1e3));
|
|
2851
|
+
target.pausedAt = null;
|
|
2852
|
+
}
|
|
2853
|
+
target.status = "cancelled";
|
|
2854
|
+
target.cancelledAt = at;
|
|
2855
|
+
await target.save();
|
|
2856
|
+
await this.cancelEscalationJob(target);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
async pauseTargets(supportCase, terms, at) {
|
|
2860
|
+
const caseId = this.requireId(supportCase, "SupportCase");
|
|
2861
|
+
const pending = await this.targets.list({ where: {
|
|
2862
|
+
caseId,
|
|
2863
|
+
status: "pending"
|
|
2864
|
+
} });
|
|
2865
|
+
for (const target of pending) {
|
|
2866
|
+
const meta = target.getMetadata();
|
|
2867
|
+
const segmentStart = parseIsoDate(meta.lastResumedAt) ?? target.startedAt;
|
|
2868
|
+
const consumedMinutes = asFiniteNumber(meta.consumedMinutes) + coveredMinutesBetween(terms.calendar, segmentStart, at);
|
|
2869
|
+
target.setMetadata({
|
|
2870
|
+
...meta,
|
|
2871
|
+
consumedMinutes
|
|
2872
|
+
});
|
|
2873
|
+
target.pausedAt = at;
|
|
2874
|
+
target.status = "paused";
|
|
2875
|
+
await target.save();
|
|
2876
|
+
await this.cancelEscalationJob(target);
|
|
2877
|
+
await this.caseService.recordEvent(supportCase, "target_paused", {
|
|
2878
|
+
actorKind: "system",
|
|
2879
|
+
occurredAt: at,
|
|
2880
|
+
summary: `${target.targetType} target paused`,
|
|
2881
|
+
payload: {
|
|
2882
|
+
targetId: target.id,
|
|
2883
|
+
targetType: target.targetType,
|
|
2884
|
+
cycle: target.cycle,
|
|
2885
|
+
consumedMinutes
|
|
2886
|
+
}
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
async resumeTargets(supportCase, terms, at) {
|
|
2891
|
+
const caseId = this.requireId(supportCase, "SupportCase");
|
|
2892
|
+
const paused = await this.targets.list({ where: {
|
|
2893
|
+
caseId,
|
|
2894
|
+
status: "paused"
|
|
2895
|
+
} });
|
|
2896
|
+
for (const target of paused) {
|
|
2897
|
+
const meta = target.getMetadata();
|
|
2898
|
+
const remainingMinutes = Math.max(target.baseMinutes - asFiniteNumber(meta.consumedMinutes), 0);
|
|
2899
|
+
if (target.pausedAt) target.pausedTotalSeconds += Math.max(0, Math.round((at.getTime() - target.pausedAt.getTime()) / 1e3));
|
|
2900
|
+
target.pausedAt = null;
|
|
2901
|
+
target.status = "pending";
|
|
2902
|
+
const dueAt = addCoveredMinutes(terms.calendar, at, remainingMinutes);
|
|
2903
|
+
target.dueAt = dueAt;
|
|
2904
|
+
target.setMetadata({
|
|
2905
|
+
...meta,
|
|
2906
|
+
lastResumedAt: at.toISOString()
|
|
2907
|
+
});
|
|
2908
|
+
await target.save();
|
|
2909
|
+
await this.scheduleEscalationJob(target, dueAt);
|
|
2910
|
+
await this.caseService.recordEvent(supportCase, "target_resumed", {
|
|
2911
|
+
actorKind: "system",
|
|
2912
|
+
occurredAt: at,
|
|
2913
|
+
summary: `${target.targetType} target resumed (due ${dueAt.toISOString()})`,
|
|
2914
|
+
payload: {
|
|
2915
|
+
targetId: target.id,
|
|
2916
|
+
targetType: target.targetType,
|
|
2917
|
+
cycle: target.cycle,
|
|
2918
|
+
remainingMinutes,
|
|
2919
|
+
dueAt: dueAt.toISOString()
|
|
2920
|
+
}
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Escalate a breached clock through the plan's escalation policy: write
|
|
2926
|
+
* the {@link SupportEscalation} audit row, apply the step action
|
|
2927
|
+
* (`notify` records the profiles to notify; `reassign` routes to the next
|
|
2928
|
+
* eligible specialist, excluding the current assignee), bump the case
|
|
2929
|
+
* escalation level, transition the case to `escalated` (unless already
|
|
2930
|
+
* resolved/closed), and schedule the next policy step when it declares a
|
|
2931
|
+
* `delayMinutes`.
|
|
2932
|
+
*/
|
|
2933
|
+
async escalateForBreach(target, opts = {}) {
|
|
2934
|
+
const at = opts.at ?? target.breachedAt ?? /* @__PURE__ */ new Date();
|
|
2935
|
+
const supportCase = await this.caseService.getCase(target.caseId);
|
|
2936
|
+
const terms = await this.resolvePlanTerms(supportCase);
|
|
2937
|
+
const level = supportCase.escalationLevel + 1;
|
|
2938
|
+
const step = terms.escalationPolicy[level - 1] ?? {
|
|
2939
|
+
level,
|
|
2940
|
+
action: "notify"
|
|
2941
|
+
};
|
|
2942
|
+
if ((await this.escalations.list({
|
|
2943
|
+
where: {
|
|
2944
|
+
targetId: target.id,
|
|
2945
|
+
level
|
|
2946
|
+
},
|
|
2947
|
+
limit: 1
|
|
2948
|
+
})).length > 0) return;
|
|
2949
|
+
const escalation = await this.escalations.create({
|
|
2950
|
+
tenantId: supportCase.tenantId,
|
|
2951
|
+
caseId: this.requireId(supportCase, "SupportCase"),
|
|
2952
|
+
level,
|
|
2953
|
+
reason: "target_breach",
|
|
2954
|
+
targetId: target.id,
|
|
2955
|
+
targetType: target.targetType,
|
|
2956
|
+
action: step.action,
|
|
2957
|
+
fromSpecialistId: supportCase.assignedSpecialistId,
|
|
2958
|
+
notifiedProfileIds: JSON.stringify(step.notifyProfileIds ?? []),
|
|
2959
|
+
occurredAt: at
|
|
2960
|
+
});
|
|
2961
|
+
let reassignedTo = null;
|
|
2962
|
+
if (step.action === "reassign") {
|
|
2963
|
+
const routing = await this.getRoutingService();
|
|
2964
|
+
const exclude = supportCase.assignedSpecialistId ? [supportCase.assignedSpecialistId] : [];
|
|
2965
|
+
const result = await routing.autoAssign(supportCase, {
|
|
2966
|
+
at,
|
|
2967
|
+
exclude,
|
|
2968
|
+
actorKind: "system"
|
|
2969
|
+
});
|
|
2970
|
+
if (result.assigned && result.specialistId) {
|
|
2971
|
+
reassignedTo = result.specialistId;
|
|
2972
|
+
escalation.toSpecialistId = result.specialistId;
|
|
2973
|
+
await escalation.save();
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
supportCase.escalationLevel = level;
|
|
2977
|
+
supportCase.escalatedAt = at;
|
|
2978
|
+
if (!(supportCase.status === "resolved" || supportCase.status === "closed") && supportCase.status !== "escalated") await this.caseService.transition(supportCase, "escalated", {
|
|
2979
|
+
actorKind: "system",
|
|
2980
|
+
reason: `${target.targetType} target breached`
|
|
2981
|
+
});
|
|
2982
|
+
else await supportCase.save();
|
|
2983
|
+
await this.caseService.recordEvent(supportCase, "target_breached", {
|
|
2984
|
+
actorKind: "system",
|
|
2985
|
+
occurredAt: at,
|
|
2986
|
+
summary: `${target.targetType} target breached`,
|
|
2987
|
+
payload: {
|
|
2988
|
+
targetId: target.id,
|
|
2989
|
+
targetType: target.targetType,
|
|
2990
|
+
cycle: target.cycle,
|
|
2991
|
+
dueAt: target.dueAt.toISOString()
|
|
2992
|
+
}
|
|
2993
|
+
});
|
|
2994
|
+
await this.caseService.recordEvent(supportCase, "escalation", {
|
|
2995
|
+
actorKind: "system",
|
|
2996
|
+
occurredAt: at,
|
|
2997
|
+
summary: `Escalated to level ${level} (${step.action})`,
|
|
2998
|
+
payload: {
|
|
2999
|
+
escalationId: escalation.id,
|
|
3000
|
+
level,
|
|
3001
|
+
action: step.action,
|
|
3002
|
+
targetId: target.id,
|
|
3003
|
+
targetType: target.targetType,
|
|
3004
|
+
notifiedProfileIds: step.notifyProfileIds ?? [],
|
|
3005
|
+
reassignedTo
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
const next = terms.escalationPolicy[level];
|
|
3009
|
+
if (next && next.delayMinutes != null && next.delayMinutes >= 0) {
|
|
3010
|
+
const runAt = new Date(at.getTime() + next.delayMinutes * MINUTE_MS);
|
|
3011
|
+
const meta = target.getMetadata();
|
|
3012
|
+
target.setMetadata({
|
|
3013
|
+
...meta,
|
|
3014
|
+
pendingEscalationLevel: next.level,
|
|
3015
|
+
pendingEscalationAt: runAt.toISOString()
|
|
3016
|
+
});
|
|
3017
|
+
await target.save();
|
|
3018
|
+
if (this.scheduleJobs) await this.jobs.enqueueJob({
|
|
3019
|
+
tenantId: target.tenantId ?? null,
|
|
3020
|
+
queue: SUPPORT_JOB_QUEUE,
|
|
3021
|
+
objectType: "SupportServiceTarget",
|
|
3022
|
+
objectId: this.requireId(target, "SupportServiceTarget"),
|
|
3023
|
+
method: "checkAndEscalate",
|
|
3024
|
+
args: {},
|
|
3025
|
+
runAt,
|
|
3026
|
+
priority: 75
|
|
3027
|
+
}, { tenantJobCap: 0 });
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Continue a delayed escalation on an already-breached clock: consumes the
|
|
3032
|
+
* pending-level marker and escalates the next policy step, unless the case
|
|
3033
|
+
* has since left its open states.
|
|
3034
|
+
*/
|
|
3035
|
+
async continueEscalation(target, at) {
|
|
3036
|
+
const meta = target.getMetadata();
|
|
3037
|
+
const pendingAt = parseIsoDate(meta.pendingEscalationAt);
|
|
3038
|
+
if (meta.pendingEscalationLevel == null || pendingAt === null) return false;
|
|
3039
|
+
if (at.getTime() < pendingAt.getTime()) return false;
|
|
3040
|
+
const { pendingEscalationLevel: _level, pendingEscalationAt: _at, ...rest } = meta;
|
|
3041
|
+
target.setMetadata(rest);
|
|
3042
|
+
await target.save();
|
|
3043
|
+
if (!(await this.caseService.getCase(target.caseId)).isOpen()) return false;
|
|
3044
|
+
await this.escalateForBreach(target, { at });
|
|
3045
|
+
return true;
|
|
3046
|
+
}
|
|
3047
|
+
};
|
|
3048
|
+
//#endregion
|
|
3049
|
+
//#region src/services/service-time-entry-service.ts
|
|
3050
|
+
var ServiceTimeEntryService = class ServiceTimeEntryService {
|
|
3051
|
+
entries;
|
|
3052
|
+
caseService;
|
|
3053
|
+
constructor(collections) {
|
|
3054
|
+
this.entries = collections.entries;
|
|
3055
|
+
this.caseService = collections.caseService;
|
|
3056
|
+
}
|
|
3057
|
+
static async create(options) {
|
|
3058
|
+
const caseService = options.caseService ?? await SupportCaseService.create(options);
|
|
3059
|
+
const entries = await ServiceTimeEntryCollection.create(options);
|
|
3060
|
+
return new ServiceTimeEntryService({
|
|
3061
|
+
entries,
|
|
3062
|
+
caseService
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
/** Load an entry or throw a descriptive error. */
|
|
3066
|
+
async getEntry(entryRef) {
|
|
3067
|
+
if (typeof entryRef !== "string") return entryRef;
|
|
3068
|
+
const found = await this.entries.get({ id: entryRef });
|
|
3069
|
+
if (!found) throw new Error(`ServiceTimeEntry not found: ${entryRef}`);
|
|
3070
|
+
return found;
|
|
3071
|
+
}
|
|
3072
|
+
/**
|
|
3073
|
+
* Record one Service Time Entry as a `draft`. One validation contract for
|
|
3074
|
+
* all four sources (FR-40):
|
|
3075
|
+
*
|
|
3076
|
+
* - a work context is required: `caseId` and/or the `workRefType`/`workRefId`
|
|
3077
|
+
* pair (a case-attached entry copies the case's tenant unless given);
|
|
3078
|
+
* - the participant must be coherent: `human` needs `participantProfileId`,
|
|
3079
|
+
* `agent` needs `agentRef`;
|
|
3080
|
+
* - `timer` entries need both period bounds; every entry needs a positive
|
|
3081
|
+
* duration — explicit `durationSeconds` or derived `endedAt − startedAt`.
|
|
3082
|
+
*
|
|
3083
|
+
* Case-attached entries append a `time_recorded` audit event.
|
|
3084
|
+
*/
|
|
3085
|
+
async record(input) {
|
|
3086
|
+
const hasWorkRef = Boolean(input.workRefType) || Boolean(input.workRefId);
|
|
3087
|
+
if (hasWorkRef && !(input.workRefType && input.workRefId)) throw new Error("ServiceTimeEntry: workRefType and workRefId must be provided together.");
|
|
3088
|
+
if (!input.caseId && !hasWorkRef) throw new Error("ServiceTimeEntry: a work context is required — provide caseId or the workRefType/workRefId pair.");
|
|
3089
|
+
if (input.participantKind === "human" && !input.participantProfileId) throw new Error("ServiceTimeEntry: participantKind 'human' requires participantProfileId (the delivering Participant).");
|
|
3090
|
+
if (input.participantKind === "agent" && !input.agentRef) throw new Error("ServiceTimeEntry: participantKind 'agent' requires agentRef (the delivering agent identity).");
|
|
3091
|
+
if (input.source === "timer" && !(input.startedAt && input.endedAt)) throw new Error("ServiceTimeEntry: source 'timer' requires both startedAt and endedAt.");
|
|
3092
|
+
if (input.startedAt && input.endedAt && input.startedAt.getTime() >= input.endedAt.getTime()) throw new Error("ServiceTimeEntry: startedAt must be before endedAt.");
|
|
3093
|
+
const durationSeconds = input.durationSeconds ?? (input.startedAt && input.endedAt ? Math.round((input.endedAt.getTime() - input.startedAt.getTime()) / 1e3) : void 0);
|
|
3094
|
+
if (durationSeconds === void 0) throw new Error("ServiceTimeEntry: a duration is required — provide durationSeconds or both startedAt and endedAt.");
|
|
3095
|
+
if (!(durationSeconds > 0)) throw new Error(`ServiceTimeEntry: durationSeconds must be greater than zero (got ${durationSeconds}).`);
|
|
3096
|
+
let tenantId = input.tenantId ?? null;
|
|
3097
|
+
let supportCase = null;
|
|
3098
|
+
if (input.caseId) {
|
|
3099
|
+
supportCase = await this.caseService.getCase(input.caseId);
|
|
3100
|
+
if (input.tenantId === void 0) tenantId = supportCase.tenantId;
|
|
3101
|
+
else if ((input.tenantId ?? null) !== supportCase.tenantId) throw new Error(`ServiceTimeEntry: tenantId '${input.tenantId}' does not match the case's tenant '${supportCase.tenantId}'.`);
|
|
3102
|
+
}
|
|
3103
|
+
const entry = await this.entries.create({
|
|
3104
|
+
tenantId,
|
|
3105
|
+
caseId: input.caseId ?? null,
|
|
3106
|
+
workRefType: input.workRefType ?? null,
|
|
3107
|
+
workRefId: input.workRefId ?? null,
|
|
3108
|
+
specialistId: input.specialistId ?? null,
|
|
3109
|
+
participantKind: input.participantKind,
|
|
3110
|
+
participantProfileId: input.participantProfileId ?? null,
|
|
3111
|
+
agentRef: input.agentRef ?? "",
|
|
3112
|
+
source: input.source,
|
|
3113
|
+
description: input.description ?? "",
|
|
3114
|
+
startedAt: input.startedAt ?? null,
|
|
3115
|
+
endedAt: input.endedAt ?? null,
|
|
3116
|
+
durationSeconds,
|
|
3117
|
+
evidence: JSON.stringify(input.evidence ?? []),
|
|
3118
|
+
status: "draft",
|
|
3119
|
+
metadata: JSON.stringify(input.metadata ?? {})
|
|
3120
|
+
});
|
|
3121
|
+
if (supportCase) await this.caseService.recordEvent(supportCase, "time_recorded", {
|
|
3122
|
+
actorKind: input.participantKind === "agent" ? "agent" : "specialist",
|
|
3123
|
+
actorProfileId: input.participantProfileId ?? null,
|
|
3124
|
+
summary: `Time recorded: ${(durationSeconds / 3600).toFixed(2)}h (${input.source})`,
|
|
3125
|
+
payload: {
|
|
3126
|
+
timeEntryId: entry.id,
|
|
3127
|
+
durationSeconds,
|
|
3128
|
+
source: input.source,
|
|
3129
|
+
participantKind: input.participantKind
|
|
3130
|
+
}
|
|
3131
|
+
});
|
|
3132
|
+
return entry;
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3135
|
+
* Submit a `draft` (or resubmit a `rejected`) entry for approval, stamping
|
|
3136
|
+
* the submitter. Approval itself is `TimeEntryApprovalService`'s job.
|
|
3137
|
+
*/
|
|
3138
|
+
async submit(entryRef, input = {}) {
|
|
3139
|
+
const entry = await this.getEntry(entryRef);
|
|
3140
|
+
if (entry.status !== "draft" && entry.status !== "rejected") throw new Error(`ServiceTimeEntry ${entry.id}: only 'draft' or 'rejected' entries can be submitted (status is '${entry.status}').`);
|
|
3141
|
+
entry.status = "submitted";
|
|
3142
|
+
entry.submittedAt = /* @__PURE__ */ new Date();
|
|
3143
|
+
entry.submittedByProfileId = input.byProfileId ?? null;
|
|
3144
|
+
await entry.save();
|
|
3145
|
+
return entry;
|
|
3146
|
+
}
|
|
3147
|
+
};
|
|
3148
|
+
//#endregion
|
|
3149
|
+
//#region src/services/support-ai-workflow.ts
|
|
3150
|
+
var HIGH_SEVERITY_MAX_RANK = 2;
|
|
3151
|
+
function severityRank(severityKey) {
|
|
3152
|
+
const match = /(\d+)\s*$/.exec((severityKey ?? "").trim());
|
|
3153
|
+
return match ? Number.parseInt(match[1] ?? "", 10) : NaN;
|
|
3154
|
+
}
|
|
3155
|
+
var SupportAiWorkflow = class SupportAiWorkflow {
|
|
3156
|
+
caseService;
|
|
3157
|
+
policies;
|
|
3158
|
+
aiRuns;
|
|
3159
|
+
interactions;
|
|
3160
|
+
handoffs;
|
|
3161
|
+
boundary;
|
|
3162
|
+
knowledge;
|
|
3163
|
+
onHandoff;
|
|
3164
|
+
constructor(deps) {
|
|
3165
|
+
this.caseService = deps.caseService;
|
|
3166
|
+
this.policies = deps.policies;
|
|
3167
|
+
this.aiRuns = deps.aiRuns;
|
|
3168
|
+
this.interactions = deps.interactions;
|
|
3169
|
+
this.handoffs = deps.handoffs;
|
|
3170
|
+
this.boundary = deps.boundary;
|
|
3171
|
+
this.knowledge = deps.knowledge;
|
|
3172
|
+
this.onHandoff = deps.onHandoff;
|
|
3173
|
+
}
|
|
3174
|
+
static async create(options) {
|
|
3175
|
+
const [caseService, policies, aiRuns, interactions, handoffs] = await Promise.all([
|
|
3176
|
+
SupportCaseService.create(options),
|
|
3177
|
+
SupportPolicyCollection.create(options),
|
|
3178
|
+
SupportAiRunCollection.create(options),
|
|
3179
|
+
SupportInteractionCollection.create(options),
|
|
3180
|
+
options.handoffService ? Promise.resolve(options.handoffService) : HumanHandoffService.create(options)
|
|
3181
|
+
]);
|
|
3182
|
+
return new SupportAiWorkflow({
|
|
3183
|
+
caseService,
|
|
3184
|
+
policies,
|
|
3185
|
+
aiRuns,
|
|
3186
|
+
interactions,
|
|
3187
|
+
handoffs,
|
|
3188
|
+
boundary: options.boundary ?? null,
|
|
3189
|
+
knowledge: options.knowledge ?? createNoopKnowledgeProvider(),
|
|
3190
|
+
onHandoff: options.onHandoff
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
/**
|
|
3194
|
+
* Entry point for `SupportIntakeService.onCaseIntake`: run one automated
|
|
3195
|
+
* pass over the case an inbound interaction created or joined.
|
|
3196
|
+
*/
|
|
3197
|
+
async processIntake(result) {
|
|
3198
|
+
const caseId = result.supportCase?.id;
|
|
3199
|
+
if (!caseId || result.outcome === "skipped") return [];
|
|
3200
|
+
return this.processCase(caseId, { interactionId: result.interaction?.id ?? null });
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Run one Automated Support Response pass over a case: acknowledge →
|
|
3204
|
+
* classify → answer → troubleshoot → resolve, each gated by the resolved
|
|
3205
|
+
* policy and audited as a {@link SupportAiRun}. Cases with `aiEnabled`
|
|
3206
|
+
* off, or no longer open, are left untouched.
|
|
3207
|
+
*/
|
|
3208
|
+
async processCase(caseId, opts = {}) {
|
|
3209
|
+
const supportCase = await this.caseService.getCase(caseId);
|
|
3210
|
+
if (supportCase.aiEnabled === false || !supportCase.isOpen()) return [];
|
|
3211
|
+
const policy = await this.resolveEffectivePolicy(supportCase);
|
|
3212
|
+
const boundary = this.boundary ?? createDefaultAiBoundary(() => supportCase);
|
|
3213
|
+
const interactionId = opts.interactionId ?? null;
|
|
3214
|
+
const body = await this.requestBodyOf(supportCase, interactionId);
|
|
3215
|
+
const runs = [];
|
|
3216
|
+
runs.push(await this.runAcknowledge(supportCase, policy, interactionId));
|
|
3217
|
+
if (!policy.autoClassify) runs.push(await this.writeRun(supportCase, {
|
|
3218
|
+
phase: "classify",
|
|
3219
|
+
outcome: "skipped",
|
|
3220
|
+
interactionId,
|
|
3221
|
+
metadata: { reason: "policy-disabled" }
|
|
3222
|
+
}));
|
|
3223
|
+
else {
|
|
3224
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
3225
|
+
let classification;
|
|
3226
|
+
try {
|
|
3227
|
+
classification = await boundary.classify({
|
|
3228
|
+
subject: supportCase.subject,
|
|
3229
|
+
body,
|
|
3230
|
+
severityKeys: this.severityKeysOf(supportCase),
|
|
3231
|
+
sensitiveCategories: policy.sensitiveCategories
|
|
3232
|
+
});
|
|
3233
|
+
} catch (error) {
|
|
3234
|
+
runs.push(await this.writeRun(supportCase, {
|
|
3235
|
+
phase: "classify",
|
|
3236
|
+
outcome: "failed",
|
|
3237
|
+
interactionId,
|
|
3238
|
+
startedAt,
|
|
3239
|
+
error: errorMessage(error)
|
|
3240
|
+
}));
|
|
3241
|
+
await this.triggerHandoff(supportCase, "failed_resolution", `AI classification failed: ${errorMessage(error)}`);
|
|
3242
|
+
return runs;
|
|
3243
|
+
}
|
|
3244
|
+
const classifiedSensitive = classification.sensitive || classification.category !== "" && policy.sensitiveCategories.includes(classification.category);
|
|
3245
|
+
let caseDirty = false;
|
|
3246
|
+
if (!supportCase.severity && classification.severity) {
|
|
3247
|
+
supportCase.severity = classification.severity;
|
|
3248
|
+
caseDirty = true;
|
|
3249
|
+
}
|
|
3250
|
+
if (!supportCase.category && classification.category) {
|
|
3251
|
+
supportCase.category = classification.category;
|
|
3252
|
+
caseDirty = true;
|
|
3253
|
+
}
|
|
3254
|
+
if (!supportCase.sensitive && classifiedSensitive) {
|
|
3255
|
+
supportCase.sensitive = true;
|
|
3256
|
+
caseDirty = true;
|
|
3257
|
+
}
|
|
3258
|
+
if (caseDirty) await supportCase.save();
|
|
3259
|
+
runs.push(await this.writeRun(supportCase, {
|
|
3260
|
+
phase: "classify",
|
|
3261
|
+
outcome: "completed",
|
|
3262
|
+
interactionId,
|
|
3263
|
+
startedAt,
|
|
3264
|
+
confidence: classification.confidence,
|
|
3265
|
+
classification: {
|
|
3266
|
+
severity: classification.severity,
|
|
3267
|
+
category: classification.category,
|
|
3268
|
+
sensitive: classifiedSensitive,
|
|
3269
|
+
confidence: classification.confidence
|
|
3270
|
+
},
|
|
3271
|
+
model: classification.model
|
|
3272
|
+
}));
|
|
3273
|
+
}
|
|
3274
|
+
const sensitive = supportCase.sensitive;
|
|
3275
|
+
if (sensitive) await this.triggerHandoff(supportCase, "sensitive", "Case involves a sensitive matter");
|
|
3276
|
+
const rank = severityRank(supportCase.severity);
|
|
3277
|
+
if (Number.isFinite(rank) && rank <= HIGH_SEVERITY_MAX_RANK) await this.triggerHandoff(supportCase, "high_severity", `Severity ${supportCase.severity} requires a Support Specialist`);
|
|
3278
|
+
let answer = null;
|
|
3279
|
+
let answerPosted = false;
|
|
3280
|
+
if (!policy.autoAnswer) runs.push(await this.writeRun(supportCase, {
|
|
3281
|
+
phase: "answer",
|
|
3282
|
+
outcome: "skipped",
|
|
3283
|
+
interactionId,
|
|
3284
|
+
metadata: { reason: "policy-disabled" }
|
|
3285
|
+
}));
|
|
3286
|
+
else if (sensitive) runs.push(await this.writeRun(supportCase, {
|
|
3287
|
+
phase: "answer",
|
|
3288
|
+
outcome: "handed_off",
|
|
3289
|
+
interactionId,
|
|
3290
|
+
metadata: { reason: "sensitive" }
|
|
3291
|
+
}));
|
|
3292
|
+
else if (supportCase.humanRequestedAt) {
|
|
3293
|
+
runs.push(await this.writeRun(supportCase, {
|
|
3294
|
+
phase: "answer",
|
|
3295
|
+
outcome: "handed_off",
|
|
3296
|
+
interactionId,
|
|
3297
|
+
metadata: { reason: "human-requested" }
|
|
3298
|
+
}));
|
|
3299
|
+
await this.triggerHandoff(supportCase, "client_request", "Client explicitly requested a human");
|
|
3300
|
+
} else {
|
|
3301
|
+
const priorAttempts = await this.countAnswerAttempts(caseId);
|
|
3302
|
+
if (priorAttempts >= policy.maxAutoAttempts) {
|
|
3303
|
+
runs.push(await this.writeRun(supportCase, {
|
|
3304
|
+
phase: "answer",
|
|
3305
|
+
outcome: "skipped",
|
|
3306
|
+
interactionId,
|
|
3307
|
+
metadata: {
|
|
3308
|
+
reason: "max-attempts",
|
|
3309
|
+
attempts: priorAttempts,
|
|
3310
|
+
maxAutoAttempts: policy.maxAutoAttempts
|
|
3311
|
+
}
|
|
3312
|
+
}));
|
|
3313
|
+
await this.triggerHandoff(supportCase, "policy", `Automated answer attempts exhausted (${priorAttempts}/${policy.maxAutoAttempts})`);
|
|
3314
|
+
} else {
|
|
3315
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
3316
|
+
let snippets = [];
|
|
3317
|
+
try {
|
|
3318
|
+
snippets = await this.knowledge.retrieve({
|
|
3319
|
+
subject: supportCase.subject,
|
|
3320
|
+
body,
|
|
3321
|
+
projectId: supportCase.projectId,
|
|
3322
|
+
tenantId: supportCase.tenantId
|
|
3323
|
+
});
|
|
3324
|
+
answer = await boundary.answer({
|
|
3325
|
+
subject: supportCase.subject,
|
|
3326
|
+
body,
|
|
3327
|
+
knowledge: snippets,
|
|
3328
|
+
caseSummary: this.caseSummaryOf(supportCase)
|
|
3329
|
+
});
|
|
3330
|
+
} catch (error) {
|
|
3331
|
+
runs.push(await this.writeRun(supportCase, {
|
|
3332
|
+
phase: "answer",
|
|
3333
|
+
outcome: "failed",
|
|
3334
|
+
interactionId,
|
|
3335
|
+
startedAt,
|
|
3336
|
+
knowledgeRefs: snippets,
|
|
3337
|
+
error: errorMessage(error)
|
|
3338
|
+
}));
|
|
3339
|
+
await this.triggerHandoff(supportCase, "failed_resolution", `AI answer failed: ${errorMessage(error)}`);
|
|
3340
|
+
return runs;
|
|
3341
|
+
}
|
|
3342
|
+
if (answer.confidence >= policy.confidenceThreshold) {
|
|
3343
|
+
const attemptNumber = priorAttempts + 1;
|
|
3344
|
+
const draft = supportCase.channelKind === "email" && !policy.autoSendEmailReplies;
|
|
3345
|
+
const interaction = await this.caseService.recordInteraction(supportCase, {
|
|
3346
|
+
direction: "outbound",
|
|
3347
|
+
channelKind: supportCase.channelKind || "chat",
|
|
3348
|
+
actorKind: "agent",
|
|
3349
|
+
body: answer.reply,
|
|
3350
|
+
sourceKey: `ai:answer:${caseId}:${attemptNumber}`,
|
|
3351
|
+
metadata: { draft }
|
|
3352
|
+
});
|
|
3353
|
+
runs.push(await this.writeRun(supportCase, {
|
|
3354
|
+
phase: "answer",
|
|
3355
|
+
outcome: "completed",
|
|
3356
|
+
interactionId,
|
|
3357
|
+
startedAt,
|
|
3358
|
+
confidence: answer.confidence,
|
|
3359
|
+
knowledgeRefs: snippets,
|
|
3360
|
+
responseInteractionId: interaction.id ?? null,
|
|
3361
|
+
model: answer.model,
|
|
3362
|
+
metadata: {
|
|
3363
|
+
attempt: attemptNumber,
|
|
3364
|
+
draft
|
|
3365
|
+
}
|
|
3366
|
+
}));
|
|
3367
|
+
answerPosted = !draft;
|
|
3368
|
+
} else {
|
|
3369
|
+
runs.push(await this.writeRun(supportCase, {
|
|
3370
|
+
phase: "answer",
|
|
3371
|
+
outcome: "handed_off",
|
|
3372
|
+
interactionId,
|
|
3373
|
+
startedAt,
|
|
3374
|
+
confidence: answer.confidence,
|
|
3375
|
+
knowledgeRefs: snippets,
|
|
3376
|
+
model: answer.model,
|
|
3377
|
+
metadata: {
|
|
3378
|
+
reason: "low-confidence",
|
|
3379
|
+
unsentReply: answer.reply
|
|
3380
|
+
}
|
|
3381
|
+
}));
|
|
3382
|
+
await this.triggerHandoff(supportCase, "low_confidence", `Answer confidence ${answer.confidence} below threshold ${policy.confidenceThreshold}`);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
runs.push(await this.writeRun(supportCase, {
|
|
3387
|
+
phase: "troubleshoot",
|
|
3388
|
+
outcome: "skipped",
|
|
3389
|
+
interactionId,
|
|
3390
|
+
metadata: { reason: policy.autoTroubleshoot ? "not-implemented" : "policy-disabled" }
|
|
3391
|
+
}));
|
|
3392
|
+
const skipReason = this.resolveSkipReason(supportCase, policy, {
|
|
3393
|
+
answer,
|
|
3394
|
+
answerPosted,
|
|
3395
|
+
sensitive
|
|
3396
|
+
});
|
|
3397
|
+
if (skipReason !== null || !answer) runs.push(await this.writeRun(supportCase, {
|
|
3398
|
+
phase: "resolve",
|
|
3399
|
+
outcome: "skipped",
|
|
3400
|
+
interactionId,
|
|
3401
|
+
metadata: { reason: skipReason ?? "no-answer" }
|
|
3402
|
+
}));
|
|
3403
|
+
else {
|
|
3404
|
+
await this.caseService.resolve(supportCase, {
|
|
3405
|
+
actorKind: "agent",
|
|
3406
|
+
summary: answer.reply,
|
|
3407
|
+
resolutionKind: "automated"
|
|
3408
|
+
});
|
|
3409
|
+
runs.push(await this.writeRun(supportCase, {
|
|
3410
|
+
phase: "resolve",
|
|
3411
|
+
outcome: "completed",
|
|
3412
|
+
interactionId,
|
|
3413
|
+
confidence: answer.confidence
|
|
3414
|
+
}));
|
|
3415
|
+
}
|
|
3416
|
+
return runs;
|
|
3417
|
+
}
|
|
3418
|
+
/** Acknowledge phase: post the templated receipt once per case. */
|
|
3419
|
+
async runAcknowledge(supportCase, policy, interactionId) {
|
|
3420
|
+
if (!policy.autoAcknowledge) return this.writeRun(supportCase, {
|
|
3421
|
+
phase: "acknowledge",
|
|
3422
|
+
outcome: "skipped",
|
|
3423
|
+
interactionId,
|
|
3424
|
+
metadata: { reason: "policy-disabled" }
|
|
3425
|
+
});
|
|
3426
|
+
if (supportCase.acknowledgedAt) return this.writeRun(supportCase, {
|
|
3427
|
+
phase: "acknowledge",
|
|
3428
|
+
outcome: "skipped",
|
|
3429
|
+
interactionId,
|
|
3430
|
+
metadata: { reason: "already-acknowledged" }
|
|
3431
|
+
});
|
|
3432
|
+
const caseId = this.caseIdOf(supportCase);
|
|
3433
|
+
const draft = supportCase.channelKind === "email" && !policy.autoSendEmailReplies;
|
|
3434
|
+
const interaction = await this.caseService.recordInteraction(supportCase, {
|
|
3435
|
+
direction: "outbound",
|
|
3436
|
+
channelKind: supportCase.channelKind || "chat",
|
|
3437
|
+
actorKind: "agent",
|
|
3438
|
+
body: `Thanks for reaching out \u2014 we've opened case ${supportCase.caseNumber} and are looking into it now.`,
|
|
3439
|
+
sourceKey: `ai:ack:${caseId}`,
|
|
3440
|
+
metadata: {
|
|
3441
|
+
acknowledgement: true,
|
|
3442
|
+
draft
|
|
3443
|
+
}
|
|
3444
|
+
});
|
|
3445
|
+
return this.writeRun(supportCase, {
|
|
3446
|
+
phase: "acknowledge",
|
|
3447
|
+
outcome: "completed",
|
|
3448
|
+
interactionId,
|
|
3449
|
+
responseInteractionId: interaction.id ?? null
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
/** Why autonomous resolution must not run, or `null` when it may. */
|
|
3453
|
+
resolveSkipReason(supportCase, policy, outcome) {
|
|
3454
|
+
if (!policy.autoResolve) return "policy-disabled";
|
|
3455
|
+
const caseRank = severityRank(supportCase.severity);
|
|
3456
|
+
const maxRank = severityRank(policy.autoResolveMaxSeverity);
|
|
3457
|
+
if (!Number.isFinite(caseRank) || !Number.isFinite(maxRank) || caseRank < maxRank) return "severity-ineligible";
|
|
3458
|
+
if (!outcome.answerPosted || !outcome.answer) return "no-answer";
|
|
3459
|
+
if (outcome.answer.confidence < policy.confidenceThreshold) return "low-confidence";
|
|
3460
|
+
if (!outcome.answer.proposedResolution) return "no-proposed-resolution";
|
|
3461
|
+
if (outcome.sensitive) return "sensitive";
|
|
3462
|
+
if (supportCase.humanRequestedAt) return "human-requested";
|
|
3463
|
+
return null;
|
|
3464
|
+
}
|
|
3465
|
+
/** Resolve the governing policy: matching row, else built-in defaults. */
|
|
3466
|
+
async resolveEffectivePolicy(supportCase) {
|
|
3467
|
+
const policy = await this.policies.resolveForScope({
|
|
3468
|
+
planId: supportCase.planId,
|
|
3469
|
+
projectId: supportCase.projectId
|
|
3470
|
+
});
|
|
3471
|
+
if (!policy) return {
|
|
3472
|
+
policyId: null,
|
|
3473
|
+
...DEFAULT_SUPPORT_POLICY,
|
|
3474
|
+
sensitiveCategories: [...DEFAULT_SUPPORT_POLICY.sensitiveCategories],
|
|
3475
|
+
allowedTools: [...DEFAULT_SUPPORT_POLICY.allowedTools]
|
|
3476
|
+
};
|
|
3477
|
+
return {
|
|
3478
|
+
policyId: policy.id ?? null,
|
|
3479
|
+
autoAcknowledge: policy.autoAcknowledge,
|
|
3480
|
+
autoClassify: policy.autoClassify,
|
|
3481
|
+
autoAnswer: policy.autoAnswer,
|
|
3482
|
+
autoTroubleshoot: policy.autoTroubleshoot,
|
|
3483
|
+
autoResolve: policy.autoResolve,
|
|
3484
|
+
autoResolveMaxSeverity: policy.autoResolveMaxSeverity,
|
|
3485
|
+
confidenceThreshold: policy.confidenceThreshold,
|
|
3486
|
+
maxAutoAttempts: policy.maxAutoAttempts,
|
|
3487
|
+
autoSendEmailReplies: policy.autoSendEmailReplies,
|
|
3488
|
+
sensitiveCategories: policy.getSensitiveCategories(),
|
|
3489
|
+
allowedTools: policy.getAllowedTools()
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
/**
|
|
3493
|
+
* Prior boundary-consuming answer attempts. Low-confidence answers consume
|
|
3494
|
+
* the boundary too (`handed_off`), so they count toward the attempt budget
|
|
3495
|
+
* — only `skipped` runs (which never called the boundary) are free.
|
|
3496
|
+
*/
|
|
3497
|
+
async countAnswerAttempts(caseId) {
|
|
3498
|
+
return (await this.aiRuns.forCase(caseId)).filter((run) => run.phase === "answer" && run.outcome !== "skipped").length;
|
|
3499
|
+
}
|
|
3500
|
+
/** The request text a pass reasons over: triggering interaction, else the
|
|
3501
|
+
* case description. */
|
|
3502
|
+
async requestBodyOf(supportCase, interactionId) {
|
|
3503
|
+
if (interactionId) {
|
|
3504
|
+
const interaction = await this.interactions.get({ id: interactionId });
|
|
3505
|
+
if (interaction?.body) return interaction.body;
|
|
3506
|
+
}
|
|
3507
|
+
return supportCase.description;
|
|
3508
|
+
}
|
|
3509
|
+
/** Severity vocabulary: the case's plan snapshot, else the defaults. */
|
|
3510
|
+
severityKeysOf(supportCase) {
|
|
3511
|
+
const defs = supportCase.getPlanSnapshot().severityDefinitions;
|
|
3512
|
+
if (defs && typeof defs === "object" && !Array.isArray(defs)) {
|
|
3513
|
+
const keys = Object.keys(defs);
|
|
3514
|
+
if (keys.length > 0) return keys;
|
|
3515
|
+
}
|
|
3516
|
+
return Object.keys(DEFAULT_SEVERITY_DEFINITIONS);
|
|
3517
|
+
}
|
|
3518
|
+
/** One-line case summary handed to the answer boundary. */
|
|
3519
|
+
caseSummaryOf(supportCase) {
|
|
3520
|
+
const parts = [`Case ${supportCase.caseNumber}`, `status ${supportCase.status}`];
|
|
3521
|
+
if (supportCase.severity) parts.push(`severity ${supportCase.severity}`);
|
|
3522
|
+
if (supportCase.category) parts.push(`category ${supportCase.category}`);
|
|
3523
|
+
return `${parts.join(", ")} \u2014 ${supportCase.subject}`;
|
|
3524
|
+
}
|
|
3525
|
+
/** Route a trigger through the handoff engine and notify the app seam. */
|
|
3526
|
+
async triggerHandoff(supportCase, trigger, note) {
|
|
3527
|
+
const { alreadyActive } = await this.handoffs.handoff(supportCase, {
|
|
3528
|
+
trigger,
|
|
3529
|
+
note
|
|
3530
|
+
});
|
|
3531
|
+
if (this.onHandoff) await this.onHandoff({
|
|
3532
|
+
supportCase,
|
|
3533
|
+
trigger,
|
|
3534
|
+
alreadyActive
|
|
3535
|
+
});
|
|
3536
|
+
}
|
|
3537
|
+
/** Append one audit run plus its `ai_run` case event. */
|
|
3538
|
+
async writeRun(supportCase, input) {
|
|
3539
|
+
const run = await this.aiRuns.create({
|
|
3540
|
+
tenantId: supportCase.tenantId,
|
|
3541
|
+
caseId: this.caseIdOf(supportCase),
|
|
3542
|
+
interactionId: input.interactionId ?? null,
|
|
3543
|
+
phase: input.phase,
|
|
3544
|
+
outcome: input.outcome,
|
|
3545
|
+
confidence: input.confidence ?? null,
|
|
3546
|
+
classification: JSON.stringify(input.classification ?? {}),
|
|
3547
|
+
responseInteractionId: input.responseInteractionId ?? null,
|
|
3548
|
+
knowledgeRefs: JSON.stringify(input.knowledgeRefs ?? []),
|
|
3549
|
+
error: input.error ?? "",
|
|
3550
|
+
correlationId: crypto.randomUUID(),
|
|
3551
|
+
model: input.model ?? "",
|
|
3552
|
+
startedAt: input.startedAt ?? /* @__PURE__ */ new Date(),
|
|
3553
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
3554
|
+
metadata: JSON.stringify(input.metadata ?? {})
|
|
3555
|
+
});
|
|
3556
|
+
await this.caseService.recordEvent(supportCase, "ai_run", {
|
|
3557
|
+
actorKind: "agent",
|
|
3558
|
+
summary: `AI ${input.phase} \u2192 ${input.outcome}`,
|
|
3559
|
+
payload: {
|
|
3560
|
+
runId: run.id,
|
|
3561
|
+
phase: input.phase,
|
|
3562
|
+
outcome: input.outcome,
|
|
3563
|
+
confidence: input.confidence ?? null
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
return run;
|
|
3567
|
+
}
|
|
3568
|
+
/** The persisted id of a saved case. */
|
|
3569
|
+
caseIdOf(supportCase) {
|
|
3570
|
+
if (!supportCase.id) throw new Error("SupportCase has no id — was it saved?");
|
|
3571
|
+
return supportCase.id;
|
|
3572
|
+
}
|
|
3573
|
+
};
|
|
3574
|
+
function createNoopKnowledgeProvider() {
|
|
3575
|
+
return { retrieve: () => Promise.resolve([]) };
|
|
3576
|
+
}
|
|
3577
|
+
function createDefaultAiBoundary(getCase) {
|
|
3578
|
+
return {
|
|
3579
|
+
async classify(input) {
|
|
3580
|
+
const parsed = extractJsonObject(String(await getCase().do(buildClassifyInstructions(input))));
|
|
3581
|
+
if (!parsed) return {
|
|
3582
|
+
severity: "",
|
|
3583
|
+
category: "",
|
|
3584
|
+
sensitive: true,
|
|
3585
|
+
confidence: 0
|
|
3586
|
+
};
|
|
3587
|
+
return {
|
|
3588
|
+
severity: typeof parsed.severity === "string" ? parsed.severity : "",
|
|
3589
|
+
category: typeof parsed.category === "string" ? parsed.category : "",
|
|
3590
|
+
sensitive: parsed.sensitive === true,
|
|
3591
|
+
confidence: clampConfidence(parsed.confidence)
|
|
3592
|
+
};
|
|
3593
|
+
},
|
|
3594
|
+
async answer(input) {
|
|
3595
|
+
const parsed = extractJsonObject(String(await getCase().do(buildAnswerInstructions(input))));
|
|
3596
|
+
if (!parsed || typeof parsed.reply !== "string" || !parsed.reply.trim()) return {
|
|
3597
|
+
reply: "",
|
|
3598
|
+
confidence: 0,
|
|
3599
|
+
proposedResolution: false
|
|
3600
|
+
};
|
|
3601
|
+
return {
|
|
3602
|
+
reply: parsed.reply,
|
|
3603
|
+
confidence: clampConfidence(parsed.confidence),
|
|
3604
|
+
proposedResolution: parsed.proposedResolution === true
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
function buildClassifyInstructions(input) {
|
|
3610
|
+
const sensitiveList = input.sensitiveCategories.length > 0 ? input.sensitiveCategories.join(", ") : "(none configured)";
|
|
3611
|
+
return [
|
|
3612
|
+
"You are triaging a client support request.",
|
|
3613
|
+
`Severity keys, most severe first: ${input.severityKeys.join(", ")}.`,
|
|
3614
|
+
`Sensitive categories: ${sensitiveList}.`,
|
|
3615
|
+
`Subject: ${input.subject}`,
|
|
3616
|
+
"Request:",
|
|
3617
|
+
input.body,
|
|
3618
|
+
"",
|
|
3619
|
+
"Respond with ONLY a strict JSON object:",
|
|
3620
|
+
"{\"severity\": \"<one severity key>\", \"category\": \"<short-kebab-case>\",",
|
|
3621
|
+
"\"sensitive\": <true when the matter touches a sensitive category, legal",
|
|
3622
|
+
"or security exposure, or personal data>, \"confidence\": <number 0..1>}"
|
|
3623
|
+
].join("\n");
|
|
3624
|
+
}
|
|
3625
|
+
function buildAnswerInstructions(input) {
|
|
3626
|
+
const knowledgeBlock = input.knowledge.length > 0 ? input.knowledge.map((snippet) => `- [${snippet.kind}:${snippet.ref}]${snippet.label ? ` ${snippet.label}:` : ""} ${snippet.content}`).join("\n") : "(no knowledge available)";
|
|
3627
|
+
return [
|
|
3628
|
+
"You are drafting a support reply grounded ONLY in the knowledge below.",
|
|
3629
|
+
`Context: ${input.caseSummary}`,
|
|
3630
|
+
`Subject: ${input.subject}`,
|
|
3631
|
+
"Request:",
|
|
3632
|
+
input.body,
|
|
3633
|
+
"Knowledge:",
|
|
3634
|
+
knowledgeBlock,
|
|
3635
|
+
"",
|
|
3636
|
+
"Respond with ONLY a strict JSON object:",
|
|
3637
|
+
"{\"reply\": \"<the reply to send>\", \"confidence\": <number 0..1>,",
|
|
3638
|
+
"\"proposedResolution\": <true when the reply fully resolves the request>}"
|
|
3639
|
+
].join("\n");
|
|
3640
|
+
}
|
|
3641
|
+
function extractJsonObject(raw) {
|
|
3642
|
+
const text = raw.trim();
|
|
3643
|
+
const candidates = [text];
|
|
3644
|
+
const embedded = /\{[\s\S]*\}/.exec(text);
|
|
3645
|
+
if (embedded && embedded[0] !== text) candidates.push(embedded[0]);
|
|
3646
|
+
for (const candidate of candidates) try {
|
|
3647
|
+
const parsed = JSON.parse(candidate);
|
|
3648
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
3649
|
+
} catch {}
|
|
3650
|
+
return null;
|
|
3651
|
+
}
|
|
3652
|
+
function clampConfidence(value) {
|
|
3653
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
3654
|
+
if (!Number.isFinite(num)) return 0;
|
|
3655
|
+
return Math.min(1, Math.max(0, num));
|
|
3656
|
+
}
|
|
3657
|
+
function errorMessage(error) {
|
|
3658
|
+
return error instanceof Error ? error.message : String(error);
|
|
3659
|
+
}
|
|
3660
|
+
//#endregion
|
|
3661
|
+
//#region src/services/support-intake-service.ts
|
|
3662
|
+
var CHAT_MESSAGE_SOURCE_TYPE = "@happyvertical/smrt-chat:ChatMessage";
|
|
3663
|
+
var CHAT_ROOM_TARGET_TYPE = "@happyvertical/smrt-chat:ChatRoom";
|
|
3664
|
+
var EMAIL_SOURCE_TYPE = "@happyvertical/smrt-messages:Email";
|
|
3665
|
+
var EMAIL_ACCOUNT_TARGET_TYPE = "@happyvertical/smrt-messages:EmailAccount";
|
|
3666
|
+
var SupportIntakeService = class SupportIntakeService {
|
|
3667
|
+
caseService;
|
|
3668
|
+
bindings;
|
|
3669
|
+
options;
|
|
3670
|
+
conversationMutex = new KeyedMutex();
|
|
3671
|
+
constructor(caseService, bindings, options) {
|
|
3672
|
+
this.caseService = caseService;
|
|
3673
|
+
this.bindings = bindings;
|
|
3674
|
+
this.options = options;
|
|
3675
|
+
}
|
|
3676
|
+
static async create(options) {
|
|
3677
|
+
const [caseService, bindings] = await Promise.all([SupportCaseService.create(options), SupportChannelBindingCollection.create(options)]);
|
|
3678
|
+
return new SupportIntakeService(caseService, bindings, options);
|
|
3679
|
+
}
|
|
3680
|
+
/** Conversation key for a chat room/thread. */
|
|
3681
|
+
static chatThreadKey(roomId, threadId) {
|
|
3682
|
+
return threadId ? `chat:${roomId}:${threadId}` : `chat:${roomId}`;
|
|
3683
|
+
}
|
|
3684
|
+
/** Idempotency key for a chat message. */
|
|
3685
|
+
static chatSourceKey(messageId) {
|
|
3686
|
+
return `chat:${messageId}`;
|
|
3687
|
+
}
|
|
3688
|
+
/** Idempotency key for an email. */
|
|
3689
|
+
static emailSourceKey(emailId) {
|
|
3690
|
+
return `email:${emailId}`;
|
|
3691
|
+
}
|
|
3692
|
+
/**
|
|
3693
|
+
* Ingest one inbound chat message from a bound room: create-or-join the
|
|
3694
|
+
* conversation's case and record the interaction.
|
|
3695
|
+
*/
|
|
3696
|
+
async ingestChatMessage(input) {
|
|
3697
|
+
if (input.role && input.role !== "user") return {
|
|
3698
|
+
outcome: "skipped",
|
|
3699
|
+
reason: `role:${input.role}`
|
|
3700
|
+
};
|
|
3701
|
+
const binding = await this.bindings.findForTarget(CHAT_ROOM_TARGET_TYPE, input.roomId);
|
|
3702
|
+
if (!binding) return {
|
|
3703
|
+
outcome: "skipped",
|
|
3704
|
+
reason: "no-binding"
|
|
3705
|
+
};
|
|
3706
|
+
const threadKey = SupportIntakeService.chatThreadKey(input.roomId, input.threadId);
|
|
3707
|
+
const sourceKey = SupportIntakeService.chatSourceKey(input.messageId);
|
|
3708
|
+
const clientProfileId = binding.clientProfileId ?? input.senderProfileId;
|
|
3709
|
+
return this.createOrJoin({
|
|
3710
|
+
binding,
|
|
3711
|
+
threadKey,
|
|
3712
|
+
sourceKey,
|
|
3713
|
+
tenantId: input.tenantId ?? binding.tenantId ?? null,
|
|
3714
|
+
subject: truncateSubject(input.content),
|
|
3715
|
+
body: input.content,
|
|
3716
|
+
clientProfileId,
|
|
3717
|
+
openedByProfileId: input.senderProfileId,
|
|
3718
|
+
authorProfileId: input.senderProfileId,
|
|
3719
|
+
occurredAt: input.occurredAt,
|
|
3720
|
+
sourceType: CHAT_MESSAGE_SOURCE_TYPE,
|
|
3721
|
+
sourceId: input.messageId,
|
|
3722
|
+
interactionMetadata: {
|
|
3723
|
+
roomId: input.roomId,
|
|
3724
|
+
threadId: input.threadId ?? null
|
|
3725
|
+
}
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* Ingest one inbound email from a bound account: joins the RFC thread's
|
|
3730
|
+
* case when `inReplyTo` references a known interaction, else creates a new
|
|
3731
|
+
* case keyed by the message id. Mail sent FROM the binding's own addresses
|
|
3732
|
+
* is skipped (our outbound side of the conversation).
|
|
3733
|
+
*/
|
|
3734
|
+
async ingestEmail(input) {
|
|
3735
|
+
const binding = await this.bindings.findForTarget(EMAIL_ACCOUNT_TARGET_TYPE, input.accountId);
|
|
3736
|
+
if (!binding) return {
|
|
3737
|
+
outcome: "skipped",
|
|
3738
|
+
reason: "no-binding"
|
|
3739
|
+
};
|
|
3740
|
+
const from = input.fromAddress.trim().toLowerCase();
|
|
3741
|
+
if (binding.getSelfAddresses().includes(from)) return {
|
|
3742
|
+
outcome: "skipped",
|
|
3743
|
+
reason: "self-address"
|
|
3744
|
+
};
|
|
3745
|
+
const sourceKey = SupportIntakeService.emailSourceKey(input.emailId);
|
|
3746
|
+
let threadKey = null;
|
|
3747
|
+
if (input.inReplyTo) {
|
|
3748
|
+
const parent = await this.findInteractionByRfcMessageId(input.inReplyTo);
|
|
3749
|
+
if (parent) {
|
|
3750
|
+
const parentCase = await this.caseService.cases.get({ id: parent.caseId });
|
|
3751
|
+
if (parentCase) threadKey = parentCase.threadKey;
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
if (!threadKey) threadKey = `email:${input.rfcMessageId || input.emailId}`;
|
|
3755
|
+
let clientProfileId = binding.clientProfileId ?? null;
|
|
3756
|
+
let unresolvedClient = false;
|
|
3757
|
+
if (!clientProfileId && this.options.resolveClientProfileId) clientProfileId = await this.options.resolveClientProfileId({
|
|
3758
|
+
email: input.fromAddress,
|
|
3759
|
+
name: input.fromName,
|
|
3760
|
+
tenantId: input.tenantId ?? binding.tenantId ?? null
|
|
3761
|
+
});
|
|
3762
|
+
if (!clientProfileId) unresolvedClient = true;
|
|
3763
|
+
return this.createOrJoin({
|
|
3764
|
+
binding,
|
|
3765
|
+
threadKey,
|
|
3766
|
+
sourceKey,
|
|
3767
|
+
tenantId: input.tenantId ?? binding.tenantId ?? null,
|
|
3768
|
+
subject: input.subject?.trim() || truncateSubject(input.body),
|
|
3769
|
+
body: input.body,
|
|
3770
|
+
clientProfileId,
|
|
3771
|
+
openedByProfileId: clientProfileId,
|
|
3772
|
+
authorProfileId: clientProfileId,
|
|
3773
|
+
occurredAt: input.occurredAt,
|
|
3774
|
+
sourceType: EMAIL_SOURCE_TYPE,
|
|
3775
|
+
sourceId: input.emailId,
|
|
3776
|
+
rfcMessageId: input.rfcMessageId ?? null,
|
|
3777
|
+
caseMetadata: unresolvedClient ? {
|
|
3778
|
+
unresolvedClient: true,
|
|
3779
|
+
fromAddress: input.fromAddress
|
|
3780
|
+
} : void 0,
|
|
3781
|
+
interactionMetadata: {
|
|
3782
|
+
fromAddress: input.fromAddress,
|
|
3783
|
+
fromName: input.fromName ?? null,
|
|
3784
|
+
rfcMessageId: input.rfcMessageId ?? null,
|
|
3785
|
+
inReplyTo: input.inReplyTo ?? null
|
|
3786
|
+
}
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
/** The shared create-or-join core (deterministic per conversation key). */
|
|
3790
|
+
async createOrJoin(input) {
|
|
3791
|
+
const conversationKey = `${input.binding.id ?? ""}:${input.threadKey}`;
|
|
3792
|
+
return this.conversationMutex.run(conversationKey, () => this.createOrJoinExclusive(input));
|
|
3793
|
+
}
|
|
3794
|
+
async createOrJoinExclusive(input) {
|
|
3795
|
+
const { binding } = input;
|
|
3796
|
+
const existing = await this.caseService.interactions.bySourceKey(input.sourceKey);
|
|
3797
|
+
if (existing) return {
|
|
3798
|
+
outcome: "joined",
|
|
3799
|
+
reason: "duplicate-source",
|
|
3800
|
+
supportCase: await this.caseService.cases.get({ id: existing.caseId }) ?? void 0,
|
|
3801
|
+
interaction: existing,
|
|
3802
|
+
binding
|
|
3803
|
+
};
|
|
3804
|
+
let outcome;
|
|
3805
|
+
let supportCase = await this.caseService.cases.findOpenByThreadKey(input.threadKey, { bindingId: binding.id });
|
|
3806
|
+
if (supportCase) outcome = "joined";
|
|
3807
|
+
else {
|
|
3808
|
+
const resolved = await this.findResolvedByThreadKey(input.threadKey, binding.id ?? null);
|
|
3809
|
+
if (resolved) {
|
|
3810
|
+
supportCase = await this.caseService.reopen(resolved, {
|
|
3811
|
+
actorKind: "client",
|
|
3812
|
+
actorProfileId: input.authorProfileId,
|
|
3813
|
+
reason: "new inbound interaction on resolved case"
|
|
3814
|
+
});
|
|
3815
|
+
outcome = "reopened";
|
|
3816
|
+
} else {
|
|
3817
|
+
supportCase = await this.caseService.openCase({
|
|
3818
|
+
tenantId: input.tenantId,
|
|
3819
|
+
subject: input.subject,
|
|
3820
|
+
description: input.body,
|
|
3821
|
+
channelKind: binding.channelKind,
|
|
3822
|
+
clientProfileId: input.clientProfileId,
|
|
3823
|
+
openedByProfileId: input.openedByProfileId,
|
|
3824
|
+
projectId: binding.projectId ?? null,
|
|
3825
|
+
bindingId: binding.id ?? null,
|
|
3826
|
+
threadKey: input.threadKey,
|
|
3827
|
+
planId: binding.planId ?? null,
|
|
3828
|
+
metadata: input.caseMetadata
|
|
3829
|
+
});
|
|
3830
|
+
outcome = "created";
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
const interaction = await this.caseService.recordInteraction(supportCase, {
|
|
3834
|
+
direction: "inbound",
|
|
3835
|
+
channelKind: binding.channelKind,
|
|
3836
|
+
actorKind: "client",
|
|
3837
|
+
authorProfileId: input.authorProfileId,
|
|
3838
|
+
body: input.body,
|
|
3839
|
+
occurredAt: input.occurredAt,
|
|
3840
|
+
sourceType: input.sourceType,
|
|
3841
|
+
sourceId: input.sourceId,
|
|
3842
|
+
sourceKey: input.sourceKey,
|
|
3843
|
+
rfcMessageId: input.rfcMessageId ?? "",
|
|
3844
|
+
metadata: input.interactionMetadata
|
|
3845
|
+
});
|
|
3846
|
+
const result = {
|
|
3847
|
+
outcome,
|
|
3848
|
+
supportCase,
|
|
3849
|
+
interaction,
|
|
3850
|
+
binding
|
|
3851
|
+
};
|
|
3852
|
+
if (this.options.onCaseIntake) await this.options.onCaseIntake(result);
|
|
3853
|
+
return result;
|
|
3854
|
+
}
|
|
3855
|
+
async findResolvedByThreadKey(threadKey, bindingId) {
|
|
3856
|
+
const where = {
|
|
3857
|
+
threadKey,
|
|
3858
|
+
status: "resolved"
|
|
3859
|
+
};
|
|
3860
|
+
if (bindingId) where.bindingId = bindingId;
|
|
3861
|
+
return (await this.caseService.cases.list({
|
|
3862
|
+
where,
|
|
3863
|
+
orderBy: "updated_at DESC",
|
|
3864
|
+
limit: 1
|
|
3865
|
+
}))[0] ?? null;
|
|
3866
|
+
}
|
|
3867
|
+
async findInteractionByRfcMessageId(rfcMessageId) {
|
|
3868
|
+
return this.caseService.interactions.byRfcMessageId(rfcMessageId);
|
|
3869
|
+
}
|
|
3870
|
+
};
|
|
3871
|
+
function truncateSubject(body, max = 120) {
|
|
3872
|
+
const firstLine = (body ?? "").split("\n", 1)[0]?.trim() ?? "";
|
|
3873
|
+
if (firstLine.length <= max) return firstLine || "(no subject)";
|
|
3874
|
+
return `${firstLine.slice(0, max - 1)}\u2026`;
|
|
3875
|
+
}
|
|
3876
|
+
var SUPPORT_INTAKE_INTERCEPTOR = "smrt-support:intake";
|
|
3877
|
+
function registerSupportIntake(intake, options = {}) {
|
|
3878
|
+
const handleError = (error, instance) => {
|
|
3879
|
+
if (options.onError) {
|
|
3880
|
+
options.onError(error, instance);
|
|
3881
|
+
return;
|
|
3882
|
+
}
|
|
3883
|
+
createLogger(true).error("[smrt-support] intake failed", { error });
|
|
3884
|
+
};
|
|
3885
|
+
GlobalInterceptors.register({
|
|
3886
|
+
name: SUPPORT_INTAKE_INTERCEPTOR,
|
|
3887
|
+
async afterSave(instance) {
|
|
3888
|
+
const record = instance;
|
|
3889
|
+
try {
|
|
3890
|
+
const className = instance.constructor?.name;
|
|
3891
|
+
if (className === "ChatMessage") {
|
|
3892
|
+
if (record.isDeleted === true) return;
|
|
3893
|
+
await intake.ingestChatMessage({
|
|
3894
|
+
messageId: String(record.id ?? ""),
|
|
3895
|
+
roomId: String(record.roomId ?? ""),
|
|
3896
|
+
threadId: record.threadId ?? null,
|
|
3897
|
+
senderProfileId: String(record.senderProfileId ?? ""),
|
|
3898
|
+
role: record.role ?? "user",
|
|
3899
|
+
content: String(record.content ?? ""),
|
|
3900
|
+
tenantId: record.tenantId ?? null,
|
|
3901
|
+
occurredAt: record.created_at instanceof Date ? record.created_at : void 0
|
|
3902
|
+
});
|
|
3903
|
+
} else if (className === "Email") await intake.ingestEmail({
|
|
3904
|
+
emailId: String(record.id ?? ""),
|
|
3905
|
+
accountId: String(record.accountId ?? ""),
|
|
3906
|
+
fromAddress: String(record.fromAddress ?? ""),
|
|
3907
|
+
fromName: record.fromName ?? void 0,
|
|
3908
|
+
subject: record.subject ?? void 0,
|
|
3909
|
+
body: String(record.textBody ?? record.body ?? ""),
|
|
3910
|
+
rfcMessageId: record.messageId ?? null,
|
|
3911
|
+
inReplyTo: record.inReplyTo ?? null,
|
|
3912
|
+
tenantId: record.tenantId ?? null,
|
|
3913
|
+
occurredAt: record.date instanceof Date ? record.date : void 0
|
|
3914
|
+
});
|
|
3915
|
+
} catch (error) {
|
|
3916
|
+
handleError(error, instance);
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
});
|
|
3920
|
+
return () => {
|
|
3921
|
+
GlobalInterceptors.unregister(SUPPORT_INTAKE_INTERCEPTOR);
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
//#endregion
|
|
3925
|
+
//#region src/services/support-plan-admin-service.ts
|
|
3926
|
+
var PlanAdminDeniedError = class extends Error {
|
|
3927
|
+
constructor(message) {
|
|
3928
|
+
super(message);
|
|
3929
|
+
this.name = "PlanAdminDeniedError";
|
|
3930
|
+
}
|
|
3931
|
+
};
|
|
3932
|
+
var SupportPlanAdminService = class SupportPlanAdminService {
|
|
3933
|
+
plans;
|
|
3934
|
+
compensationPlans;
|
|
3935
|
+
constructor(collections) {
|
|
3936
|
+
this.plans = collections.plans;
|
|
3937
|
+
this.compensationPlans = collections.compensationPlans;
|
|
3938
|
+
}
|
|
3939
|
+
static async create(options) {
|
|
3940
|
+
const [plans, compensationPlans] = await Promise.all([SupportPlanCollection.create(options), SupportCompensationPlanCollection.create(options)]);
|
|
3941
|
+
return new SupportPlanAdminService({
|
|
3942
|
+
plans,
|
|
3943
|
+
compensationPlans
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3946
|
+
/** Create (or overwrite via natural key) a Managed Support Plan. */
|
|
3947
|
+
async savePlan(input) {
|
|
3948
|
+
this.assertManager(input.principal, "writing a Managed Support Plan");
|
|
3949
|
+
this.assertTenant(input.principal, input.fields.tenantId ?? null);
|
|
3950
|
+
return this.plans.create(input.fields);
|
|
3951
|
+
}
|
|
3952
|
+
/** Update an existing Managed Support Plan's fields. */
|
|
3953
|
+
async updatePlan(planId, input) {
|
|
3954
|
+
this.assertManager(input.principal, "updating a Managed Support Plan");
|
|
3955
|
+
const plan = await this.plans.get({ id: planId });
|
|
3956
|
+
if (!plan) throw new Error(`SupportPlan not found: ${planId}`);
|
|
3957
|
+
this.assertTenant(input.principal, plan.tenantId);
|
|
3958
|
+
Object.assign(plan, input.fields);
|
|
3959
|
+
await plan.save();
|
|
3960
|
+
return plan;
|
|
3961
|
+
}
|
|
3962
|
+
/** Archive a Managed Support Plan (soft retirement; history untouched). */
|
|
3963
|
+
async archivePlan(planId, input) {
|
|
3964
|
+
return this.updatePlan(planId, {
|
|
3965
|
+
principal: input.principal,
|
|
3966
|
+
fields: { status: "archived" }
|
|
3967
|
+
});
|
|
3968
|
+
}
|
|
3969
|
+
/** Create a Support Compensation Plan (effective-dated earning terms). */
|
|
3970
|
+
async saveCompensationPlan(input) {
|
|
3971
|
+
this.assertManager(input.principal, "writing a Support Compensation Plan");
|
|
3972
|
+
this.assertTenant(input.principal, input.fields.tenantId ?? null);
|
|
3973
|
+
return this.compensationPlans.create(input.fields);
|
|
3974
|
+
}
|
|
3975
|
+
/** Update an existing Support Compensation Plan's fields. */
|
|
3976
|
+
async updateCompensationPlan(planId, input) {
|
|
3977
|
+
this.assertManager(input.principal, "updating a Support Compensation Plan");
|
|
3978
|
+
const plan = await this.compensationPlans.get({ id: planId });
|
|
3979
|
+
if (!plan) throw new Error(`SupportCompensationPlan not found: ${planId}`);
|
|
3980
|
+
this.assertTenant(input.principal, plan.tenantId);
|
|
3981
|
+
Object.assign(plan, input.fields);
|
|
3982
|
+
await plan.save();
|
|
3983
|
+
return plan;
|
|
3984
|
+
}
|
|
3985
|
+
/** Archive a Support Compensation Plan. */
|
|
3986
|
+
async archiveCompensationPlan(planId, input) {
|
|
3987
|
+
return this.updateCompensationPlan(planId, {
|
|
3988
|
+
principal: input.principal,
|
|
3989
|
+
fields: { status: "archived" }
|
|
3990
|
+
});
|
|
3991
|
+
}
|
|
3992
|
+
assertManager(principal, act) {
|
|
3993
|
+
if (!principal?.can("support.manage-plans")) throw new PlanAdminDeniedError(`Denied ${act}: principal lacks '${MANAGE_PLANS_PERMISSION}'.`);
|
|
3994
|
+
}
|
|
3995
|
+
/** Cross-tenant plan writes are refused when both sides carry a tenant. */
|
|
3996
|
+
assertTenant(principal, planTenantId) {
|
|
3997
|
+
if (!principal.tenantId || !planTenantId) return;
|
|
3998
|
+
if (principal.tenantId !== planTenantId) throw new PlanAdminDeniedError(`Denied: principal tenant '${principal.tenantId}' does not match the plan tenant '${planTenantId}'.`);
|
|
3999
|
+
}
|
|
4000
|
+
};
|
|
4001
|
+
//#endregion
|
|
4002
|
+
//#region src/services/time-entry-approval-service.ts
|
|
4003
|
+
var TimeEntryApprovalDeniedError = class extends Error {
|
|
4004
|
+
constructor(reason) {
|
|
4005
|
+
super(`Time entry approval denied: ${reason}`);
|
|
4006
|
+
this.name = "TimeEntryApprovalDeniedError";
|
|
4007
|
+
}
|
|
4008
|
+
};
|
|
4009
|
+
function roundMoney(value) {
|
|
4010
|
+
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
4011
|
+
}
|
|
4012
|
+
var TimeEntryApprovalService = class TimeEntryApprovalService {
|
|
4013
|
+
entries;
|
|
4014
|
+
charges;
|
|
4015
|
+
compensations;
|
|
4016
|
+
compensationPlans;
|
|
4017
|
+
caseService;
|
|
4018
|
+
approvalMutex = new KeyedMutex();
|
|
4019
|
+
constructor(collections) {
|
|
4020
|
+
this.entries = collections.entries;
|
|
4021
|
+
this.charges = collections.charges;
|
|
4022
|
+
this.compensations = collections.compensations;
|
|
4023
|
+
this.compensationPlans = collections.compensationPlans;
|
|
4024
|
+
this.caseService = collections.caseService;
|
|
4025
|
+
}
|
|
4026
|
+
static async create(options) {
|
|
4027
|
+
const caseService = options.caseService ?? await SupportCaseService.create(options);
|
|
4028
|
+
const [entries, charges, compensations, compensationPlans] = await Promise.all([
|
|
4029
|
+
ServiceTimeEntryCollection.create(options),
|
|
4030
|
+
SupportChargeCollection.create(options),
|
|
4031
|
+
SupportCompensationCollection.create(options),
|
|
4032
|
+
SupportCompensationPlanCollection.create(options)
|
|
4033
|
+
]);
|
|
4034
|
+
return new TimeEntryApprovalService({
|
|
4035
|
+
entries,
|
|
4036
|
+
charges,
|
|
4037
|
+
compensations,
|
|
4038
|
+
compensationPlans,
|
|
4039
|
+
caseService
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
/** Load an entry or throw a descriptive error. */
|
|
4043
|
+
async getEntry(entryRef) {
|
|
4044
|
+
if (typeof entryRef !== "string") return entryRef;
|
|
4045
|
+
const found = await this.entries.get({ id: entryRef });
|
|
4046
|
+
if (!found) throw new Error(`ServiceTimeEntry not found: ${entryRef}`);
|
|
4047
|
+
return found;
|
|
4048
|
+
}
|
|
4049
|
+
/**
|
|
4050
|
+
* Resolve the plan terms governing an entry: the case's frozen
|
|
4051
|
+
* `planSnapshot` wins field-by-field (history never rewritten by plan
|
|
4052
|
+
* edits), the live plan row fills gaps, and with no case or plan at all the
|
|
4053
|
+
* terms fall back to zero rates under operator approval.
|
|
4054
|
+
*/
|
|
4055
|
+
async resolvePlanTerms(entry, supportCase) {
|
|
4056
|
+
const zeroRate = {
|
|
4057
|
+
policy: { mode: "operator" },
|
|
4058
|
+
currency: "USD",
|
|
4059
|
+
includedMinutes: 0,
|
|
4060
|
+
overageHourlyRate: 0,
|
|
4061
|
+
onCallHourlyRate: 0,
|
|
4062
|
+
planId: null,
|
|
4063
|
+
planKey: ""
|
|
4064
|
+
};
|
|
4065
|
+
if (!entry.caseId) return zeroRate;
|
|
4066
|
+
const owningCase = supportCase ?? await this.caseService.getCase(entry.caseId);
|
|
4067
|
+
const snapshot = owningCase.getPlanSnapshot();
|
|
4068
|
+
const plan = owningCase.planId ? await this.caseService.plans.get({ id: owningCase.planId }) : null;
|
|
4069
|
+
if (Object.keys(snapshot).length === 0 && !plan) return zeroRate;
|
|
4070
|
+
const num = (snapshotValue, planValue) => typeof snapshotValue === "number" ? snapshotValue : planValue ?? 0;
|
|
4071
|
+
return {
|
|
4072
|
+
policy: {
|
|
4073
|
+
mode: "operator",
|
|
4074
|
+
...(snapshot.timeApprovalPolicy && typeof snapshot.timeApprovalPolicy === "object" ? snapshot.timeApprovalPolicy : plan?.getTimeApprovalPolicy()) ?? {}
|
|
4075
|
+
},
|
|
4076
|
+
currency: typeof snapshot.currency === "string" && snapshot.currency ? snapshot.currency : plan?.currency ?? "USD",
|
|
4077
|
+
includedMinutes: num(snapshot.includedMinutes, plan?.includedMinutes),
|
|
4078
|
+
overageHourlyRate: num(snapshot.overageHourlyRate, plan?.overageHourlyRate),
|
|
4079
|
+
onCallHourlyRate: num(snapshot.onCallHourlyRate, plan?.onCallHourlyRate),
|
|
4080
|
+
planId: typeof snapshot.planId === "string" && snapshot.planId ? snapshot.planId : plan?.id ?? null,
|
|
4081
|
+
planKey: typeof snapshot.planKey === "string" ? snapshot.planKey : plan?.planKey ?? ""
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
/**
|
|
4085
|
+
* Approve a `submitted` entry: gate the acting principal by the plan's
|
|
4086
|
+
* approval policy, freeze the entry, and derive its charge and compensation
|
|
4087
|
+
* snapshots. Throws {@link TimeEntryApprovalDeniedError} when the gate
|
|
4088
|
+
* refuses.
|
|
4089
|
+
*/
|
|
4090
|
+
async approve(entryRef, input = {}) {
|
|
4091
|
+
const entry = await this.getEntry(entryRef);
|
|
4092
|
+
const lockKey = entry.caseId ?? `entry:${entry.id ?? ""}`;
|
|
4093
|
+
return this.approvalMutex.run(lockKey, () => this.approveExclusive(entry, input));
|
|
4094
|
+
}
|
|
4095
|
+
async approveExclusive(entry, input) {
|
|
4096
|
+
if (entry.status !== "submitted") throw new Error(`ServiceTimeEntry ${entry.id}: only 'submitted' entries can be approved (status is '${entry.status}').`);
|
|
4097
|
+
const at = input.at ?? /* @__PURE__ */ new Date();
|
|
4098
|
+
const principal = input.principal;
|
|
4099
|
+
this.assertTenantMatch(entry, principal);
|
|
4100
|
+
const supportCase = entry.caseId ? await this.caseService.getCase(entry.caseId) : null;
|
|
4101
|
+
const terms = await this.resolvePlanTerms(entry, supportCase);
|
|
4102
|
+
const derivation = await this.deriveCharge(entry, terms);
|
|
4103
|
+
const path = this.determinePath(entry, terms.policy, derivation.amount, principal, supportCase);
|
|
4104
|
+
const charge = await this.upsertChargeRow(entry, {
|
|
4105
|
+
tenantId: entry.tenantId,
|
|
4106
|
+
timeEntryId: entry.id ?? "",
|
|
4107
|
+
caseId: entry.caseId,
|
|
4108
|
+
planId: terms.planId,
|
|
4109
|
+
amount: derivation.amount,
|
|
4110
|
+
currency: terms.currency,
|
|
4111
|
+
billableSeconds: derivation.billableSeconds,
|
|
4112
|
+
includedSecondsApplied: derivation.includedSecondsApplied,
|
|
4113
|
+
rateSnapshot: JSON.stringify({
|
|
4114
|
+
hourlyRate: derivation.hourlyRate,
|
|
4115
|
+
rateSource: derivation.rateSource,
|
|
4116
|
+
planId: terms.planId,
|
|
4117
|
+
planKey: terms.planKey,
|
|
4118
|
+
includedMinutes: terms.includedMinutes,
|
|
4119
|
+
includedSecondsBefore: derivation.includedSecondsBefore,
|
|
4120
|
+
includedSecondsApplied: derivation.includedSecondsApplied,
|
|
4121
|
+
derivedAt: at.toISOString(),
|
|
4122
|
+
scope: "per-case (period accounting arrives with usage pricing #1925)"
|
|
4123
|
+
}),
|
|
4124
|
+
status: "final",
|
|
4125
|
+
finalizedAt: at
|
|
4126
|
+
});
|
|
4127
|
+
const compensation = await this.deriveCompensation(entry, at);
|
|
4128
|
+
entry.approvedAt = at;
|
|
4129
|
+
entry.approvedByProfileId = principal?.id ?? null;
|
|
4130
|
+
entry.approvalPath = path;
|
|
4131
|
+
entry.status = "approved";
|
|
4132
|
+
await entry.save();
|
|
4133
|
+
if (supportCase) await this.caseService.recordEvent(supportCase, "time_recorded", {
|
|
4134
|
+
actorKind: path === "client" ? "client" : principal ? "specialist" : "system",
|
|
4135
|
+
actorProfileId: principal?.id ?? null,
|
|
4136
|
+
summary: `Time entry approved (${path})`,
|
|
4137
|
+
payload: {
|
|
4138
|
+
approved: true,
|
|
4139
|
+
path,
|
|
4140
|
+
timeEntryId: entry.id ?? "",
|
|
4141
|
+
chargeAmount: charge.amount
|
|
4142
|
+
},
|
|
4143
|
+
occurredAt: at
|
|
4144
|
+
});
|
|
4145
|
+
return {
|
|
4146
|
+
entry,
|
|
4147
|
+
charge,
|
|
4148
|
+
compensation,
|
|
4149
|
+
path
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
4152
|
+
/**
|
|
4153
|
+
* Reject a `submitted` entry with a reason (requires the
|
|
4154
|
+
* `support.approve-time-entry` split).
|
|
4155
|
+
*/
|
|
4156
|
+
async reject(entryRef, input) {
|
|
4157
|
+
const entry = await this.getEntry(entryRef);
|
|
4158
|
+
if (entry.status !== "submitted") throw new Error(`ServiceTimeEntry ${entry.id}: only 'submitted' entries can be rejected (status is '${entry.status}').`);
|
|
4159
|
+
if (!input.reason?.trim()) throw new Error("ServiceTimeEntry: a rejection reason is required.");
|
|
4160
|
+
this.assertTenantMatch(entry, input.principal);
|
|
4161
|
+
this.assertOperator(input.principal, "rejecting a time entry");
|
|
4162
|
+
entry.rejectedAt = /* @__PURE__ */ new Date();
|
|
4163
|
+
entry.rejectedByProfileId = input.principal.id ?? null;
|
|
4164
|
+
entry.rejectionReason = input.reason;
|
|
4165
|
+
entry.status = "rejected";
|
|
4166
|
+
await entry.save();
|
|
4167
|
+
return entry;
|
|
4168
|
+
}
|
|
4169
|
+
/**
|
|
4170
|
+
* Correct an `approved` entry (requires the `support.approve-time-entry`
|
|
4171
|
+
* split): a NEW `draft` entry copies the original's context, participant,
|
|
4172
|
+
* and source with the patch applied and links back via `correctionOfId`;
|
|
4173
|
+
* the original flips to `corrected` with its frozen fields and snapshots
|
|
4174
|
+
* untouched, and its charge/compensation rows are marked `corrected`. The
|
|
4175
|
+
* correction then flows submit → approve normally, deriving fresh
|
|
4176
|
+
* snapshots.
|
|
4177
|
+
*/
|
|
4178
|
+
async correct(entryRef, input) {
|
|
4179
|
+
const original = await this.getEntry(entryRef);
|
|
4180
|
+
if (original.status !== "approved") throw new Error(`ServiceTimeEntry ${original.id}: only 'approved' entries can be corrected (status is '${original.status}').`);
|
|
4181
|
+
this.assertTenantMatch(original, input.principal);
|
|
4182
|
+
this.assertOperator(input.principal, "correcting an approved time entry");
|
|
4183
|
+
const patch = input.patch ?? {};
|
|
4184
|
+
const startedAt = patch.startedAt !== void 0 ? patch.startedAt : original.startedAt;
|
|
4185
|
+
const endedAt = patch.endedAt !== void 0 ? patch.endedAt : original.endedAt;
|
|
4186
|
+
const durationSeconds = patch.durationSeconds ?? original.durationSeconds;
|
|
4187
|
+
if (!(durationSeconds > 0)) throw new Error(`ServiceTimeEntry: correction durationSeconds must be greater than zero (got ${durationSeconds}).`);
|
|
4188
|
+
if (startedAt && endedAt && startedAt.getTime() >= endedAt.getTime()) throw new Error("ServiceTimeEntry: correction startedAt must be before endedAt.");
|
|
4189
|
+
const correction = await this.entries.create({
|
|
4190
|
+
tenantId: original.tenantId,
|
|
4191
|
+
caseId: original.caseId,
|
|
4192
|
+
workRefType: original.workRefType,
|
|
4193
|
+
workRefId: original.workRefId,
|
|
4194
|
+
specialistId: original.specialistId,
|
|
4195
|
+
participantKind: original.participantKind,
|
|
4196
|
+
participantProfileId: original.participantProfileId,
|
|
4197
|
+
agentRef: original.agentRef,
|
|
4198
|
+
source: original.source,
|
|
4199
|
+
description: patch.description ?? original.description,
|
|
4200
|
+
startedAt,
|
|
4201
|
+
endedAt,
|
|
4202
|
+
durationSeconds,
|
|
4203
|
+
evidence: patch.evidence !== void 0 ? JSON.stringify(patch.evidence) : original.evidence,
|
|
4204
|
+
status: "draft",
|
|
4205
|
+
correctionOfId: original.id,
|
|
4206
|
+
metadata: JSON.stringify({
|
|
4207
|
+
...original.getMetadata(),
|
|
4208
|
+
correctionNote: input.note ?? null
|
|
4209
|
+
})
|
|
4210
|
+
});
|
|
4211
|
+
original.status = "corrected";
|
|
4212
|
+
await original.save();
|
|
4213
|
+
const originalId = original.id ?? "";
|
|
4214
|
+
const [charge, compensation] = await Promise.all([this.charges.forTimeEntry(originalId), this.compensations.forTimeEntry(originalId)]);
|
|
4215
|
+
if (charge) {
|
|
4216
|
+
charge.status = "corrected";
|
|
4217
|
+
await charge.save();
|
|
4218
|
+
}
|
|
4219
|
+
if (compensation) {
|
|
4220
|
+
compensation.status = "corrected";
|
|
4221
|
+
await compensation.save();
|
|
4222
|
+
}
|
|
4223
|
+
return {
|
|
4224
|
+
original,
|
|
4225
|
+
correction
|
|
4226
|
+
};
|
|
4227
|
+
}
|
|
4228
|
+
/**
|
|
4229
|
+
* Determine the approval path for an entry under a policy, or throw
|
|
4230
|
+
* {@link TimeEntryApprovalDeniedError} when the acting principal does not
|
|
4231
|
+
* satisfy it.
|
|
4232
|
+
*/
|
|
4233
|
+
determinePath(entry, policy, chargeAmount, principal, supportCase) {
|
|
4234
|
+
const isOperator = principal?.can("support.approve-time-entry") ?? false;
|
|
4235
|
+
if (policy.mode === "automatic") {
|
|
4236
|
+
const minutes = entry.durationSeconds / 60;
|
|
4237
|
+
const overMinutes = policy.thresholdMinutes !== void 0 && minutes > policy.thresholdMinutes;
|
|
4238
|
+
const overAmount = policy.thresholdAmount !== void 0 && chargeAmount > policy.thresholdAmount;
|
|
4239
|
+
if (!overMinutes && !overAmount) return "automatic";
|
|
4240
|
+
if (!isOperator) throw new TimeEntryApprovalDeniedError(`the entry exceeds the automatic-approval ${overMinutes ? "duration" : "amount"} threshold and requires an operator holding '${APPROVE_TIME_ENTRY_PERMISSION}'.`);
|
|
4241
|
+
return "threshold";
|
|
4242
|
+
}
|
|
4243
|
+
if (policy.mode === "client") {
|
|
4244
|
+
if (!principal) throw new TimeEntryApprovalDeniedError("client approval requires an acting principal.");
|
|
4245
|
+
const clientProfileId = supportCase?.clientProfileId ?? null;
|
|
4246
|
+
if (clientProfileId && principal.id === clientProfileId) return "client";
|
|
4247
|
+
if (isOperator) return "client";
|
|
4248
|
+
throw new TimeEntryApprovalDeniedError(`client approval requires the case's Client${clientProfileId ? ` (${clientProfileId})` : ""} or an operator holding '${APPROVE_TIME_ENTRY_PERMISSION}'.`);
|
|
4249
|
+
}
|
|
4250
|
+
if (!isOperator) throw new TimeEntryApprovalDeniedError(`operator approval requires '${APPROVE_TIME_ENTRY_PERMISSION}'.`);
|
|
4251
|
+
return "operator";
|
|
4252
|
+
}
|
|
4253
|
+
/**
|
|
4254
|
+
* Charge math from the Managed Support Plan terms: remaining included time
|
|
4255
|
+
* (per case — period windows are #1925's scope) absorbs first, the
|
|
4256
|
+
* remainder meters at the overage (or on-call) hourly rate.
|
|
4257
|
+
*/
|
|
4258
|
+
async deriveCharge(entry, terms) {
|
|
4259
|
+
const billableSeconds = entry.durationSeconds;
|
|
4260
|
+
let includedSecondsBefore = 0;
|
|
4261
|
+
if (entry.caseId && terms.includedMinutes > 0) {
|
|
4262
|
+
const consumed = (await this.charges.forCase(entry.caseId)).filter((charge) => (charge.status === "final" || charge.status === "pending") && charge.timeEntryId !== entry.id).reduce((sum, charge) => sum + (charge.includedSecondsApplied ?? 0), 0);
|
|
4263
|
+
includedSecondsBefore = Math.max(0, terms.includedMinutes * 60 - consumed);
|
|
4264
|
+
}
|
|
4265
|
+
const includedSecondsApplied = Math.min(includedSecondsBefore, billableSeconds);
|
|
4266
|
+
const onCall = Boolean(entry.getMetadata().onCall);
|
|
4267
|
+
const hourlyRate = onCall ? terms.onCallHourlyRate || terms.overageHourlyRate : terms.overageHourlyRate;
|
|
4268
|
+
const amount = roundMoney((billableSeconds - includedSecondsApplied) / 3600 * hourlyRate);
|
|
4269
|
+
return {
|
|
4270
|
+
billableSeconds,
|
|
4271
|
+
includedSecondsBefore,
|
|
4272
|
+
includedSecondsApplied,
|
|
4273
|
+
hourlyRate,
|
|
4274
|
+
rateSource: onCall ? "on_call" : hourlyRate > 0 ? "overage" : "none",
|
|
4275
|
+
amount
|
|
4276
|
+
};
|
|
4277
|
+
}
|
|
4278
|
+
/**
|
|
4279
|
+
* Provider earning from the Support Compensation Plan effective at the work
|
|
4280
|
+
* instant (`endedAt`, falling back to the approval instant). No specialist
|
|
4281
|
+
* or no plan resolves to a zero-amount row so the entry still settles.
|
|
4282
|
+
*/
|
|
4283
|
+
async deriveCompensation(entry, at) {
|
|
4284
|
+
const plan = entry.specialistId ? await this.compensationPlans.resolveForSpecialist(entry.specialistId, entry.endedAt ?? at, { tenantId: entry.tenantId ?? null }) : null;
|
|
4285
|
+
const payableSeconds = entry.durationSeconds;
|
|
4286
|
+
if (!plan) return this.upsertCompensationRow(entry, {
|
|
4287
|
+
tenantId: entry.tenantId,
|
|
4288
|
+
timeEntryId: entry.id ?? "",
|
|
4289
|
+
specialistId: entry.specialistId,
|
|
4290
|
+
compensationPlanId: null,
|
|
4291
|
+
amount: 0,
|
|
4292
|
+
currency: "USD",
|
|
4293
|
+
payableSeconds,
|
|
4294
|
+
rateSnapshot: JSON.stringify({
|
|
4295
|
+
rateSource: "none",
|
|
4296
|
+
derivedAt: at.toISOString()
|
|
4297
|
+
}),
|
|
4298
|
+
status: "final",
|
|
4299
|
+
finalizedAt: at
|
|
4300
|
+
});
|
|
4301
|
+
const amount = roundMoney(payableSeconds / 3600 * plan.hourlyRate);
|
|
4302
|
+
return this.upsertCompensationRow(entry, {
|
|
4303
|
+
tenantId: entry.tenantId,
|
|
4304
|
+
timeEntryId: entry.id ?? "",
|
|
4305
|
+
specialistId: entry.specialistId,
|
|
4306
|
+
compensationPlanId: plan.id,
|
|
4307
|
+
amount,
|
|
4308
|
+
currency: plan.currency,
|
|
4309
|
+
payableSeconds,
|
|
4310
|
+
rateSnapshot: JSON.stringify({
|
|
4311
|
+
hourlyRate: plan.hourlyRate,
|
|
4312
|
+
currency: plan.currency,
|
|
4313
|
+
planId: plan.id,
|
|
4314
|
+
effectiveFrom: plan.effectiveFrom?.toISOString() ?? null,
|
|
4315
|
+
effectiveTo: plan.effectiveTo?.toISOString() ?? null,
|
|
4316
|
+
derivedAt: at.toISOString()
|
|
4317
|
+
}),
|
|
4318
|
+
status: "final",
|
|
4319
|
+
finalizedAt: at
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
4322
|
+
/**
|
|
4323
|
+
* Idempotent write of the entry's single charge row: an approval retry
|
|
4324
|
+
* after a partial failure refreshes the existing row instead of colliding
|
|
4325
|
+
* on the `time_entry_id` unique key.
|
|
4326
|
+
*/
|
|
4327
|
+
async upsertChargeRow(entry, fields) {
|
|
4328
|
+
const existing = entry.id ? await this.charges.forTimeEntry(entry.id) : null;
|
|
4329
|
+
if (!existing) return this.charges.create(fields);
|
|
4330
|
+
Object.assign(existing, fields);
|
|
4331
|
+
await existing.save();
|
|
4332
|
+
return existing;
|
|
4333
|
+
}
|
|
4334
|
+
/** Idempotent write of the entry's single compensation row (see above). */
|
|
4335
|
+
async upsertCompensationRow(entry, fields) {
|
|
4336
|
+
const existing = entry.id ? await this.compensations.forTimeEntry(entry.id) : null;
|
|
4337
|
+
if (!existing) return this.compensations.create(fields);
|
|
4338
|
+
Object.assign(existing, fields);
|
|
4339
|
+
await existing.save();
|
|
4340
|
+
return existing;
|
|
4341
|
+
}
|
|
4342
|
+
/** Cross-tenant acts are refused when both sides carry a tenant. */
|
|
4343
|
+
assertTenantMatch(entry, principal) {
|
|
4344
|
+
if (!principal?.tenantId || !entry.tenantId) return;
|
|
4345
|
+
if (principal.tenantId !== entry.tenantId) throw new TimeEntryApprovalDeniedError(`principal tenant '${principal.tenantId}' does not match the entry's tenant '${entry.tenantId}'.`);
|
|
4346
|
+
}
|
|
4347
|
+
/** Require the operator permission split for a privileged act. */
|
|
4348
|
+
assertOperator(principal, act) {
|
|
4349
|
+
if (!principal?.can("support.approve-time-entry")) throw new TimeEntryApprovalDeniedError(`${act} requires '${APPROVE_TIME_ENTRY_PERMISSION}'.`);
|
|
4350
|
+
}
|
|
4351
|
+
};
|
|
4352
|
+
//#endregion
|
|
4353
|
+
//#region src/index.ts
|
|
4354
|
+
ensureSupportPermissionsRegistered();
|
|
4355
|
+
//#endregion
|
|
4356
|
+
export { APPROVE_TIME_ENTRY_PERMISSION, CHAT_MESSAGE_SOURCE_TYPE, CHAT_ROOM_TARGET_TYPE, DEFAULT_SEVERITY_DEFINITIONS, DEFAULT_SEVERITY_KEY, DEFAULT_SUPPORT_POLICY, DEFAULT_TARGET_MINUTES, EMAIL_ACCOUNT_TARGET_TYPE, EMAIL_SOURCE_TYPE, ESCALATION_JOB_PRIORITY, HumanHandoffService, KeyedMutex, MANAGE_PLANS_PERMISSION, MAX_COVERAGE_SCAN_DAYS, OPEN_SUPPORT_CASE_STATUSES, PlanAdminDeniedError, REASSIGN_CASE_PERMISSION, ROUTING_WEIGHTS, ReassignDeniedError, SERVICE_TIME_ENTRY_STATUS_TRANSITIONS, SUPPORT_CASE_STATUS_TRANSITIONS, SUPPORT_INTAKE_INTERCEPTOR, SUPPORT_JOB_QUEUE, SUPPORT_PERMISSION_DEFS, ServiceTargetEngine, ServiceTimeEntry, ServiceTimeEntryCollection, ServiceTimeEntryService, SupportAiRun, SupportAiRunCollection, SupportAiWorkflow, SupportAvailability, SupportAvailabilityCollection, SupportCase, SupportCaseCollection, SupportCaseEvent, SupportCaseEventCollection, SupportCaseService, SupportChannelBinding, SupportChannelBindingCollection, SupportCharge, SupportChargeCollection, SupportCompensation, SupportCompensationCollection, SupportCompensationPlan, SupportCompensationPlanCollection, SupportEscalation, SupportEscalationCollection, SupportIntakeService, SupportInteraction, SupportInteractionCollection, SupportPlan, SupportPlanAdminService, SupportPlanCollection, SupportPolicy, SupportPolicyCollection, SupportQualification, SupportQualificationCollection, SupportRoutingService, SupportServiceTarget, SupportServiceTargetCollection, SupportSpecialist, SupportSpecialistCollection, SupportWorkLink, SupportWorkLinkCollection, TimeEntryApprovalDeniedError, TimeEntryApprovalService, addCoveredMinutes, coveredMinutesBetween, createDefaultAiBoundary, createNoopKnowledgeProvider, ensureSupportPermissionsRegistered, generateCaseNumber, parseJsonField, parseStringArrayField, registerSupportIntake, registerSupportPermissions, severityRank, supportPrincipalFromPermissions, zonedParts };
|
|
4357
|
+
|
|
4358
|
+
//# sourceMappingURL=index.js.map
|