@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.
Files changed (96) hide show
  1. package/AGENTS.md +270 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/dist/__smrt-register__.d.ts +2 -0
  5. package/dist/__smrt-register__.d.ts.map +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +4358 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/manifest.json +6770 -0
  11. package/dist/models/index.d.ts +17 -0
  12. package/dist/models/index.d.ts.map +1 -0
  13. package/dist/models/service-time-entry.d.ts +75 -0
  14. package/dist/models/service-time-entry.d.ts.map +1 -0
  15. package/dist/models/support-ai-run.d.ts +45 -0
  16. package/dist/models/support-ai-run.d.ts.map +1 -0
  17. package/dist/models/support-case-event.d.ts +25 -0
  18. package/dist/models/support-case-event.d.ts.map +1 -0
  19. package/dist/models/support-case.d.ts +125 -0
  20. package/dist/models/support-case.d.ts.map +1 -0
  21. package/dist/models/support-channel-binding.d.ts +47 -0
  22. package/dist/models/support-channel-binding.d.ts.map +1 -0
  23. package/dist/models/support-compensation-plan.d.ts +42 -0
  24. package/dist/models/support-compensation-plan.d.ts.map +1 -0
  25. package/dist/models/support-interaction.d.ts +49 -0
  26. package/dist/models/support-interaction.d.ts.map +1 -0
  27. package/dist/models/support-plan.d.ts +92 -0
  28. package/dist/models/support-plan.d.ts.map +1 -0
  29. package/dist/models/support-policy.d.ts +84 -0
  30. package/dist/models/support-policy.d.ts.map +1 -0
  31. package/dist/models/support-service-target.d.ts +90 -0
  32. package/dist/models/support-service-target.d.ts.map +1 -0
  33. package/dist/models/support-settlement.d.ts +58 -0
  34. package/dist/models/support-settlement.d.ts.map +1 -0
  35. package/dist/models/support-specialist.d.ts +71 -0
  36. package/dist/models/support-specialist.d.ts.map +1 -0
  37. package/dist/models/support-work-link.d.ts +31 -0
  38. package/dist/models/support-work-link.d.ts.map +1 -0
  39. package/dist/permissions.d.ts +32 -0
  40. package/dist/permissions.d.ts.map +1 -0
  41. package/dist/services/coverage-calendar.d.ts +53 -0
  42. package/dist/services/coverage-calendar.d.ts.map +1 -0
  43. package/dist/services/human-handoff-service.d.ts +113 -0
  44. package/dist/services/human-handoff-service.d.ts.map +1 -0
  45. package/dist/services/index.d.ts +15 -0
  46. package/dist/services/index.d.ts.map +1 -0
  47. package/dist/services/keyed-mutex.d.ts +22 -0
  48. package/dist/services/keyed-mutex.d.ts.map +1 -0
  49. package/dist/services/service-target-engine.d.ts +133 -0
  50. package/dist/services/service-target-engine.d.ts.map +1 -0
  51. package/dist/services/service-time-entry-service.d.ts +73 -0
  52. package/dist/services/service-time-entry-service.d.ts.map +1 -0
  53. package/dist/services/support-ai-workflow.d.ts +178 -0
  54. package/dist/services/support-ai-workflow.d.ts.map +1 -0
  55. package/dist/services/support-case-service.d.ts +162 -0
  56. package/dist/services/support-case-service.d.ts.map +1 -0
  57. package/dist/services/support-intake-service.d.ts +106 -0
  58. package/dist/services/support-intake-service.d.ts.map +1 -0
  59. package/dist/services/support-plan-admin-service.d.ts +45 -0
  60. package/dist/services/support-plan-admin-service.d.ts.map +1 -0
  61. package/dist/services/support-routing-service.d.ts +117 -0
  62. package/dist/services/support-routing-service.d.ts.map +1 -0
  63. package/dist/services/time-entry-approval-service.d.ts +162 -0
  64. package/dist/services/time-entry-approval-service.d.ts.map +1 -0
  65. package/dist/smrt-knowledge.json +4242 -0
  66. package/dist/svelte/components/CaseDetail.svelte +277 -0
  67. package/dist/svelte/components/CaseDetail.svelte.d.ts +10 -0
  68. package/dist/svelte/components/CaseDetail.svelte.d.ts.map +1 -0
  69. package/dist/svelte/components/CaseQueue.svelte +148 -0
  70. package/dist/svelte/components/CaseQueue.svelte.d.ts +12 -0
  71. package/dist/svelte/components/CaseQueue.svelte.d.ts.map +1 -0
  72. package/dist/svelte/components/RoutingRationale.svelte +131 -0
  73. package/dist/svelte/components/RoutingRationale.svelte.d.ts +11 -0
  74. package/dist/svelte/components/RoutingRationale.svelte.d.ts.map +1 -0
  75. package/dist/svelte/components/TargetList.svelte +127 -0
  76. package/dist/svelte/components/TargetList.svelte.d.ts +9 -0
  77. package/dist/svelte/components/TargetList.svelte.d.ts.map +1 -0
  78. package/dist/svelte/components/TimeEntryApprovalQueue.svelte +226 -0
  79. package/dist/svelte/components/TimeEntryApprovalQueue.svelte.d.ts +11 -0
  80. package/dist/svelte/components/TimeEntryApprovalQueue.svelte.d.ts.map +1 -0
  81. package/dist/svelte/components/__tests__/CaseDetail.test.js +86 -0
  82. package/dist/svelte/components/__tests__/CaseQueue.test.js +67 -0
  83. package/dist/svelte/components/__tests__/RoutingRationale.test.js +69 -0
  84. package/dist/svelte/components/__tests__/TargetList.test.js +84 -0
  85. package/dist/svelte/components/__tests__/TimeEntryApprovalQueue.test.js +105 -0
  86. package/dist/svelte/index.d.ts +25 -0
  87. package/dist/svelte/index.d.ts.map +1 -0
  88. package/dist/svelte/index.js +18 -0
  89. package/dist/svelte/types.d.ts +129 -0
  90. package/dist/svelte/types.d.ts.map +1 -0
  91. package/dist/svelte/types.js +182 -0
  92. package/dist/types.d.ts +130 -0
  93. package/dist/types.d.ts.map +1 -0
  94. package/dist/types.js +109 -0
  95. package/dist/types.js.map +1 -0
  96. 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