@dxv-systems/helpdesk 0.1.0 → 0.3.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.
- package/README.md +299 -279
- package/dist/bounce.d.ts +6 -0
- package/dist/bounce.js +38 -0
- package/dist/controls.js +1 -6
- package/dist/draft-body.d.ts +0 -1
- package/dist/draft-body.js +0 -14
- package/dist/drafting.d.ts +1 -2
- package/dist/drafting.js +12 -80
- package/dist/errors.d.ts +0 -3
- package/dist/errors.js +1 -7
- package/dist/inbox/handlers.js +27 -36
- package/dist/inbox/messages.d.ts +4 -5
- package/dist/inbox/messages.js +23 -14
- package/dist/inbox/types.d.ts +12 -2
- package/dist/index.d.ts +3 -4
- package/dist/index.js +2 -6
- package/dist/ingest.d.ts +4 -0
- package/dist/ingest.js +144 -22
- package/dist/react.d.ts +42 -2
- package/dist/react.js +249 -94
- package/dist/sending.d.ts +2 -1
- package/dist/sending.js +35 -58
- package/dist/states.d.ts +1 -1
- package/dist/states.js +1 -2
- package/dist/testing/index.d.ts +4 -2
- package/dist/testing/index.js +19 -29
- package/dist/types.d.ts +6 -8
- package/package.json +73 -73
- package/dist/autosend.d.ts +0 -43
- package/dist/autosend.js +0 -83
- package/dist/cleanup.d.ts +0 -7
- package/dist/cleanup.js +0 -39
package/dist/sending.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.classifySendError = classifySendError;
|
|
4
|
+
exports.sendIntentId = sendIntentId;
|
|
4
5
|
exports.approveAndSend = approveAndSend;
|
|
5
6
|
exports.reconcileUnknownSends = reconcileUnknownSends;
|
|
6
7
|
exports.resolveUnknownSend = resolveUnknownSend;
|
|
@@ -8,7 +9,6 @@ exports.markRepliedExternally = markRepliedExternally;
|
|
|
8
9
|
exports.dismissThread = dismissThread;
|
|
9
10
|
exports.markWaiting = markWaiting;
|
|
10
11
|
exports.closeThread = closeThread;
|
|
11
|
-
const cleanup_1 = require("./cleanup");
|
|
12
12
|
const controls_1 = require("./controls");
|
|
13
13
|
const draft_body_1 = require("./draft-body");
|
|
14
14
|
const errors_1 = require("./errors");
|
|
@@ -22,6 +22,9 @@ function classifySendError(err) {
|
|
|
22
22
|
return typeof status === "number" && NOT_SENT_STATUSES.has(status) ? "not_sent" : "unknown";
|
|
23
23
|
}
|
|
24
24
|
const SEND_IN_FLIGHT_MS = 2 * 60 * 1000;
|
|
25
|
+
function sendIntentId(run) {
|
|
26
|
+
return `send:${run.mailbox}:${run.idempotencyKey}`;
|
|
27
|
+
}
|
|
25
28
|
async function markSent(deps, thread, intent) {
|
|
26
29
|
const fresh = await (0, internal_1.loadThread)(deps, thread.id);
|
|
27
30
|
const sent = await deps.store.transitionSendIntent(intent.id, ["sending", "unknown"], {
|
|
@@ -58,7 +61,7 @@ async function sendingElsewhereOrLost(deps, thread, intent) {
|
|
|
58
61
|
}
|
|
59
62
|
return surfaceUnknown(deps, thread, intent);
|
|
60
63
|
}
|
|
61
|
-
async function
|
|
64
|
+
async function prepareDraft(deps, access, run, intent) {
|
|
62
65
|
const gateway = access.gateway();
|
|
63
66
|
let current = run;
|
|
64
67
|
if (!intent.draftId || run.placedDraftId !== intent.draftId || run.placedBodyHtml === null) {
|
|
@@ -66,7 +69,6 @@ async function prepareAppDraft(deps, access, run, intent) {
|
|
|
66
69
|
current = await deps.store.updateDraftRun(run.idempotencyKey, {
|
|
67
70
|
placedDraftId: draft.id,
|
|
68
71
|
placedBodyHtml: draft.body.content,
|
|
69
|
-
placementError: null,
|
|
70
72
|
});
|
|
71
73
|
await deps.store.transitionSendIntent(intent.id, ["sending"], { draftId: draft.id });
|
|
72
74
|
}
|
|
@@ -84,21 +86,17 @@ async function approveAndSend(deps, input) {
|
|
|
84
86
|
const access = (0, controls_1.mailboxAccess)(deps, controls, thread.mailbox);
|
|
85
87
|
if (access.mode === "read_only")
|
|
86
88
|
return { status: "write_blocked", mode: access.mode };
|
|
87
|
-
const inApp = access.mode === "drafts_in_app";
|
|
88
89
|
const run = (0, drafting_1.currentReplyRun)(deps, thread, await deps.store.getLatestDraftRunForThread(thread.id));
|
|
89
|
-
if (!run ||
|
|
90
|
-
throw new errors_1.DraftNotReadyError(thread.id,
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (prior?.status === "sending")
|
|
100
|
-
return sendingElsewhereOrLost(deps, thread, prior);
|
|
101
|
-
}
|
|
90
|
+
if (!run || run.status !== "succeeded")
|
|
91
|
+
throw new errors_1.DraftNotReadyError(thread.id, "no reply text to approve");
|
|
92
|
+
const intentId = sendIntentId(run);
|
|
93
|
+
const prior = await deps.store.getSendIntent(intentId);
|
|
94
|
+
if (prior?.status === "sent")
|
|
95
|
+
return { status: "already_sent" };
|
|
96
|
+
if (prior?.status === "unknown")
|
|
97
|
+
return surfaceUnknown(deps, thread, prior);
|
|
98
|
+
if (prior?.status === "sending")
|
|
99
|
+
return sendingElsewhereOrLost(deps, thread, prior);
|
|
102
100
|
(0, states_1.transition)(thread.state, "approve");
|
|
103
101
|
if ((run.commentText ?? run.text ?? "").trim() === "")
|
|
104
102
|
throw new errors_1.EmptyReplyError(thread.id);
|
|
@@ -107,10 +105,10 @@ async function approveAndSend(deps, input) {
|
|
|
107
105
|
}
|
|
108
106
|
const now = (0, internal_1.nowIso)(deps);
|
|
109
107
|
const { intent } = await deps.store.createSendIntentIfAbsent({
|
|
110
|
-
id:
|
|
108
|
+
id: intentId,
|
|
111
109
|
mailbox: thread.mailbox,
|
|
112
110
|
threadId: thread.id,
|
|
113
|
-
draftId:
|
|
111
|
+
draftId: null,
|
|
114
112
|
approvedBy: input.approvedBy,
|
|
115
113
|
status: "intended",
|
|
116
114
|
createdAt: now,
|
|
@@ -134,10 +132,9 @@ async function approveAndSend(deps, input) {
|
|
|
134
132
|
await failNotSent(deps, intent.id, new Error("ticket changed before sending"));
|
|
135
133
|
throw new errors_1.StaleThreadError(thread.id);
|
|
136
134
|
}
|
|
137
|
-
let draftId
|
|
135
|
+
let draftId;
|
|
138
136
|
try {
|
|
139
|
-
|
|
140
|
-
draftId = await prepareAppDraft(deps, access, run, claimed);
|
|
137
|
+
draftId = await prepareDraft(deps, access, run, claimed);
|
|
141
138
|
await (0, reply_attachments_1.placeReplyAttachments)(deps, access, thread.id, draftId);
|
|
142
139
|
}
|
|
143
140
|
catch (err) {
|
|
@@ -202,14 +199,8 @@ async function resolveUnknownSend(deps, input) {
|
|
|
202
199
|
const run = await deps.store.getLatestDraftRunForThread(thread.id);
|
|
203
200
|
if (!run)
|
|
204
201
|
throw new errors_1.DraftNotReadyError(thread.id, "no draft run");
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
for (const id of [ids.perRun, ids.perDraft]) {
|
|
208
|
-
const found = id ? await deps.store.getSendIntent(id) : null;
|
|
209
|
-
if (found?.status === "unknown")
|
|
210
|
-
current = found;
|
|
211
|
-
}
|
|
212
|
-
if (!current)
|
|
202
|
+
const current = await deps.store.getSendIntent(sendIntentId(run));
|
|
203
|
+
if (current?.status !== "unknown")
|
|
213
204
|
throw new errors_1.DraftNotReadyError(thread.id, "no unknown send to resolve");
|
|
214
205
|
const id = current.id;
|
|
215
206
|
if (input.outcome === "not_sent") {
|
|
@@ -229,56 +220,42 @@ async function markRepliedExternally(deps, threadId) {
|
|
|
229
220
|
if (controls.paused)
|
|
230
221
|
return (0, controls_1.paused)();
|
|
231
222
|
const thread = await (0, internal_1.loadThread)(deps, threadId);
|
|
232
|
-
|
|
223
|
+
(0, controls_1.assertInScope)(deps.config, thread.mailbox);
|
|
233
224
|
const updated = await (0, internal_1.applyEvents)(deps, thread, ["approve"], { reviewReason: null });
|
|
234
225
|
await (0, reply_attachments_1.discardPendingReplyAttachments)(deps, thread.id);
|
|
235
|
-
const run = await deps.store.getLatestDraftRunForThread(thread.id).catch(() => null);
|
|
236
|
-
if (run)
|
|
237
|
-
await (0, cleanup_1.discardPlacedDraft)(deps, run, access, thread.id);
|
|
238
226
|
return updated;
|
|
239
227
|
}
|
|
240
228
|
async function dismissThread(deps, input) {
|
|
241
|
-
|
|
242
|
-
if (controls.paused)
|
|
229
|
+
if ((await (0, controls_1.loadControls)(deps)).paused)
|
|
243
230
|
return (0, controls_1.paused)();
|
|
244
231
|
const thread = await (0, internal_1.loadThread)(deps, input.threadId);
|
|
245
|
-
|
|
232
|
+
(0, controls_1.assertInScope)(deps.config, thread.mailbox);
|
|
246
233
|
(0, states_1.transition)(thread.state, "dismiss");
|
|
247
234
|
const run = await deps.store.getLatestDraftRunForThread(thread.id);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
throw new errors_1.SendPendingError(thread.id, seen.status === "unknown_outcome" ? "unknown" : "sending");
|
|
253
|
-
}
|
|
254
|
-
if (intent?.status === "unknown")
|
|
255
|
-
throw new errors_1.SendPendingError(thread.id, "unknown");
|
|
235
|
+
const intent = run ? await deps.store.getSendIntent(sendIntentId(run)) : null;
|
|
236
|
+
if (intent?.status === "sending") {
|
|
237
|
+
const seen = await sendingElsewhereOrLost(deps, thread, intent);
|
|
238
|
+
throw new errors_1.SendPendingError(thread.id, seen.status === "unknown_outcome" ? "unknown" : "sending");
|
|
256
239
|
}
|
|
240
|
+
if (intent?.status === "unknown")
|
|
241
|
+
throw new errors_1.SendPendingError(thread.id, "unknown");
|
|
257
242
|
const updated = await (0, internal_1.applyEvents)(deps, thread, ["dismiss"], {
|
|
258
243
|
reviewReason: null,
|
|
259
244
|
dismissedBy: input.dismissedBy,
|
|
260
245
|
});
|
|
261
|
-
await stopDrafting(deps,
|
|
246
|
+
await stopDrafting(deps, thread.id);
|
|
262
247
|
await (0, reply_attachments_1.discardPendingReplyAttachments)(deps, thread.id);
|
|
263
248
|
return updated;
|
|
264
249
|
}
|
|
265
|
-
async function stopDrafting(deps,
|
|
250
|
+
async function stopDrafting(deps, threadId) {
|
|
266
251
|
try {
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
if (latest.status === "requested" || latest.status === "started") {
|
|
271
|
-
latest = await deps.store.updateDraftRun(latest.idempotencyKey, {
|
|
252
|
+
const latest = await deps.store.getLatestDraftRunForThread(threadId);
|
|
253
|
+
if (latest?.status === "requested" || latest?.status === "started") {
|
|
254
|
+
await deps.store.updateDraftRun(latest.idempotencyKey, {
|
|
272
255
|
status: "failed",
|
|
273
256
|
error: "stopped: staff marked the ticket as needing no reply",
|
|
274
257
|
});
|
|
275
258
|
}
|
|
276
|
-
const previous = latest.generation > 1
|
|
277
|
-
? await deps.store.getDraftRunGeneration(latest.mailbox, latest.inboundMessageId, latest.generation - 1)
|
|
278
|
-
: null;
|
|
279
|
-
for (const run of previous ? [latest, previous] : [latest]) {
|
|
280
|
-
await (0, cleanup_1.discardPlacedDraft)(deps, run, access, threadId);
|
|
281
|
-
}
|
|
282
259
|
}
|
|
283
260
|
catch {
|
|
284
261
|
}
|
package/dist/states.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TicketState } from "./types";
|
|
2
2
|
export declare const TICKET_STATES: readonly TicketState[];
|
|
3
|
-
export type TicketEvent = "ignore" | "draft" | "needs_review" | "
|
|
3
|
+
export type TicketEvent = "ignore" | "draft" | "needs_review" | "approve" | "redraft" | "settle" | "customer_reply" | "close" | "unignore" | "dismiss" | "bounce";
|
|
4
4
|
export declare const TICKET_EVENTS: readonly TicketEvent[];
|
|
5
5
|
export declare function transition(from: TicketState, event: TicketEvent): TicketState;
|
|
6
6
|
export declare function canTransition(from: TicketState, event: TicketEvent): boolean;
|
package/dist/states.js
CHANGED
|
@@ -18,7 +18,6 @@ exports.TICKET_EVENTS = [
|
|
|
18
18
|
"ignore",
|
|
19
19
|
"draft",
|
|
20
20
|
"needs_review",
|
|
21
|
-
"auto_send",
|
|
22
21
|
"approve",
|
|
23
22
|
"redraft",
|
|
24
23
|
"settle",
|
|
@@ -31,7 +30,7 @@ exports.TICKET_EVENTS = [
|
|
|
31
30
|
const TABLE = {
|
|
32
31
|
new: { ignore: "ignored", draft: "drafted", dismiss: "ignored" },
|
|
33
32
|
ignored: { unignore: "new" },
|
|
34
|
-
drafted: { needs_review: "review",
|
|
33
|
+
drafted: { needs_review: "review", dismiss: "ignored" },
|
|
35
34
|
review: { approve: "sent", redraft: "drafted", dismiss: "ignored" },
|
|
36
35
|
sent: { settle: "waiting", bounce: "review" },
|
|
37
36
|
waiting: { customer_reply: "new", close: "closed", bounce: "review" },
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface FakeMail {
|
|
|
26
26
|
isDraft?: boolean;
|
|
27
27
|
bodyText?: string;
|
|
28
28
|
attachments?: FakeAttachment[];
|
|
29
|
+
folder?: "inbox" | "sentitems";
|
|
30
|
+
to?: string[];
|
|
31
|
+
cc?: string[];
|
|
32
|
+
sentDateTime?: string;
|
|
29
33
|
}
|
|
30
34
|
export interface FakeAttachment {
|
|
31
35
|
id: string;
|
|
@@ -53,14 +57,12 @@ export interface FakeMailbox extends MailboxGateway {
|
|
|
53
57
|
sendBehaviour: SendBehaviour;
|
|
54
58
|
sent: string[];
|
|
55
59
|
drafts: Map<string, Message>;
|
|
56
|
-
deleteCalls: string[];
|
|
57
60
|
draftAttachments: Map<string, Array<{
|
|
58
61
|
name: string;
|
|
59
62
|
contentType: string;
|
|
60
63
|
size: number;
|
|
61
64
|
}>>;
|
|
62
65
|
addAttachmentError: Error | null;
|
|
63
|
-
deleteError: Error | null;
|
|
64
66
|
outage: Error | null;
|
|
65
67
|
getMessageCalls: number;
|
|
66
68
|
deltaCalls: DeltaOptions[];
|
package/dist/testing/index.js
CHANGED
|
@@ -63,7 +63,7 @@ function createMemoryStore() {
|
|
|
63
63
|
async getLatestMessageForThread(threadId) {
|
|
64
64
|
await tick();
|
|
65
65
|
const last = [...messages.values()]
|
|
66
|
-
.filter((m) => m.threadId === threadId)
|
|
66
|
+
.filter((m) => m.threadId === threadId && m.direction === "inbound")
|
|
67
67
|
.sort((a, b) => a.receivedAt.localeCompare(b.receivedAt))
|
|
68
68
|
.at(-1);
|
|
69
69
|
return last ? clone(last) : null;
|
|
@@ -132,11 +132,6 @@ function createMemoryStore() {
|
|
|
132
132
|
draftRuns.push(clone(run));
|
|
133
133
|
return { created: true, run: clone(run) };
|
|
134
134
|
},
|
|
135
|
-
async getDraftRunGeneration(mailbox, inboundMessageId, generation) {
|
|
136
|
-
await tick();
|
|
137
|
-
const r = draftRuns.find((x) => x.mailbox === mailbox && x.inboundMessageId === inboundMessageId && x.generation === generation);
|
|
138
|
-
return r ? clone(r) : null;
|
|
139
|
-
},
|
|
140
135
|
async getLatestDraftRun(mailbox, inboundMessageId) {
|
|
141
136
|
await tick();
|
|
142
137
|
const runs = draftRuns.filter((r) => r.mailbox === mailbox && r.inboundMessageId === inboundMessageId);
|
|
@@ -186,10 +181,11 @@ function createMemoryStore() {
|
|
|
186
181
|
},
|
|
187
182
|
};
|
|
188
183
|
}
|
|
189
|
-
const WRITE_METHODS = ["createReply", "updateDraftBody", "sendDraft"
|
|
184
|
+
const WRITE_METHODS = ["createReply", "updateDraftBody", "sendDraft"];
|
|
190
185
|
function createFakeMailbox(address) {
|
|
191
186
|
const mails = new Map();
|
|
192
|
-
const
|
|
187
|
+
const logs = { inbox: [], sentitems: [] };
|
|
188
|
+
const folderOf = (id) => (mails.get(id)?.folder ?? "inbox");
|
|
193
189
|
const drafts = new Map();
|
|
194
190
|
let expired = false;
|
|
195
191
|
let draftCounter = 0;
|
|
@@ -197,7 +193,7 @@ function createFakeMailbox(address) {
|
|
|
197
193
|
id: m.id,
|
|
198
194
|
removed: false,
|
|
199
195
|
...(m.subject !== undefined ? { subject: m.subject } : {}),
|
|
200
|
-
...(m.from ? { from: { address: m.from } } : {}),
|
|
196
|
+
...(m.from ? { from: { address: m.from } } : m.folder === "sentitems" ? { from: { address } } : {}),
|
|
201
197
|
...(m.receivedDateTime ? { receivedDateTime: m.receivedDateTime } : {}),
|
|
202
198
|
...(m.internetMessageId ? { internetMessageId: m.internetMessageId } : {}),
|
|
203
199
|
...(m.conversationId ? { conversationId: m.conversationId } : {}),
|
|
@@ -210,10 +206,8 @@ function createFakeMailbox(address) {
|
|
|
210
206
|
sent: [],
|
|
211
207
|
drafts,
|
|
212
208
|
getMessageCalls: 0,
|
|
213
|
-
deleteCalls: [],
|
|
214
209
|
draftAttachments: new Map(),
|
|
215
210
|
addAttachmentError: null,
|
|
216
|
-
deleteError: null,
|
|
217
211
|
outage: null,
|
|
218
212
|
deltaCalls: [],
|
|
219
213
|
calls: [],
|
|
@@ -222,16 +216,17 @@ function createFakeMailbox(address) {
|
|
|
222
216
|
},
|
|
223
217
|
deliver(mail) {
|
|
224
218
|
mails.set(mail.id, mail);
|
|
225
|
-
|
|
219
|
+
logs[mail.folder ?? "inbox"].push(summary(mail));
|
|
226
220
|
},
|
|
227
221
|
touch(id) {
|
|
228
222
|
const m = mails.get(id);
|
|
229
223
|
if (m)
|
|
230
|
-
|
|
224
|
+
logs[m.folder ?? "inbox"].push({ ...summary(m), isRead: true, categories: ["seen"] });
|
|
231
225
|
},
|
|
232
226
|
remove(id, reason = "deleted") {
|
|
227
|
+
const folder = folderOf(id);
|
|
233
228
|
mails.delete(id);
|
|
234
|
-
|
|
229
|
+
logs[folder].push({ id, removed: true, removedReason: reason });
|
|
235
230
|
},
|
|
236
231
|
expireLinks() {
|
|
237
232
|
expired = true;
|
|
@@ -244,11 +239,15 @@ function createFakeMailbox(address) {
|
|
|
244
239
|
throw box.outage;
|
|
245
240
|
const size = options.pageSize ?? 50;
|
|
246
241
|
let start = 0;
|
|
242
|
+
let folder = options.folderId === "sentitems" ? "sentitems" : "inbox";
|
|
247
243
|
if (options.link) {
|
|
248
244
|
if (expired)
|
|
249
245
|
throw new errors_1.DeltaLinkExpiredError();
|
|
250
|
-
|
|
246
|
+
folder = options.link.startsWith("s:") ? "sentitems" : "inbox";
|
|
247
|
+
start = Number(options.link.split(":").at(-1));
|
|
251
248
|
}
|
|
249
|
+
const log = logs[folder];
|
|
250
|
+
const prefix = folder === "sentitems" ? "s:" : "";
|
|
252
251
|
const since = options.since ? Date.parse(String(options.since)) : null;
|
|
253
252
|
const visible = (i) => Boolean(options.link) ||
|
|
254
253
|
since === null ||
|
|
@@ -260,8 +259,8 @@ function createFakeMailbox(address) {
|
|
|
260
259
|
const more = end < log.length;
|
|
261
260
|
return {
|
|
262
261
|
items: slice.filter(visible),
|
|
263
|
-
nextLink: more ?
|
|
264
|
-
deltaLink: more ? null :
|
|
262
|
+
nextLink: more ? `${prefix}p:${end}` : null,
|
|
263
|
+
deltaLink: more ? null : `${prefix}d:${end}`,
|
|
265
264
|
};
|
|
266
265
|
},
|
|
267
266
|
async getMessage(id, options) {
|
|
@@ -279,8 +278,9 @@ function createFakeMailbox(address) {
|
|
|
279
278
|
return {
|
|
280
279
|
...summary(m),
|
|
281
280
|
body: { contentType: "text", content: m.bodyText ?? "hello" },
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
...(m.sentDateTime ? { sentDateTime: m.sentDateTime } : {}),
|
|
282
|
+
toRecipients: (m.to ?? (m.folder === "sentitems" ? [] : [address])).map((a) => ({ address: a })),
|
|
283
|
+
ccRecipients: (m.cc ?? []).map((a) => ({ address: a })),
|
|
284
284
|
replyTo: (m.replyTo ?? []).map((address) => ({ address })),
|
|
285
285
|
...(options?.includeHeaders
|
|
286
286
|
? {
|
|
@@ -339,16 +339,6 @@ function createFakeMailbox(address) {
|
|
|
339
339
|
throw new Error(`no draft ${draftId}`);
|
|
340
340
|
d.body = clone(body);
|
|
341
341
|
},
|
|
342
|
-
async deleteDraft(draftId) {
|
|
343
|
-
await tick();
|
|
344
|
-
box.calls.push(`deleteDraft:${draftId}`);
|
|
345
|
-
box.deleteCalls.push(draftId);
|
|
346
|
-
if (box.deleteError)
|
|
347
|
-
throw box.deleteError;
|
|
348
|
-
if (box.sent.includes(draftId))
|
|
349
|
-
return { status: "not_draft" };
|
|
350
|
-
return { status: drafts.delete(draftId) ? "deleted" : "gone" };
|
|
351
|
-
},
|
|
352
342
|
async addAttachment(draftId, attachment) {
|
|
353
343
|
await tick();
|
|
354
344
|
box.calls.push(`addAttachment:${draftId}:${attachment.name}`);
|
package/dist/types.d.ts
CHANGED
|
@@ -78,9 +78,6 @@ export interface MailboxGateway {
|
|
|
78
78
|
contentType: string;
|
|
79
79
|
content: Uint8Array;
|
|
80
80
|
}): Promise<unknown>;
|
|
81
|
-
deleteDraft(draftId: string): Promise<{
|
|
82
|
-
status: "deleted" | "gone" | "not_draft";
|
|
83
|
-
}>;
|
|
84
81
|
}
|
|
85
82
|
export interface Clock {
|
|
86
83
|
now(): Date;
|
|
@@ -140,7 +137,7 @@ export interface ReplyAttachmentsDeps {
|
|
|
140
137
|
files: ReplyFiles;
|
|
141
138
|
}
|
|
142
139
|
export type TicketState = "new" | "ignored" | "drafted" | "review" | "sent" | "waiting" | "closed";
|
|
143
|
-
export type ReviewReason = "low_confidence" | "policy" | "stale_draft" | "drafter_failed" | "
|
|
140
|
+
export type ReviewReason = "low_confidence" | "policy" | "stale_draft" | "drafter_failed" | "staff_edited" | "no_draft" | "delivery_failed";
|
|
144
141
|
export interface Thread {
|
|
145
142
|
id: string;
|
|
146
143
|
mailbox: string;
|
|
@@ -164,6 +161,7 @@ export interface ThreadUpdate {
|
|
|
164
161
|
patch: ThreadPatch;
|
|
165
162
|
}
|
|
166
163
|
export type NoiseReason = "auto_submitted" | "precedence" | "bounce" | "out_of_office" | "mailing_list" | "machine_sender" | "own_address" | "connected_mailbox" | "none";
|
|
164
|
+
export type MessageDirection = "inbound" | "outbound";
|
|
167
165
|
export interface StoredMessage {
|
|
168
166
|
id: string;
|
|
169
167
|
mailbox: string;
|
|
@@ -179,6 +177,7 @@ export interface StoredMessage {
|
|
|
179
177
|
};
|
|
180
178
|
suppressAutoResponse: boolean;
|
|
181
179
|
removedAt: string | null;
|
|
180
|
+
direction: MessageDirection;
|
|
182
181
|
}
|
|
183
182
|
export interface SyncState {
|
|
184
183
|
link: string | null;
|
|
@@ -187,6 +186,8 @@ export interface SyncState {
|
|
|
187
186
|
failingSince?: string | null;
|
|
188
187
|
failures?: number;
|
|
189
188
|
lastError?: string | null;
|
|
189
|
+
sentLink?: string | null;
|
|
190
|
+
sentLastReceivedAt?: string | null;
|
|
190
191
|
}
|
|
191
192
|
export interface SyncFailing {
|
|
192
193
|
status: "failing";
|
|
@@ -212,8 +213,6 @@ export interface DraftRun {
|
|
|
212
213
|
placedDraftId: string | null;
|
|
213
214
|
placedBodyHtml: string | null;
|
|
214
215
|
commentText: string | null;
|
|
215
|
-
placementError: string | null;
|
|
216
|
-
draftCleanupError: string | null;
|
|
217
216
|
}
|
|
218
217
|
export type SendStatus = "intended" | "sending" | "sent" | "failed" | "unknown";
|
|
219
218
|
export interface SendIntent {
|
|
@@ -249,7 +248,6 @@ export interface HelpdeskStore {
|
|
|
249
248
|
run: DraftRun;
|
|
250
249
|
}>;
|
|
251
250
|
getLatestDraftRun(mailbox: string, inboundMessageId: string): Promise<DraftRun | null>;
|
|
252
|
-
getDraftRunGeneration(mailbox: string, inboundMessageId: string, generation: number): Promise<DraftRun | null>;
|
|
253
251
|
getLatestDraftRunForThread(threadId: string): Promise<DraftRun | null>;
|
|
254
252
|
listInFlightDraftRuns(mailbox: string, olderThan: string): Promise<DraftRun[]>;
|
|
255
253
|
updateDraftRun(key: string, patch: Partial<Omit<DraftRun, "idempotencyKey">>): Promise<DraftRun>;
|
|
@@ -319,7 +317,7 @@ export interface HelpdeskConfig {
|
|
|
319
317
|
minConfidence?: number;
|
|
320
318
|
pageSize?: number;
|
|
321
319
|
}
|
|
322
|
-
export type MailboxWriteMode = "read_only" | "
|
|
320
|
+
export type MailboxWriteMode = "read_only" | "approve_and_send";
|
|
323
321
|
export interface HelpdeskControls {
|
|
324
322
|
paused: boolean;
|
|
325
323
|
writeMode?: MailboxWriteMode;
|
package/package.json
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@dxv-systems/helpdesk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Mailbox helpdesk domain logic for DXV apps — idempotent delta ingestion, conservative threading, ticket state machine, noise detection,
|
|
5
|
-
"keywords": [
|
|
6
|
-
"dxv",
|
|
7
|
-
"helpdesk",
|
|
8
|
-
"mail",
|
|
9
|
-
"microsoft-graph",
|
|
10
|
-
"threading",
|
|
11
|
-
"tickets"
|
|
12
|
-
],
|
|
13
|
-
"engines": {
|
|
14
|
-
"node": ">=20.0.0"
|
|
15
|
-
},
|
|
16
|
-
"exports": {
|
|
17
|
-
".": {
|
|
18
|
-
"types": "./dist/index.d.ts",
|
|
19
|
-
"default": "./dist/index.js"
|
|
20
|
-
},
|
|
21
|
-
"./testing": {
|
|
22
|
-
"types": "./dist/testing/index.d.ts",
|
|
23
|
-
"default": "./dist/testing/index.js"
|
|
24
|
-
},
|
|
25
|
-
"./server": {
|
|
26
|
-
"types": "./dist/server.d.ts",
|
|
27
|
-
"default": "./dist/server.js"
|
|
28
|
-
},
|
|
29
|
-
"./react": {
|
|
30
|
-
"types": "./dist/react.d.ts",
|
|
31
|
-
"default": "./dist/react.js"
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"main": "./dist/index.js",
|
|
35
|
-
"types": "./dist/index.d.ts",
|
|
36
|
-
"files": [
|
|
37
|
-
"dist"
|
|
38
|
-
],
|
|
39
|
-
"sideEffects": false,
|
|
40
|
-
"scripts": {
|
|
41
|
-
"build": "tsc",
|
|
42
|
-
"test": "vitest run",
|
|
43
|
-
"test:watch": "vitest",
|
|
44
|
-
"prepublishOnly": "npm run build"
|
|
45
|
-
},
|
|
46
|
-
"peerDependencies": {
|
|
47
|
-
"react": ">=18"
|
|
48
|
-
},
|
|
49
|
-
"peerDependenciesMeta": {
|
|
50
|
-
"react": {
|
|
51
|
-
"optional": true
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"@testing-library/react": "^16.0.1",
|
|
56
|
-
"@types/node": "^20.19.0",
|
|
57
|
-
"@types/react": "^18.3.12",
|
|
58
|
-
"jsdom": "^25.0.1",
|
|
59
|
-
"react": "^18.3.1",
|
|
60
|
-
"react-dom": "^18.3.1",
|
|
61
|
-
"typescript": "^5.6.0",
|
|
62
|
-
"vitest": "^4.1.11"
|
|
63
|
-
},
|
|
64
|
-
"repository": {
|
|
65
|
-
"type": "git",
|
|
66
|
-
"url": "git+https://github.com/dxv-systems/dxv-packages.git",
|
|
67
|
-
"directory": "packages/helpdesk"
|
|
68
|
-
},
|
|
69
|
-
"publishConfig": {
|
|
70
|
-
"access": "public"
|
|
71
|
-
},
|
|
72
|
-
"license": "MIT"
|
|
73
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@dxv-systems/helpdesk",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Mailbox helpdesk domain logic for DXV apps — idempotent delta ingestion, conservative threading, ticket state machine, noise detection, and replies that a person approves in the helpdesk before they are sent from the mailbox (quoted history kept, retry-safe, never sent twice). Pure logic behind interfaces, plus the staff inbox as a route handler (./server) and a React component (./react). Zero runtime dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dxv",
|
|
7
|
+
"helpdesk",
|
|
8
|
+
"mail",
|
|
9
|
+
"microsoft-graph",
|
|
10
|
+
"threading",
|
|
11
|
+
"tickets"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.0.0"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./testing": {
|
|
22
|
+
"types": "./dist/testing/index.d.ts",
|
|
23
|
+
"default": "./dist/testing/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./server": {
|
|
26
|
+
"types": "./dist/server.d.ts",
|
|
27
|
+
"default": "./dist/server.js"
|
|
28
|
+
},
|
|
29
|
+
"./react": {
|
|
30
|
+
"types": "./dist/react.d.ts",
|
|
31
|
+
"default": "./dist/react.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"prepublishOnly": "npm run build"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"react": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@testing-library/react": "^16.0.1",
|
|
56
|
+
"@types/node": "^20.19.0",
|
|
57
|
+
"@types/react": "^18.3.12",
|
|
58
|
+
"jsdom": "^25.0.1",
|
|
59
|
+
"react": "^18.3.1",
|
|
60
|
+
"react-dom": "^18.3.1",
|
|
61
|
+
"typescript": "^5.6.0",
|
|
62
|
+
"vitest": "^4.1.11"
|
|
63
|
+
},
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "git+https://github.com/dxv-systems/dxv-packages.git",
|
|
67
|
+
"directory": "packages/helpdesk"
|
|
68
|
+
},
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public"
|
|
71
|
+
},
|
|
72
|
+
"license": "MIT"
|
|
73
|
+
}
|
package/dist/autosend.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { MailboxWriteMode, TicketState } from "./types";
|
|
2
|
-
export interface OfficeHours {
|
|
3
|
-
timeZone: string;
|
|
4
|
-
windows: Array<{
|
|
5
|
-
days: number[];
|
|
6
|
-
start: string;
|
|
7
|
-
end: string;
|
|
8
|
-
}>;
|
|
9
|
-
}
|
|
10
|
-
export interface AutoSendPolicy {
|
|
11
|
-
enabled: boolean;
|
|
12
|
-
categories?: string[];
|
|
13
|
-
senders?: string[];
|
|
14
|
-
minConfidence: number;
|
|
15
|
-
officeHours?: OfficeHours;
|
|
16
|
-
maxAutoRepliesPerThread: number;
|
|
17
|
-
internalDomains?: string[];
|
|
18
|
-
}
|
|
19
|
-
export declare const DEFAULT_AUTO_SEND_POLICY: AutoSendPolicy;
|
|
20
|
-
export interface AutoSendContext {
|
|
21
|
-
paused: boolean;
|
|
22
|
-
writeMode: MailboxWriteMode;
|
|
23
|
-
threadState: TicketState;
|
|
24
|
-
inboundIgnored: boolean;
|
|
25
|
-
sender: string;
|
|
26
|
-
category?: string | null;
|
|
27
|
-
confidence: number;
|
|
28
|
-
now: Date;
|
|
29
|
-
autoRepliesInThread: number;
|
|
30
|
-
suppressAutoResponse: boolean;
|
|
31
|
-
draftIsStale: boolean;
|
|
32
|
-
ownAddresses: string[];
|
|
33
|
-
connectedMailboxes: string[];
|
|
34
|
-
}
|
|
35
|
-
export type AutoSendDecision = {
|
|
36
|
-
allow: true;
|
|
37
|
-
reason: "all_rules_passed";
|
|
38
|
-
} | {
|
|
39
|
-
allow: false;
|
|
40
|
-
reason: "paused" | "write_mode" | "disabled" | "ignored_mail" | "internal_sender" | "auto_response_suppressed" | "wrong_state" | "stale_draft" | "no_scope_configured" | "sender_not_allowed" | "category_not_allowed" | "low_confidence" | "outside_office_hours" | "max_auto_replies";
|
|
41
|
-
detail?: string;
|
|
42
|
-
};
|
|
43
|
-
export declare function evaluateAutoSend(policy: AutoSendPolicy, ctx: AutoSendContext): AutoSendDecision;
|