@pegma/support-desk-application 0.2.3 → 0.2.4
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 +57 -11
- package/dist/assistant-turns.d.ts.map +1 -1
- package/dist/assistant-turns.js +7 -5
- package/dist/assistant-turns.js.map +1 -1
- package/dist/engineering-followup-source.d.ts +1 -1
- package/dist/engineering-followup-source.d.ts.map +1 -1
- package/dist/engineering-followup-source.js +15 -1
- package/dist/engineering-followup-source.js.map +1 -1
- package/dist/engineering-followup-types.d.ts +22 -1
- package/dist/engineering-followup-types.d.ts.map +1 -1
- package/dist/index.d.ts +25 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +220 -39
- package/dist/index.js.map +1 -1
- package/dist/resolution-learning.d.ts.map +1 -1
- package/dist/resolution-learning.js +54 -5
- package/dist/resolution-learning.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { createEngineeringFollowupSourceServices, planEngineeringFollowupSourceC
|
|
|
16
16
|
import { createEngineeringFollowupWorkServices } from "./engineering-followup-work.js";
|
|
17
17
|
import { fields as followupFields, integer as followupInteger, text as followupText, } from "./assistant-turn-records.js";
|
|
18
18
|
import { engineeringFollowupCommandKey, engineeringFollowupHeadKey, safeEngineeringFollowupSnapshot as followupSnapshot, checkEngineeringFollowupRecordCapacity, } from "./engineering-followup-records.js";
|
|
19
|
-
import { engineeringFollowupNow, engineeringFollowupVersionsCurrent, } from "./engineering-followup-source.js";
|
|
19
|
+
import { engineeringFollowupNow, engineeringFollowupTrusted, engineeringFollowupVersionsCurrent, } from "./engineering-followup-source.js";
|
|
20
20
|
import { assertResolutionLearningRecord, resolveResolutionLearningOptions, } from "./resolution-learning-records.js";
|
|
21
21
|
import { createResolutionLearningServices, createResolutionLearningSource, planResolutionLearningTicketMutation, } from "./resolution-learning.js";
|
|
22
22
|
export { resolutionLearningDeliveryBinding, resolutionLearningManifestHash, } from "./resolution-learning-worker.js";
|
|
@@ -861,6 +861,65 @@ function snapshotCreateCustomerTicketCommand(input) {
|
|
|
861
861
|
...(initialHandling === undefined ? {} : { initialHandling }),
|
|
862
862
|
});
|
|
863
863
|
}
|
|
864
|
+
function snapshotCustomerTicketAdmission(input) {
|
|
865
|
+
const source = boundaryObject(input, "admitted create command");
|
|
866
|
+
const allowed = [
|
|
867
|
+
"commandId",
|
|
868
|
+
"correlationId",
|
|
869
|
+
"ticketId",
|
|
870
|
+
"messageId",
|
|
871
|
+
"subject",
|
|
872
|
+
"body",
|
|
873
|
+
"category",
|
|
874
|
+
"requesterEmail",
|
|
875
|
+
"preference",
|
|
876
|
+
"disclosureVersion",
|
|
877
|
+
];
|
|
878
|
+
const keys = Reflect.ownKeys(source);
|
|
879
|
+
if (Array.isArray(source) ||
|
|
880
|
+
keys.some((key) => typeof key !== "string" || !allowed.includes(key)))
|
|
881
|
+
throw new TypeError("Invalid admitted create command fields");
|
|
882
|
+
const command = Object.freeze(Object.fromEntries(keys.map((key) => [
|
|
883
|
+
key,
|
|
884
|
+
ownDataProperty(source, key, "admitted create command field"),
|
|
885
|
+
])));
|
|
886
|
+
const preference = ownDataProperty(command, "preference", "preference");
|
|
887
|
+
if (preference !== "human" && preference !== "assistant_if_available")
|
|
888
|
+
throw new TypeError("Invalid admitted create preference");
|
|
889
|
+
const raw = ownDataProperty(command, "disclosureVersion", "disclosureVersion", true);
|
|
890
|
+
const disclosureVersion = raw === undefined ? undefined : boundaryString(raw, "disclosureVersion");
|
|
891
|
+
if (disclosureVersion !== undefined)
|
|
892
|
+
requireIdentifier(disclosureVersion, "disclosureVersion");
|
|
893
|
+
return {
|
|
894
|
+
command,
|
|
895
|
+
intent: Object.freeze({
|
|
896
|
+
preference,
|
|
897
|
+
...(disclosureVersion === undefined ? {} : { disclosureVersion }),
|
|
898
|
+
}),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function customerCreateMethods(create) {
|
|
902
|
+
return {
|
|
903
|
+
async createCustomerTicket(access, input) {
|
|
904
|
+
return (await create(access, input)).view;
|
|
905
|
+
},
|
|
906
|
+
async createAdmittedCustomerTicket(access, input, options) {
|
|
907
|
+
requirePermission(access, supportPermissions.create);
|
|
908
|
+
requirePermission(access, supportPermissions.readOwn);
|
|
909
|
+
const { command, intent } = snapshotCustomerTicketAdmission(input);
|
|
910
|
+
return create(access, command, {
|
|
911
|
+
intent,
|
|
912
|
+
...(options === undefined ? {} : { options }),
|
|
913
|
+
});
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
function admittedCreateMode(record) {
|
|
918
|
+
if (record?.kind !== "command" ||
|
|
919
|
+
(record.initialMode !== "human" && record.initialMode !== "assistant"))
|
|
920
|
+
throw new SupportDeskConflictError("Admitted create receipt mode is invalid");
|
|
921
|
+
return record.initialMode;
|
|
922
|
+
}
|
|
864
923
|
function snapshotReplyToCustomerTicketCommand(input) {
|
|
865
924
|
const source = boundaryObject(input, "reply command");
|
|
866
925
|
const raw = {
|
|
@@ -2408,9 +2467,12 @@ export function createSupportDeskApplication(options) {
|
|
|
2408
2467
|
};
|
|
2409
2468
|
const engineeringFollowupSource = createEngineeringFollowupSourceServices(followupDependencies);
|
|
2410
2469
|
const engineeringFollowupWork = createEngineeringFollowupWorkServices(followupDependencies, engineeringFollowupSource.internal.load);
|
|
2411
|
-
const approveAndPostEngineeringFollowupWebUpdate = async (access, input, guard) => {
|
|
2470
|
+
const approveAndPostEngineeringFollowupWebUpdate = async (access, input, guard, options = {}) => {
|
|
2412
2471
|
const actorId = access.principalId;
|
|
2472
|
+
const { signal, verifyBeforeCommit } = options;
|
|
2413
2473
|
const authorize = () => {
|
|
2474
|
+
if (signal?.aborted)
|
|
2475
|
+
throw new SupportDeskConflictError("Follow-up posting cancelled");
|
|
2414
2476
|
requireIdentifier(access.principalId, "principalId");
|
|
2415
2477
|
if (access.principalId !== actorId)
|
|
2416
2478
|
throw new SupportDeskAuthorizationError(supportPermissions.engineeringFollowupReview);
|
|
@@ -2495,7 +2557,7 @@ export function createSupportDeskApplication(options) {
|
|
|
2495
2557
|
requireNonempty(body, "body");
|
|
2496
2558
|
enforceLimit(body, "body", limits.maxMessageCharacters);
|
|
2497
2559
|
const bodyHash = await requestFingerprint({ body });
|
|
2498
|
-
await guard();
|
|
2560
|
+
const assertProofCurrent = await guard();
|
|
2499
2561
|
authorize();
|
|
2500
2562
|
const occurredAt = clampTicketTime(clock.now(), ticket.updatedAt);
|
|
2501
2563
|
const updated = applyTicketEvent(ticket, {
|
|
@@ -2566,8 +2628,22 @@ export function createSupportDeskApplication(options) {
|
|
|
2566
2628
|
if (!(await engineeringFollowupVersionsCurrent(followupDependencies, current.loaded, current.work, current.engineeringWork)))
|
|
2567
2629
|
continue;
|
|
2568
2630
|
authorize();
|
|
2569
|
-
|
|
2570
|
-
|
|
2631
|
+
const proofDeadline = Math.min(Date.parse(work.deadline), checked.deadline, Date.parse(current.proof.attestation.validUntil));
|
|
2632
|
+
if (now() >= proofDeadline)
|
|
2633
|
+
throw new SupportDeskConflictError("Follow-up posting proof expired");
|
|
2634
|
+
if (verifyBeforeCommit !== undefined) {
|
|
2635
|
+
const authorityDeadline = Math.min(proofDeadline, now() + engineeringFollowupOptions.trustTimeoutMilliseconds);
|
|
2636
|
+
await engineeringFollowupTrusted(followupDependencies, async () => {
|
|
2637
|
+
authorize();
|
|
2638
|
+
await verifyBeforeCommit();
|
|
2639
|
+
}, signal);
|
|
2640
|
+
authorize();
|
|
2641
|
+
if (now() >= authorityDeadline)
|
|
2642
|
+
throw new SupportDeskConflictError("Follow-up posting authority or proof expired");
|
|
2643
|
+
}
|
|
2644
|
+
if (assertProofCurrent !== undefined &&
|
|
2645
|
+
(typeof assertProofCurrent !== "function" ||
|
|
2646
|
+
assertProofCurrent() !== true))
|
|
2571
2647
|
throw new SupportDeskConflictError("Follow-up posting proof expired");
|
|
2572
2648
|
const outcome = await records.transact(command.ticketId, [
|
|
2573
2649
|
...derivations.actions,
|
|
@@ -2663,7 +2739,7 @@ export function createSupportDeskApplication(options) {
|
|
|
2663
2739
|
: { assistantTurnFingerprint: assistantTurnCommit.fingerprint }),
|
|
2664
2740
|
});
|
|
2665
2741
|
const completion = assistantTurnCommit;
|
|
2666
|
-
const parallel = completion?.kind === "complete";
|
|
2742
|
+
const parallel = completion?.kind === "complete" || completion?.kind === "finish";
|
|
2667
2743
|
for (let attempt = 0; attempt < maxConflictAttempts; attempt += 1) {
|
|
2668
2744
|
const ticketRead = records.getVersioned(ticketKey(command.ticketId));
|
|
2669
2745
|
const duplicateRead = parallel
|
|
@@ -2692,8 +2768,8 @@ export function createSupportDeskApplication(options) {
|
|
|
2692
2768
|
ticket.revision !== command.expectedRevision ||
|
|
2693
2769
|
effectiveTicketHandling(ticket).generation !==
|
|
2694
2770
|
command.expectedHandlingGeneration)) {
|
|
2695
|
-
// The speculative receipt may predate a concurrent
|
|
2696
|
-
//
|
|
2771
|
+
// The speculative receipt may predate a concurrent completion or
|
|
2772
|
+
// finish already reflected in this Ticket. Recover its exact receipt
|
|
2697
2773
|
// before rejecting the now-stale live fence; ordinary paired reads
|
|
2698
2774
|
// retain their existing cost and original whole-attempt CAS versions.
|
|
2699
2775
|
const accepted = await records.get(commandKey(command.ticketId, command.commandId));
|
|
@@ -3106,19 +3182,63 @@ export function createSupportDeskApplication(options) {
|
|
|
3106
3182
|
.sort((a, b) => a.revision - b.revision || a.commandId.localeCompare(b.commandId))
|
|
3107
3183
|
.map(({ kind: _kind, partition: _partition, id: _id, ...proof }) => Object.freeze(proof));
|
|
3108
3184
|
},
|
|
3109
|
-
async
|
|
3185
|
+
...customerCreateMethods(async (access, input, admitted) => {
|
|
3110
3186
|
requirePermission(access, supportPermissions.create);
|
|
3111
3187
|
requireIdentifier(access.principalId, "principalId");
|
|
3188
|
+
const actorId = access.principalId;
|
|
3189
|
+
const principal = () => admitted === undefined ? access.principalId : actorId;
|
|
3190
|
+
const signal = admitted?.options?.signal;
|
|
3191
|
+
function authorizeAdmitted(current = access, assistant = false) {
|
|
3192
|
+
if (admitted === undefined)
|
|
3193
|
+
return;
|
|
3194
|
+
if (signal?.aborted)
|
|
3195
|
+
throw new SupportDeskConflictError("Admitted create was cancelled");
|
|
3196
|
+
if (current.principalId !== actorId)
|
|
3197
|
+
throw new SupportDeskAuthorizationError(supportPermissions.create);
|
|
3198
|
+
requirePermission(current, supportPermissions.create);
|
|
3199
|
+
requirePermission(current, supportPermissions.readOwn);
|
|
3200
|
+
if (assistant)
|
|
3201
|
+
requirePermission(current, supportPermissions.assistantStart);
|
|
3202
|
+
}
|
|
3203
|
+
async function waitForAdmission(check) {
|
|
3204
|
+
authorizeAdmitted();
|
|
3205
|
+
if (signal === undefined)
|
|
3206
|
+
return check();
|
|
3207
|
+
let onAbort;
|
|
3208
|
+
try {
|
|
3209
|
+
return await Promise.race([
|
|
3210
|
+
Promise.resolve().then(() => {
|
|
3211
|
+
authorizeAdmitted();
|
|
3212
|
+
return check();
|
|
3213
|
+
}),
|
|
3214
|
+
new Promise((_resolve, reject) => {
|
|
3215
|
+
onAbort = () => reject(new SupportDeskConflictError("Admitted create was cancelled"));
|
|
3216
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3217
|
+
if (signal.aborted)
|
|
3218
|
+
onAbort();
|
|
3219
|
+
}),
|
|
3220
|
+
]);
|
|
3221
|
+
}
|
|
3222
|
+
finally {
|
|
3223
|
+
if (onAbort !== undefined)
|
|
3224
|
+
signal.removeEventListener("abort", onAbort);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
authorizeAdmitted();
|
|
3112
3228
|
const command = snapshotCreateCustomerTicketCommand(input);
|
|
3113
|
-
|
|
3229
|
+
let initialHandling = command.initialHandling;
|
|
3230
|
+
if (admitted === undefined && initialHandling === "assistant") {
|
|
3114
3231
|
requirePermission(access, supportPermissions.assistantStart);
|
|
3115
3232
|
}
|
|
3116
3233
|
requireIdentifier(command.ticketId, "ticketId");
|
|
3117
3234
|
requireIdentifier(command.messageId, "messageId");
|
|
3118
3235
|
requireIdentifier(command.commandId, "commandId");
|
|
3119
3236
|
requireIdentifier(command.correlationId, "correlationId");
|
|
3120
|
-
|
|
3121
|
-
|
|
3237
|
+
// Admitted replay keeps fixed ceilings when later instance limits shrink.
|
|
3238
|
+
// Fresh admission applies the configured limits after receipt recovery.
|
|
3239
|
+
const inputLimits = admitted === undefined ? limits : supportDeskLimitMaximums;
|
|
3240
|
+
enforceLimit(command.subject, "subject", inputLimits.maxSubjectCharacters);
|
|
3241
|
+
enforceLimit(command.body, "body", inputLimits.maxMessageCharacters);
|
|
3122
3242
|
const fingerprintPayload = {
|
|
3123
3243
|
type: "create_customer_ticket",
|
|
3124
3244
|
ticketId: command.ticketId,
|
|
@@ -3135,7 +3255,20 @@ export function createSupportDeskApplication(options) {
|
|
|
3135
3255
|
? {}
|
|
3136
3256
|
: { initialHandling: command.initialHandling }),
|
|
3137
3257
|
};
|
|
3138
|
-
const
|
|
3258
|
+
const commandType = admitted === undefined
|
|
3259
|
+
? "create_customer_ticket"
|
|
3260
|
+
: "create_admitted_customer_ticket";
|
|
3261
|
+
const fingerprint = await requestFingerprint(admitted === undefined
|
|
3262
|
+
? fingerprintPayload
|
|
3263
|
+
: {
|
|
3264
|
+
...fingerprintPayload,
|
|
3265
|
+
type: commandType,
|
|
3266
|
+
version: 1,
|
|
3267
|
+
actorId,
|
|
3268
|
+
commandId: command.commandId,
|
|
3269
|
+
preference: admitted.intent.preference,
|
|
3270
|
+
disclosureVersion: admitted.intent.disclosureVersion ?? null,
|
|
3271
|
+
});
|
|
3139
3272
|
const createThreading = {
|
|
3140
3273
|
ticketNumber: true,
|
|
3141
3274
|
...(command.notification === undefined
|
|
@@ -3146,8 +3279,10 @@ export function createSupportDeskApplication(options) {
|
|
|
3146
3279
|
// later config change cannot break idempotent retries of the same command.
|
|
3147
3280
|
// Replay never reserves another instance ticket number.
|
|
3148
3281
|
const existingCreate = await records.get(commandKey(command.ticketId, command.commandId));
|
|
3149
|
-
|
|
3150
|
-
|
|
3282
|
+
authorizeAdmitted();
|
|
3283
|
+
let createIsReplay = duplicateMatches(existingCreate, commandType, command.messageId, fingerprint);
|
|
3284
|
+
if (admitted === undefined &&
|
|
3285
|
+
!createIsReplay &&
|
|
3151
3286
|
command.initialHandling === undefined &&
|
|
3152
3287
|
existingCreate?.kind === "command" &&
|
|
3153
3288
|
existingCreate.commandType === "create_customer_ticket" &&
|
|
@@ -3174,19 +3309,28 @@ export function createSupportDeskApplication(options) {
|
|
|
3174
3309
|
}
|
|
3175
3310
|
if (createIsReplay) {
|
|
3176
3311
|
const current = await records.get(ticketKey(command.ticketId));
|
|
3177
|
-
|
|
3178
|
-
|
|
3312
|
+
authorizeAdmitted();
|
|
3313
|
+
if (current?.kind !== "ticket" || !owns(current.ticket, principal()))
|
|
3179
3314
|
throw new SupportDeskNotFoundError();
|
|
3315
|
+
const initialMode = admitted === undefined
|
|
3316
|
+
? (initialHandling ?? "human")
|
|
3317
|
+
: admittedCreateMode(existingCreate);
|
|
3180
3318
|
// A crash after ticket commit and before index confirmation must still
|
|
3181
3319
|
// converge the principal hint on retry.
|
|
3182
3320
|
const fence = await records.get(ticketReservationKey(command.ticketId));
|
|
3321
|
+
authorizeAdmitted();
|
|
3183
3322
|
if (fence?.kind === "ticket_reservation" &&
|
|
3184
3323
|
fence.state === "committed") {
|
|
3185
|
-
await confirmCustomerTicket(
|
|
3324
|
+
await confirmCustomerTicket(principal(), command.ticketId, fence.token, fence.generation);
|
|
3186
3325
|
}
|
|
3187
|
-
|
|
3326
|
+
authorizeAdmitted();
|
|
3327
|
+
const view = await customerViewAfterChange(principal(), command.ticketId, createThreading);
|
|
3328
|
+
authorizeAdmitted();
|
|
3329
|
+
return { view, initialMode };
|
|
3188
3330
|
}
|
|
3189
|
-
if (
|
|
3331
|
+
if (admitted !== undefined && existingCreate !== null)
|
|
3332
|
+
throw new SupportDeskConflictError("Admitted create command changed");
|
|
3333
|
+
if (initialHandling === "assistant" && !assistantHandlingEnabled)
|
|
3190
3334
|
throw new SupportDeskConflictError("Assistant handling is disabled");
|
|
3191
3335
|
if (command.category !== undefined) {
|
|
3192
3336
|
if (allowedCategories === undefined ||
|
|
@@ -3194,6 +3338,29 @@ export function createSupportDeskApplication(options) {
|
|
|
3194
3338
|
throw new TypeError("create command.category is not configured for this Support Desk instance");
|
|
3195
3339
|
}
|
|
3196
3340
|
}
|
|
3341
|
+
const verifyBeforeCommit = admitted?.options?.verifyBeforeCommit;
|
|
3342
|
+
let assistantAccess;
|
|
3343
|
+
if (admitted !== undefined) {
|
|
3344
|
+
enforceLimit(command.subject, "subject", limits.maxSubjectCharacters);
|
|
3345
|
+
enforceLimit(command.body, "body", limits.maxMessageCharacters);
|
|
3346
|
+
if (typeof verifyBeforeCommit !== "function")
|
|
3347
|
+
throw new SupportDeskConflictError("Fresh create admission is required");
|
|
3348
|
+
const resolveAssistantStart = admitted.options?.resolveAssistantStart;
|
|
3349
|
+
if (admitted.intent.preference === "assistant_if_available" &&
|
|
3350
|
+
admitted.intent.disclosureVersion !== undefined &&
|
|
3351
|
+
assistantHandlingEnabled &&
|
|
3352
|
+
assistantTurnOptions !== undefined &&
|
|
3353
|
+
resolveAssistantStart !== undefined) {
|
|
3354
|
+
const current = await waitForAdmission(resolveAssistantStart);
|
|
3355
|
+
authorizeAdmitted();
|
|
3356
|
+
if (current !== null) {
|
|
3357
|
+
authorizeAdmitted(current, true);
|
|
3358
|
+
assistantAccess = current;
|
|
3359
|
+
initialHandling = "assistant";
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
let initialMode = initialHandling ?? "human";
|
|
3197
3364
|
// Fail closed before durable number/index reservation so a too-small
|
|
3198
3365
|
// partition budget cannot exhaust the principal ticket index with
|
|
3199
3366
|
// reserved-but-never-created tickets.
|
|
@@ -3201,18 +3368,17 @@ export function createSupportDeskApplication(options) {
|
|
|
3201
3368
|
(resolutionLearningOptions.enabled ? 1 : 0) +
|
|
3202
3369
|
(engineeringOptions.enabled ? 1 : 0) +
|
|
3203
3370
|
(command.notification === undefined ? 0 : 1) +
|
|
3204
|
-
(assistantTurnOptions !== undefined &&
|
|
3205
|
-
command.initialHandling === "assistant"
|
|
3371
|
+
(assistantTurnOptions !== undefined && initialHandling === "assistant"
|
|
3206
3372
|
? 1
|
|
3207
3373
|
: 0);
|
|
3208
3374
|
enforcePartitionRecordLimit(0, limits.maxRecordsPerTicket, estimatedCreateRows +
|
|
3209
3375
|
(resolutionLearningOptions.enabled ? 2 : 0) +
|
|
3210
3376
|
(engineeringOptions.enabled ? 2 : 0) +
|
|
3211
|
-
(
|
|
3377
|
+
(initialHandling === "assistant"
|
|
3212
3378
|
? assistantHumanReserveRows +
|
|
3213
3379
|
(resolutionLearningOptions.enabled ? 1 : 0)
|
|
3214
3380
|
: 0));
|
|
3215
|
-
if (
|
|
3381
|
+
if (initialHandling === "assistant" &&
|
|
3216
3382
|
limits.maxMessagesPerTicket < 1 + assistantHumanReserveMessages)
|
|
3217
3383
|
throw new SupportDeskLimitError("ticket_messages", limits.maxMessagesPerTicket);
|
|
3218
3384
|
// Reserve outside the ticket transaction; gaps are accepted if create fails.
|
|
@@ -3228,15 +3394,13 @@ export function createSupportDeskApplication(options) {
|
|
|
3228
3394
|
number: ticketNumber,
|
|
3229
3395
|
subject: command.subject,
|
|
3230
3396
|
channel: "web",
|
|
3231
|
-
...(
|
|
3232
|
-
? {}
|
|
3233
|
-
: { initialHandling: command.initialHandling }),
|
|
3397
|
+
...(initialHandling === undefined ? {} : { initialHandling }),
|
|
3234
3398
|
...(command.category === undefined
|
|
3235
3399
|
? {}
|
|
3236
3400
|
: { category: command.category }),
|
|
3237
3401
|
requester: {
|
|
3238
3402
|
association: "authenticated",
|
|
3239
|
-
principalId:
|
|
3403
|
+
principalId: principal(),
|
|
3240
3404
|
...(command.requesterEmail === undefined
|
|
3241
3405
|
? {}
|
|
3242
3406
|
: { email: command.requesterEmail }),
|
|
@@ -3247,22 +3411,21 @@ export function createSupportDeskApplication(options) {
|
|
|
3247
3411
|
id: command.messageId,
|
|
3248
3412
|
ticketId: command.ticketId,
|
|
3249
3413
|
authorKind: "customer",
|
|
3250
|
-
authorPrincipalId:
|
|
3414
|
+
authorPrincipalId: principal(),
|
|
3251
3415
|
channel: "web",
|
|
3252
3416
|
visibility: "customer",
|
|
3253
3417
|
format: "plain_text",
|
|
3254
3418
|
body: command.body,
|
|
3255
3419
|
createdAt: now,
|
|
3256
3420
|
};
|
|
3257
|
-
const assistantTurn = assistantTurnOptions !== undefined &&
|
|
3258
|
-
command.initialHandling === "assistant"
|
|
3421
|
+
const assistantTurn = assistantTurnOptions !== undefined && initialHandling === "assistant"
|
|
3259
3422
|
? await createAssistantTurnRecord(ticket, message, command.correlationId, assistantTurnOptions)
|
|
3260
3423
|
: undefined;
|
|
3261
3424
|
const notificationJob = notification === undefined
|
|
3262
3425
|
? undefined
|
|
3263
3426
|
: deliveryJobAction(command.ticketId, command.messageId, now, notification);
|
|
3264
3427
|
const notificationContent = deliveryContent(notification);
|
|
3265
|
-
const reservation = await reserveCustomerTicket(
|
|
3428
|
+
const reservation = await reserveCustomerTicket(principal(), command.ticketId, now);
|
|
3266
3429
|
const existingReservation = await records.getVersioned(ticketReservationKey(command.ticketId));
|
|
3267
3430
|
if (existingReservation?.value.kind === "ticket_reservation" &&
|
|
3268
3431
|
existingReservation.value.state === "committed" &&
|
|
@@ -3332,7 +3495,7 @@ export function createSupportDeskApplication(options) {
|
|
|
3332
3495
|
partition: command.ticketId,
|
|
3333
3496
|
id: "quota",
|
|
3334
3497
|
messageCount: 1,
|
|
3335
|
-
...(
|
|
3498
|
+
...(initialHandling === "assistant"
|
|
3336
3499
|
? { assistantManaged: true }
|
|
3337
3500
|
: {}),
|
|
3338
3501
|
},
|
|
@@ -3356,7 +3519,7 @@ export function createSupportDeskApplication(options) {
|
|
|
3356
3519
|
correlationId: command.correlationId,
|
|
3357
3520
|
ticketId: command.ticketId,
|
|
3358
3521
|
revision: ticket.revision,
|
|
3359
|
-
actorId:
|
|
3522
|
+
actorId: principal(),
|
|
3360
3523
|
occurredAt: now,
|
|
3361
3524
|
action: supportTicketAuditActions.created,
|
|
3362
3525
|
}),
|
|
@@ -3367,10 +3530,11 @@ export function createSupportDeskApplication(options) {
|
|
|
3367
3530
|
partition: command.ticketId,
|
|
3368
3531
|
id: `command:${command.commandId}`,
|
|
3369
3532
|
commandId: command.commandId,
|
|
3370
|
-
commandType
|
|
3533
|
+
commandType,
|
|
3371
3534
|
messageId: command.messageId,
|
|
3372
3535
|
requestFingerprint: fingerprint,
|
|
3373
3536
|
completedAt: now,
|
|
3537
|
+
...(admitted === undefined ? {} : { initialMode }),
|
|
3374
3538
|
},
|
|
3375
3539
|
},
|
|
3376
3540
|
...(notificationJob === undefined ? [] : [notificationJob]),
|
|
@@ -3381,16 +3545,33 @@ export function createSupportDeskApplication(options) {
|
|
|
3381
3545
|
// Create starts an empty partition; fail closed if the first write would
|
|
3382
3546
|
// exceed the configured physical-record budget.
|
|
3383
3547
|
enforcePartitionRecordLimit(0, limits.maxRecordsPerTicket, actions.length);
|
|
3548
|
+
if (admitted !== undefined && verifyBeforeCommit !== undefined) {
|
|
3549
|
+
authorizeAdmitted();
|
|
3550
|
+
const assertCurrent = await waitForAdmission(() => verifyBeforeCommit(initialMode));
|
|
3551
|
+
authorizeAdmitted();
|
|
3552
|
+
if (assistantAccess !== undefined)
|
|
3553
|
+
authorizeAdmitted(assistantAccess, true);
|
|
3554
|
+
if (typeof assertCurrent !== "function" || assertCurrent() !== true)
|
|
3555
|
+
throw new SupportDeskConflictError("Admitted create authority changed");
|
|
3556
|
+
}
|
|
3384
3557
|
const outcome = await records.transact(command.ticketId, actions);
|
|
3385
3558
|
if (!outcome.committed) {
|
|
3386
3559
|
const existing = await records.get(commandKey(command.ticketId, command.commandId));
|
|
3387
|
-
if (!duplicateMatches(existing,
|
|
3560
|
+
if (!duplicateMatches(existing, commandType, command.messageId, fingerprint)) {
|
|
3388
3561
|
throw new SupportDeskConflictError();
|
|
3389
3562
|
}
|
|
3563
|
+
// An exact competing commit recovers the winner's historical mode,
|
|
3564
|
+
// using the same create/read-own authority as the early receipt path.
|
|
3565
|
+
if (admitted !== undefined)
|
|
3566
|
+
initialMode = admittedCreateMode(existing);
|
|
3390
3567
|
}
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3568
|
+
authorizeAdmitted();
|
|
3569
|
+
await confirmCustomerTicket(principal(), command.ticketId, reservation.reservationToken, reservation.reservationGeneration);
|
|
3570
|
+
authorizeAdmitted();
|
|
3571
|
+
const view = await customerViewAfterChange(principal(), command.ticketId, createThreading);
|
|
3572
|
+
authorizeAdmitted();
|
|
3573
|
+
return { view, initialMode };
|
|
3574
|
+
}),
|
|
3394
3575
|
async replyToCustomerTicket(access, input) {
|
|
3395
3576
|
requirePermission(access, supportPermissions.replyOwn);
|
|
3396
3577
|
requireIdentifier(access.principalId, "principalId");
|