@opencxh/domain 1.224.0 → 1.226.0

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.
@@ -5,7 +5,7 @@ import { ReactionSummary } from '../reaction/types';
5
5
  export type CallStatus = "new" | "connecting" | "ringing" | "connected" | "held" | "ended" | "failed";
6
6
  export type CallDirection = "inbound" | "outbound";
7
7
  export type CallType = "audio" | "video" | "data" | "screen-share";
8
- export type ActivityType = "VOICE_CALL_STARTED" | "VOICE_CALL_ANSWERED" | "VOICE_CALL_HOLD" | "VOICE_CALL_UNHOLD" | "VOICE_CALL_ENDED" | "VOICE_CALL_MISSED" | "VOICE_CALL_VOICEMAIL" | "VIDEO_CALL_STARTED" | "VIDEO_CALL_ANSWERED" | "VIDEO_CALL_HOLD" | "VIDEO_CALL_UNHOLD" | "VIDEO_CALL_ENDED" | "VIDEO_CALL_MISSED" | "EMAIL_RECEIVED" | "EMAIL_SENT" | "CHAT_MESSAGE_SENT" | "CHAT_MESSAGE_RECEIVED" | "CHAT_MEMBER_JOINED" | "CHAT_MEMBER_LEFT" | "CHAT_RENAMED" | "CHAT_CALL_STARTED" | "CHAT_CALL_ENDED" | "CHAT_EVENT" | "AI_MESSAGE_ADDED" | "AI_ACTION_PROPOSED" | "PLAYBOOK_STARTED" | "PLAYBOOK_COMPLETED" | "PLAYBOOK_ESCALATED" | "MEETING_SCHEDULED" | "MEETING_STARTED" | "MEETING_ENDED" | "MEETING_PARTICIPANT_JOINED" | "MEETING_PARTICIPANT_LEFT" | "COMMENT_ADDED" | "FILE_UPLOADED" | "INTERACTION_CREATED" | "INTERACTION_STATUS_CHANGED" | "INTERACTION_ASSIGNED" | "VOICE_CALL_FAILED" | "VIDEO_CALL_FAILED" | "TRANSCRIPT_ADDED" | "WORK_COMMENT_ADDED" | "WORK_ITEM_ASSIGNED" | "WORK_ITEM_STATUS_CHANGED";
8
+ export type ActivityType = "VOICE_CALL_STARTED" | "VOICE_CALL_ANSWERED" | "VOICE_CALL_HOLD" | "VOICE_CALL_UNHOLD" | "VOICE_CALL_ENDED" | "VOICE_CALL_MISSED" | "VOICE_CALL_VOICEMAIL" | "VIDEO_CALL_STARTED" | "VIDEO_CALL_ANSWERED" | "VIDEO_CALL_HOLD" | "VIDEO_CALL_UNHOLD" | "VIDEO_CALL_ENDED" | "VIDEO_CALL_MISSED" | "EMAIL_RECEIVED" | "EMAIL_SENT" | "CHAT_MESSAGE_SENT" | "CHAT_MESSAGE_RECEIVED" | "CHAT_MEMBER_JOINED" | "CHAT_MEMBER_LEFT" | "CHAT_RENAMED" | "CHAT_CALL_STARTED" | "CHAT_CALL_ENDED" | "CHAT_EVENT" | "AI_MESSAGE_ADDED" | "AI_ACTION_PROPOSED" | "PLAYBOOK_STARTED" | "PLAYBOOK_COMPLETED" | "PLAYBOOK_ESCALATED" | "MEETING_SCHEDULED" | "MEETING_STARTED" | "MEETING_ENDED" | "MEETING_PARTICIPANT_JOINED" | "MEETING_PARTICIPANT_LEFT" | "COMMENT_ADDED" | "FILE_UPLOADED" | "INTERACTION_CREATED" | "INTERACTION_STATUS_CHANGED" | "INTERACTION_ASSIGNED" | "SLA_BREACHED" | "VOICE_CALL_FAILED" | "VIDEO_CALL_FAILED" | "TRANSCRIPT_ADDED" | "WORK_COMMENT_ADDED" | "WORK_ITEM_ASSIGNED" | "WORK_ITEM_STATUS_CHANGED";
9
9
  export interface Attachment {
10
10
  id: string;
11
11
  filename: string;
@@ -240,6 +240,23 @@ export type InteractionStatusChangedPayload = {
240
240
  fromStatus: InteractionStatus;
241
241
  toStatus: InteractionStatus;
242
242
  };
243
+ /**
244
+ * A service target that ran out. Written by the app that owns the clocks, not by comms.
245
+ *
246
+ * It is a timeline row rather than a new trigger kind on purpose: the playbook engine already
247
+ * triggers on activities with a JSONLogic filter, so "escalate when the SLA breaches" needs no
248
+ * engine change at all — and the breach is visible to a human in the same move.
249
+ */
250
+ export type SlaBreachedPayload = {
251
+ /** `first_response`, `close`, … — an `SlaMetric`, kept as a string so domain layers stay
252
+ * independent of each other. */
253
+ metric: string;
254
+ profileId: string;
255
+ /** Which attempt of this target; `next_response` has more than one. */
256
+ attempt: number;
257
+ /** The deadline that passed. */
258
+ dueAt: number;
259
+ };
243
260
  export type InteractionAssignedPayload = {
244
261
  userId: string;
245
262
  teamId?: string;
@@ -405,6 +422,9 @@ export type Activity = (BaseActivity & {
405
422
  }) | (BaseActivity & {
406
423
  type: "INTERACTION_ASSIGNED";
407
424
  payload: InteractionAssignedPayload;
425
+ }) | (BaseActivity & {
426
+ type: "SLA_BREACHED";
427
+ payload: SlaBreachedPayload;
408
428
  });
409
429
  /**
410
430
  * An activity of **any type at all**, including a type an app declared.
@@ -166,6 +166,20 @@ export interface McpConnectorDescribe {
166
166
  /** `mcp__<id>__`, same as {@link mcpToolPrefix} — passed along so the caller computes nothing. */
167
167
  toolPrefix: string;
168
168
  }
169
+ /**
170
+ * One tool of one connector, as `GET /mcp-connector/:id/tools` reports it.
171
+ *
172
+ * Its own route next to the tool catalog because the catalog cannot answer this: it drops every
173
+ * `personal` tool, and every entry in the connector catalog is per-user OAuth — so an app asking
174
+ * `GET /tools/` about an MCP server reliably gets nothing back.
175
+ */
176
+ export interface McpConnectorTool {
177
+ /** The **bare** tool name. Compose with {@link mcpToolPrefix} to invoke it. */
178
+ name: string;
179
+ description?: string;
180
+ /** JSON-Schema of the arguments, straight from the server — the right-hand side of a mapping. */
181
+ inputSchema?: unknown;
182
+ }
169
183
  /** Body accepted by the create/update endpoints (write shape, distinct from the read view). */
170
184
  export interface McpServerInput {
171
185
  name?: string;
@@ -57,6 +57,15 @@ export declare const TRIGGER_VARS: readonly [{
57
57
  }, {
58
58
  readonly name: "companyId";
59
59
  readonly type: "string";
60
+ }, {
61
+ readonly name: "priority";
62
+ readonly type: "string";
63
+ }, {
64
+ readonly name: "inboxId";
65
+ readonly type: "string";
66
+ }, {
67
+ readonly name: "topicId";
68
+ readonly type: "string";
60
69
  }, {
61
70
  readonly name: "assigneeUserId";
62
71
  readonly type: "string";
@@ -0,0 +1,7 @@
1
+ import { SlaCalendar } from './types';
2
+ /** Working ms between two instants. 0 when `to <= from`. `null` calendar = around the clock. */
3
+ export declare function workingMsBetween(from: number, to: number, cal: SlaCalendar | null): number;
4
+ /** The instant `ms` of working time after `from`. `null` calendar = around the clock. */
5
+ export declare function addWorkingMs(from: number, ms: number, cal: SlaCalendar | null): number;
6
+ /** `"08:30"` -> 510. Returns `null` on anything that is not `HH:MM`. */
7
+ export declare function parseClockMinutes(value: string): number | null;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export * from './hours';
2
+ export * from './machine';
3
+ export * from './types';
4
+ export * from './view';
@@ -0,0 +1,47 @@
1
+ import { SlaClock, SlaMetric, SlaProfile } from './types';
2
+ /**
3
+ * The nine facts a source app reports about a resource. It reports *what happened*; every
4
+ * decision about which clock starts, stops or pauses lives here, so a second source app
5
+ * (work items, later) needs an adapter and nothing else.
6
+ */
7
+ export type SlaEventKind = "apply" | "answered" | "inbound" | "outbound" | "resolved" | "closed" | "reopened" | "snoozed" | "unsnoozed" | "discarded";
8
+ /**
9
+ * The facts a source app may report. `apply` is excluded on purpose: attaching a profile is a
10
+ * user-facing decision with its own route and its own authorization, not something an app
11
+ * announces about its resource.
12
+ */
13
+ export type SlaFact = Exclude<SlaEventKind, "apply">;
14
+ export type SlaClockPatch = Partial<Pick<SlaClock, "state" | "settledAt" | "dueAt" | "remainingMs" | "pauseBits">>;
15
+ export type SlaOp = {
16
+ op: "start";
17
+ metric: SlaMetric;
18
+ attempt: number;
19
+ startedAt: number;
20
+ targetMs: number;
21
+ dueAt: number;
22
+ } | {
23
+ op: "patch";
24
+ clockId: string;
25
+ patch: SlaClockPatch;
26
+ };
27
+ export interface SlaEventInput {
28
+ event: SlaEventKind;
29
+ /**
30
+ * When the fact happened. Clamped to `now`: a source clock that runs ahead must not create
31
+ * a deadline in the past.
32
+ */
33
+ at: number;
34
+ now: number;
35
+ /** Every clock on this resource, settled ones included — `attempt` is drawn from them. */
36
+ clocks: SlaClock[];
37
+ profile: SlaProfile;
38
+ }
39
+ /**
40
+ * Turn one reported fact into the store operations it implies.
41
+ *
42
+ * Pure: no ids are minted, nothing is written. The caller applies `start` as an upsert on the
43
+ * unique `(organizationId, scopeKey, metric, attempt)` index and `patch` as a conditional
44
+ * update with the clock's current state in the *filter* — which is what makes a replayed
45
+ * event match zero rows instead of needing a dedupe table.
46
+ */
47
+ export declare function planSlaEvent({ event, at, now, clocks, profile }: SlaEventInput): SlaOp[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,107 @@
1
+ import { OrgTeamScope } from '../scope/types';
2
+ /**
3
+ * The five service targets, in the vocabulary the market already uses.
4
+ *
5
+ * `speed_of_answer` only means something on a live channel (voice, video): it measures
6
+ * arrival -> picked up. On chat and mail it would be the same moment as
7
+ * `first_response`, so the profile form refuses that combination.
8
+ */
9
+ export type SlaMetric = "speed_of_answer" | "first_response" | "next_response" | "resolution" | "close";
10
+ export type SlaPauseReason = "snoozed" | "waiting_for_customer";
11
+ /**
12
+ * `void` is not a third way of failing — it is "this clock stopped meaning anything".
13
+ * A conversation closed as spam 90 seconds into a 4h first-response target is neither a
14
+ * hit nor a miss, and counting it as either poisons the attainment number. Same for a
15
+ * profile swapped mid-flight and for the losing side of a merge.
16
+ */
17
+ export type SlaClockState = "running" | "paused" | "hit" | "missed" | "void";
18
+ /** Bit values for {@link SlaClock.pauseBits}. */
19
+ export declare const PAUSE_SNOOZED = 1;
20
+ export declare const PAUSE_WAITING_FOR_CUSTOMER = 2;
21
+ export declare function pauseBitFor(reason: SlaPauseReason): number;
22
+ /**
23
+ * Opening hours as the clock needs them: minutes from local midnight, plus the offset to
24
+ * get back to UTC.
25
+ *
26
+ * `utcOffsetMinutes` is **standard** time, minutes east of UTC — the house convention
27
+ * (`apps/time/server/src/time_entry/logic.ts`). Summer time is a rule, not a second field,
28
+ * because an offset stored once is wrong for seven months of the year.
29
+ */
30
+ export interface SlaCalendar {
31
+ /** ISO weekdays that are open, 1 = Monday. Mirrors `ProfileOpeningHours.days`. */
32
+ days: number[];
33
+ /** "08:30" -> 510 */
34
+ fromMinutes: number;
35
+ toMinutes: number;
36
+ utcOffsetMinutes: number;
37
+ dst: "none" | "eu";
38
+ }
39
+ export interface SlaTarget {
40
+ metric: SlaMetric;
41
+ minutes: number;
42
+ }
43
+ /**
44
+ * A named set of service targets. Assigned to a resource explicitly — from a workflow rule
45
+ * (`tool:context__apply_sla`) or by hand — never matched automatically. That is why there is
46
+ * no `appliesTo` and no `order`: both only exist to drive a matcher nothing calls.
47
+ */
48
+ export interface SlaProfile {
49
+ id: string;
50
+ organizationId: string;
51
+ ownerScope: OrgTeamScope;
52
+ name: string;
53
+ description?: string;
54
+ enabled: boolean;
55
+ targets: SlaTarget[];
56
+ /**
57
+ * Absent or empty = around the clock. Deliberately **not** a nullable field: `invoke`
58
+ * strips `null` out of a body before it ever reaches the server
59
+ * (`packages/app-sdk/src/modules/api.ts`), so "no calendar" has to be expressible as
60
+ * absence. Read it through {@link calendarOf}, never directly.
61
+ */
62
+ calendar?: SlaCalendar;
63
+ pauseOn: SlaPauseReason[];
64
+ }
65
+ /**
66
+ * The calendar to run against: `null` means around the clock. Takes a profile or a clock —
67
+ * both carry the field, and the clock's copy is the one a badge should trust.
68
+ */
69
+ export declare function calendarOf(source: {
70
+ calendar?: SlaCalendar;
71
+ }): SlaCalendar | null;
72
+ /**
73
+ * One target, one attempt. `next_response` gets a fresh row per customer message, which is
74
+ * why `attempt` is part of the identity: `(organizationId, scopeKey, metric, attempt)` is
75
+ * unique in the store, and that uniqueness is the whole idempotency story.
76
+ *
77
+ * `dueAt` goes stale while paused, on purpose — the sweeper only ever queries
78
+ * `state: "running"`, and a paused clock shows time remaining, not a wall-clock moment.
79
+ */
80
+ export interface SlaClock {
81
+ id: string;
82
+ organizationId: string;
83
+ /** `"<kind>:<id>"`, e.g. `"interaction:abc"`. Both halves of the resource, one column. */
84
+ scopeKey: string;
85
+ metric: SlaMetric;
86
+ attempt: number;
87
+ profileId: string;
88
+ ownerScope: OrgTeamScope;
89
+ targetMs: number;
90
+ /**
91
+ * The calendar this clock was started against, copied from the profile.
92
+ *
93
+ * Denormalised on purpose, twice over: a badge can do its arithmetic without fetching the
94
+ * profile — on any client, for any resource kind — and editing a profile does not silently
95
+ * move the deadline of something already running. Absent = around the clock.
96
+ */
97
+ calendar?: SlaCalendar;
98
+ startedAt: number;
99
+ dueAt: number;
100
+ /** Working ms left at the moment of pausing. Only meaningful while `state === "paused"`. */
101
+ remainingMs: number;
102
+ pauseBits: number;
103
+ state: SlaClockState;
104
+ settledAt?: number;
105
+ }
106
+ export declare const SLA_METRICS: SlaMetric[];
107
+ export declare const SLA_PAUSE_REASONS: SlaPauseReason[];
@@ -0,0 +1,36 @@
1
+ import { SlaClock } from './types';
2
+ /**
3
+ * What a badge needs, derived from the clocks on one resource. Pure: the same answer on the
4
+ * server, in the browser and on mobile.
5
+ */
6
+ export declare function isOpenClock(clock: SlaClock): boolean;
7
+ /**
8
+ * Working ms left. Negative once the deadline has passed.
9
+ *
10
+ * A paused clock reads its stored remainder — `dueAt` is deliberately stale then. A running one
11
+ * measures against the calendar, so "2 hours left" at 16:00 on a Friday means two *working*
12
+ * hours, which lands on Monday and is the only number that matches the deadline.
13
+ */
14
+ export declare function remainingMsOf(clock: SlaClock, now: number): number;
15
+ /** Wall-clock moment the deadline falls on, calendar included. Useful for a tooltip. */
16
+ export declare function dueAtOf(clock: SlaClock, now: number): number;
17
+ /**
18
+ * The one clock a badge should show: the open one that runs out first.
19
+ *
20
+ * Running beats paused whatever the numbers say — a paused target is not what someone has to
21
+ * act on, and showing it above a running one would put the least urgent thing first. Among
22
+ * equals, the nearest deadline.
23
+ */
24
+ export declare function nextSlaClock(clocks: SlaClock[], now: number): SlaClock | undefined;
25
+ export type SlaBadgeTone = "breached" | "urgent" | "running" | "paused" | "met";
26
+ /**
27
+ * How a clock should read. `urgent` at the last fifth of the target — a proportion, not a fixed
28
+ * number of minutes, because "five minutes left" means something very different on a 15-minute
29
+ * first response than on an eight-hour resolution.
30
+ */
31
+ export declare function slaBadgeTone(clock: SlaClock, now: number): SlaBadgeTone;
32
+ /**
33
+ * "2u 10m", "45m", "30s". Rounded down on purpose: a target with 59 seconds left should not
34
+ * read "1m" — being early about a breach is worse than being blunt about it.
35
+ */
36
+ export declare function formatSlaDuration(ms: number): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -114,6 +114,11 @@ export interface WorkProject {
114
114
  lastSequence?: number;
115
115
  archived?: boolean;
116
116
  order?: number;
117
+ /**
118
+ * Ids of this project in source systems (`asana:12345`), the dedupe axis of the ingest
119
+ * pipeline. Empty on a project created here.
120
+ */
121
+ externalIds?: string[];
117
122
  createdBy: string;
118
123
  createdAt?: number;
119
124
  updatedAt?: number;