@consilioweb/payload-support 2.0.0 → 3.0.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 +656 -355
- package/dist/components/RichTextEditor/index.d.ts +19 -0
- package/dist/components/TicketConversation/RewriteDropdown.d.ts +6 -0
- package/dist/components/TicketConversation/SkeletonText.d.ts +4 -0
- package/dist/components/TicketConversation/components/AISummaryPanel.d.ts +14 -0
- package/dist/components/TicketConversation/components/ActionPanels.d.ts +47 -0
- package/dist/components/TicketConversation/components/ActivityLog.d.ts +5 -0
- package/dist/components/TicketConversation/components/ClientBar.d.ts +5 -0
- package/dist/components/TicketConversation/components/ClientHistory.d.ts +24 -0
- package/dist/components/TicketConversation/components/CodeBlock.d.ts +26 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.d.ts +12 -0
- package/dist/components/TicketConversation/components/QuickActions.d.ts +32 -0
- package/dist/components/TicketConversation/components/TicketHeader.d.ts +19 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.d.ts +24 -0
- package/dist/components/TicketConversation/config.d.ts +10 -0
- package/dist/components/TicketConversation/config.js +1 -40
- package/dist/components/TicketConversation/constants.d.ts +56 -0
- package/dist/components/TicketConversation/context.d.ts +53 -0
- package/dist/components/TicketConversation/hooks/useAI.d.ts +21 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.d.ts +3 -0
- package/dist/components/TicketConversation/hooks/useFeatures.d.ts +23 -0
- package/dist/components/TicketConversation/hooks/useFeatures.js +61 -0
- package/dist/components/TicketConversation/hooks/useMessageActions.d.ts +20 -0
- package/dist/components/TicketConversation/hooks/useReply.d.ts +27 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.d.ts +58 -0
- package/dist/components/TicketConversation/hooks/useTimeTracking.d.ts +18 -0
- package/dist/components/TicketConversation/hooks/useTranslation.d.ts +33 -0
- package/dist/components/TicketConversation/hooks/useTranslation.js +22 -3
- package/dist/components/TicketConversation/index.d.ts +4 -0
- package/dist/components/TicketConversation/index.js +2 -2
- package/dist/components/TicketConversation/types.d.ts +56 -0
- package/dist/components/TicketConversation/utils.d.ts +2 -0
- package/dist/index.cjs +392 -320
- package/dist/index.d.cts +100 -3
- package/dist/index.d.ts +100 -3
- package/dist/index.js +388 -321
- package/dist/utils/features.d.ts +123 -0
- package/dist/utils/features.js +147 -0
- package/dist/views/BillingView/client.d.ts +2 -0
- package/dist/views/BillingView/index.d.ts +4 -0
- package/dist/views/ChatView/client.d.ts +2 -0
- package/dist/views/ChatView/client.js +9 -9
- package/dist/views/ChatView/index.d.ts +4 -0
- package/dist/views/CrmView/client.d.ts +2 -0
- package/dist/views/CrmView/client.js +6 -9
- package/dist/views/CrmView/index.d.ts +4 -0
- package/dist/views/EmailTrackingView/client.d.ts +2 -0
- package/dist/views/EmailTrackingView/index.d.ts +4 -0
- package/dist/views/ImportConversationView/client.d.ts +2 -0
- package/dist/views/ImportConversationView/index.d.ts +4 -0
- package/dist/views/LogsView/client.d.ts +2 -0
- package/dist/views/LogsView/index.d.ts +4 -0
- package/dist/views/NewTicketView/client.d.ts +2 -0
- package/dist/views/NewTicketView/index.d.ts +4 -0
- package/dist/views/PendingEmailsView/client.d.ts +2 -0
- package/dist/views/PendingEmailsView/index.d.ts +4 -0
- package/dist/views/SupportDashboardView/client.d.ts +2 -0
- package/dist/views/SupportDashboardView/client.js +1 -1
- package/dist/views/SupportDashboardView/index.d.ts +4 -0
- package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
- package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
- package/dist/views/TicketDetailView/client.d.ts +2 -0
- package/dist/views/TicketDetailView/client.js +2 -2
- package/dist/views/TicketDetailView/constants.d.ts +13 -0
- package/dist/views/TicketDetailView/helpers.d.ts +2 -0
- package/dist/views/TicketDetailView/index.d.ts +4 -0
- package/dist/views/TicketDetailView/types.d.ts +40 -0
- package/dist/views/TicketInboxView/client.d.ts +2 -0
- package/dist/views/TicketInboxView/client.js +5 -5
- package/dist/views/TicketInboxView/index.d.ts +4 -0
- package/dist/views/TicketingSettingsView/client.d.ts +2 -0
- package/dist/views/TicketingSettingsView/client.js +6 -5
- package/dist/views/TicketingSettingsView/index.d.ts +4 -0
- package/dist/views/TimeDashboardView/client.d.ts +2 -0
- package/dist/views/TimeDashboardView/index.d.ts +4 -0
- package/dist/views/shared/AdminViewHeader.d.ts +13 -0
- package/dist/views/shared/ErrorBoundary.d.ts +17 -0
- package/dist/views/shared/Skeleton.d.ts +21 -0
- package/dist/views/shared/StatusPill.d.ts +8 -0
- package/dist/views/shared/adminTokens.d.ts +19 -0
- package/dist/views/shared/config.d.ts +8 -0
- package/dist/views/shared/config.js +1 -40
- package/dist/views/shared/dateLocale.d.ts +5 -0
- package/dist/views/shared/index.d.ts +12 -0
- package/dist/views/shared/index.js +2 -1
- package/dist/views/shared/locales/en.json +708 -0
- package/dist/views/shared/locales/fr.json +708 -0
- package/dist/views/shared/sla.d.ts +14 -0
- package/dist/views.d.ts +13 -13
- package/package.json +26 -41
- package/src/collections/SupportClients.ts +29 -0
- package/src/collections/TicketCollaborators.ts +6 -1
- package/src/collections/TicketMessages.ts +46 -24
- package/src/collections/Tickets.ts +117 -79
- package/src/collections/TimeEntries.ts +57 -32
- package/src/components/TicketConversation/config.ts +16 -80
- package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +38 -4
- package/src/components/TicketConversation/index.tsx +2 -2
- package/src/endpoints/ai.ts +2 -1
- package/src/endpoints/client-intelligence.ts +2 -1
- package/src/endpoints/process-scheduled.ts +13 -8
- package/src/endpoints/round-robin-config.ts +30 -20
- package/src/endpoints/settings.ts +28 -77
- package/src/index.ts +6 -2
- package/src/portal/LiveChat.tsx +33 -22
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
- package/src/types/lucide-react.d.ts +10 -2
- package/src/utils/aiAgent.ts +2 -1
- package/src/utils/aiProvider.ts +21 -0
- package/src/utils/features.ts +297 -0
- package/src/utils/fireWebhooks.ts +6 -0
- package/src/utils/generateTicketSynthesis.ts +2 -1
- package/src/utils/index.ts +4 -2
- package/src/utils/readSettings.ts +82 -9
- package/src/utils/webhookDispatcher.ts +19 -7
- package/src/views/ChatView/client.tsx +13 -9
- package/src/views/CrmView/client.tsx +10 -11
- package/src/views/SupportDashboardView/client.tsx +1 -1
- package/src/views/TicketDetailView/client.tsx +2 -2
- package/src/views/TicketInboxView/client.tsx +10 -5
- package/src/views/TicketingSettingsView/client.tsx +9 -7
- package/src/views/shared/config.ts +14 -80
- package/src/views/shared/index.ts +3 -1
- package/src/views/shared/locales/en.json +2 -2
- package/src/views/shared/locales/fr.json +2 -2
- package/dist/components/RichTextEditor/index.cjs +0 -279
- package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
- package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
- package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
- package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
- package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
- package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
- package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
- package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
- package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
- package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
- package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
- package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
- package/dist/components/TicketConversation/config.cjs +0 -44
- package/dist/components/TicketConversation/constants.cjs +0 -127
- package/dist/components/TicketConversation/context.cjs +0 -13
- package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
- package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
- package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
- package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
- package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
- package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -78
- package/dist/components/TicketConversation/index.cjs +0 -1114
- package/dist/components/TicketConversation/types.cjs +0 -2
- package/dist/components/TicketConversation/utils.cjs +0 -27
- package/dist/views/BillingView/client.cjs +0 -469
- package/dist/views/BillingView/index.cjs +0 -34
- package/dist/views/ChatView/client.cjs +0 -301
- package/dist/views/ChatView/index.cjs +0 -34
- package/dist/views/CrmView/client.cjs +0 -458
- package/dist/views/CrmView/index.cjs +0 -34
- package/dist/views/EmailTrackingView/client.cjs +0 -171
- package/dist/views/EmailTrackingView/index.cjs +0 -34
- package/dist/views/ImportConversationView/client.cjs +0 -238
- package/dist/views/ImportConversationView/index.cjs +0 -34
- package/dist/views/LogsView/client.cjs +0 -148
- package/dist/views/LogsView/index.cjs +0 -32
- package/dist/views/NewTicketView/client.cjs +0 -213
- package/dist/views/NewTicketView/index.cjs +0 -32
- package/dist/views/PendingEmailsView/client.cjs +0 -471
- package/dist/views/PendingEmailsView/index.cjs +0 -34
- package/dist/views/SupportDashboardView/client.cjs +0 -437
- package/dist/views/SupportDashboardView/index.cjs +0 -34
- package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
- package/dist/views/TicketDetailView/client.cjs +0 -1412
- package/dist/views/TicketDetailView/constants.cjs +0 -20
- package/dist/views/TicketDetailView/helpers.cjs +0 -23
- package/dist/views/TicketDetailView/index.cjs +0 -34
- package/dist/views/TicketDetailView/types.cjs +0 -2
- package/dist/views/TicketInboxView/client.cjs +0 -359
- package/dist/views/TicketInboxView/index.cjs +0 -32
- package/dist/views/TicketingSettingsView/client.cjs +0 -740
- package/dist/views/TicketingSettingsView/index.cjs +0 -34
- package/dist/views/TimeDashboardView/client.cjs +0 -180
- package/dist/views/TimeDashboardView/index.cjs +0 -34
- package/dist/views/shared/AdminViewHeader.cjs +0 -68
- package/dist/views/shared/ErrorBoundary.cjs +0 -55
- package/dist/views/shared/Skeleton.cjs +0 -77
- package/dist/views/shared/StatusPill.cjs +0 -47
- package/dist/views/shared/adminTokens.cjs +0 -34
- package/dist/views/shared/config.cjs +0 -44
- package/dist/views/shared/dateLocale.cjs +0 -5
- package/dist/views/shared/index.cjs +0 -72
- package/dist/views/shared/sla.cjs +0 -37
- package/dist/views.cjs +0 -86
- package/dist/views.d.cts +0 -13
- package/src/__tests__/aiSummaryRendering.test.ts +0 -21
- package/src/__tests__/authResponses.test.ts +0 -69
- package/src/__tests__/capabilities.test.ts +0 -71
- package/src/__tests__/generateTrackingToken.test.ts +0 -67
- package/src/__tests__/integration/ai-agent.test.ts +0 -46
- package/src/__tests__/integration/auth.test.ts +0 -43
- package/src/__tests__/integration/automation-rules.test.ts +0 -68
- package/src/__tests__/integration/buildTestPayload.ts +0 -52
- package/src/__tests__/integration/channels.test.ts +0 -52
- package/src/__tests__/integration/core-behaviors.test.ts +0 -81
- package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
- package/src/__tests__/integration/digest.test.ts +0 -48
- package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
- package/src/__tests__/integration/isolation.test.ts +0 -81
- package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
- package/src/__tests__/integration/nps.test.ts +0 -37
- package/src/__tests__/integration/per-team-sla.test.ts +0 -44
- package/src/__tests__/integration/push.test.ts +0 -75
- package/src/__tests__/integration/sanitization.test.ts +0 -35
- package/src/__tests__/integration/sla-pause.test.ts +0 -39
- package/src/__tests__/integration/smoke.test.ts +0 -26
- package/src/__tests__/integration/snooze.test.ts +0 -68
- package/src/__tests__/integration/teams.test.ts +0 -38
- package/src/__tests__/rateLimiter.test.ts +0 -83
- package/src/__tests__/sanitizeHtml.test.ts +0 -165
- package/src/__tests__/sla.test.ts +0 -258
- package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
- package/src/__tests__/webhookSecurity.test.ts +0 -58
package/dist/index.js
CHANGED
|
@@ -368,14 +368,81 @@ function dbDelete(payload, slug, options) {
|
|
|
368
368
|
return payload.delete({ collection: slug, ...options });
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
// src/utils/features.ts
|
|
372
|
+
var DEFAULT_TICKETING_FEATURES = {
|
|
373
|
+
timeTracking: true,
|
|
374
|
+
ai: true,
|
|
375
|
+
satisfaction: true,
|
|
376
|
+
chat: true,
|
|
377
|
+
emailTracking: true,
|
|
378
|
+
canned: true,
|
|
379
|
+
merge: true,
|
|
380
|
+
snooze: true,
|
|
381
|
+
externalMessages: true,
|
|
382
|
+
clientHistory: true,
|
|
383
|
+
activityLog: true,
|
|
384
|
+
splitTicket: true,
|
|
385
|
+
scheduledReplies: true,
|
|
386
|
+
autoClose: true,
|
|
387
|
+
autoCloseDays: 7,
|
|
388
|
+
roundRobin: false
|
|
389
|
+
};
|
|
390
|
+
var BOOLEAN_FEATURE_KEYS = [
|
|
391
|
+
"timeTracking",
|
|
392
|
+
"ai",
|
|
393
|
+
"satisfaction",
|
|
394
|
+
"chat",
|
|
395
|
+
"emailTracking",
|
|
396
|
+
"canned",
|
|
397
|
+
"merge",
|
|
398
|
+
"snooze",
|
|
399
|
+
"externalMessages",
|
|
400
|
+
"clientHistory",
|
|
401
|
+
"activityLog",
|
|
402
|
+
"splitTicket",
|
|
403
|
+
"scheduledReplies",
|
|
404
|
+
"autoClose",
|
|
405
|
+
"roundRobin"
|
|
406
|
+
];
|
|
407
|
+
var PROJECTED_FEATURE_KEYS = ["autoClose", "autoCloseDays"];
|
|
408
|
+
function normalizeFeatures(raw) {
|
|
409
|
+
const out = { ...DEFAULT_TICKETING_FEATURES };
|
|
410
|
+
if (!raw || typeof raw !== "object") return out;
|
|
411
|
+
const src = raw;
|
|
412
|
+
for (const key of BOOLEAN_FEATURE_KEYS) {
|
|
413
|
+
if (typeof src[key] === "boolean") out[key] = src[key];
|
|
414
|
+
}
|
|
415
|
+
const days = src.autoCloseDays;
|
|
416
|
+
if (typeof days === "number" && Number.isFinite(days) && days >= 1) {
|
|
417
|
+
out.autoCloseDays = Math.floor(days);
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
function projectAutoClose(features, autoClose) {
|
|
422
|
+
if (!autoClose) return features;
|
|
423
|
+
return {
|
|
424
|
+
...features,
|
|
425
|
+
autoClose: typeof autoClose.enabled === "boolean" ? autoClose.enabled : features.autoClose,
|
|
426
|
+
autoCloseDays: typeof autoClose.daysBeforeClose === "number" && autoClose.daysBeforeClose >= 1 ? Math.floor(autoClose.daysBeforeClose) : features.autoCloseDays
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function stripProjectedFeatures(features) {
|
|
430
|
+
const out = { ...features };
|
|
431
|
+
for (const key of PROJECTED_FEATURE_KEYS) delete out[key];
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
371
435
|
// src/utils/readSettings.ts
|
|
372
|
-
var
|
|
436
|
+
var SUPPORT_SETTINGS_PREF_KEY = "support-settings";
|
|
437
|
+
var PREF_KEY = SUPPORT_SETTINGS_PREF_KEY;
|
|
373
438
|
var USER_PREFS_KEY_PREFIX = "support-user-prefs";
|
|
439
|
+
var LEGACY_ROUND_ROBIN_KEY = "support-round-robin";
|
|
374
440
|
var DEFAULT_SETTINGS = {
|
|
375
441
|
email: { fromAddress: "", fromName: "Support", replyToAddress: "" },
|
|
376
442
|
ai: { provider: "anthropic", model: "claude-haiku-4-5-20251001", enableSentiment: true, enableSynthesis: true, enableSuggestion: true, enableRewrite: true },
|
|
377
443
|
sla: { firstResponseMinutes: 120, resolutionMinutes: 1440, businessHoursOnly: true, escalationEmail: "" },
|
|
378
|
-
autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 }
|
|
444
|
+
autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 },
|
|
445
|
+
features: { ...DEFAULT_TICKETING_FEATURES }
|
|
379
446
|
};
|
|
380
447
|
var DEFAULT_USER_PREFS = {
|
|
381
448
|
locale: "fr",
|
|
@@ -386,32 +453,71 @@ var SETTINGS_TTL_MS = 6e4;
|
|
|
386
453
|
function invalidateSupportSettingsCache() {
|
|
387
454
|
settingsCache = null;
|
|
388
455
|
}
|
|
389
|
-
|
|
456
|
+
function mergeSupportSettings(stored, base = DEFAULT_SETTINGS) {
|
|
457
|
+
const autoClose = { ...base.autoClose, ...stored?.autoClose };
|
|
458
|
+
return {
|
|
459
|
+
email: { ...base.email, ...stored?.email },
|
|
460
|
+
ai: { ...base.ai, ...stored?.ai },
|
|
461
|
+
sla: { ...base.sla, ...stored?.sla },
|
|
462
|
+
autoClose,
|
|
463
|
+
// `autoClose` / `autoCloseDays` are projections of the block above, so they
|
|
464
|
+
// are recomputed here rather than trusted from whatever was persisted.
|
|
465
|
+
features: projectAutoClose(
|
|
466
|
+
normalizeFeatures({ ...base.features, ...stored?.features }),
|
|
467
|
+
autoClose
|
|
468
|
+
)
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
async function readSupportSettingsState(payload) {
|
|
390
472
|
if (settingsCache && Date.now() - settingsCache.ts < SETTINGS_TTL_MS) {
|
|
391
473
|
return settingsCache.value;
|
|
392
474
|
}
|
|
393
|
-
let value = {
|
|
475
|
+
let value = {
|
|
476
|
+
settings: mergeSupportSettings(null),
|
|
477
|
+
featuresConfigured: false
|
|
478
|
+
};
|
|
394
479
|
try {
|
|
395
480
|
const prefs = await dbFind(payload, "payload-preferences", {
|
|
396
481
|
where: { key: { equals: PREF_KEY } },
|
|
482
|
+
// The upsert is scoped per admin user, so several rows can share the key.
|
|
483
|
+
// Sorting makes "last write wins" deterministic instead of arbitrary.
|
|
484
|
+
sort: "-updatedAt",
|
|
397
485
|
limit: 1,
|
|
398
486
|
depth: 0,
|
|
399
487
|
overrideAccess: true
|
|
400
488
|
});
|
|
401
489
|
if (prefs.docs.length > 0) {
|
|
402
490
|
const stored = prefs.docs[0].value;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
};
|
|
491
|
+
const featuresConfigured = !!stored.features && typeof stored.features === "object";
|
|
492
|
+
const settings = mergeSupportSettings(stored);
|
|
493
|
+
if (!featuresConfigured) {
|
|
494
|
+
settings.features.roundRobin = await readLegacyRoundRobin(payload);
|
|
495
|
+
}
|
|
496
|
+
value = { settings, featuresConfigured };
|
|
409
497
|
}
|
|
410
498
|
} catch {
|
|
411
499
|
}
|
|
412
500
|
settingsCache = { value, ts: Date.now() };
|
|
413
501
|
return value;
|
|
414
502
|
}
|
|
503
|
+
async function readSupportSettings(payload) {
|
|
504
|
+
return (await readSupportSettingsState(payload)).settings;
|
|
505
|
+
}
|
|
506
|
+
async function readLegacyRoundRobin(payload) {
|
|
507
|
+
try {
|
|
508
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
509
|
+
where: { key: { equals: LEGACY_ROUND_ROBIN_KEY } },
|
|
510
|
+
limit: 1,
|
|
511
|
+
depth: 0,
|
|
512
|
+
overrideAccess: true
|
|
513
|
+
});
|
|
514
|
+
if (prefs.docs.length > 0) {
|
|
515
|
+
return prefs.docs[0].value?.enabled === true;
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
return DEFAULT_TICKETING_FEATURES.roundRobin;
|
|
520
|
+
}
|
|
415
521
|
async function readUserPrefs(payload, userId) {
|
|
416
522
|
try {
|
|
417
523
|
const key = `${USER_PREFS_KEY_PREFIX}-${userId}`;
|
|
@@ -432,13 +538,20 @@ async function readUserPrefs(payload, userId) {
|
|
|
432
538
|
}
|
|
433
539
|
return { ...DEFAULT_USER_PREFS };
|
|
434
540
|
}
|
|
541
|
+
function resolveOllamaBaseUrl() {
|
|
542
|
+
const baseURL = process.env.OLLAMA_API_URL;
|
|
543
|
+
if (!baseURL) {
|
|
544
|
+
throw new APIError("provider 'ollama' requires OLLAMA_API_URL", 500);
|
|
545
|
+
}
|
|
546
|
+
return baseURL;
|
|
547
|
+
}
|
|
435
548
|
|
|
436
549
|
// src/endpoints/ai.ts
|
|
437
550
|
async function getClient(aiSettings) {
|
|
438
551
|
const moduleName = "@anthropic-ai/sdk";
|
|
439
552
|
const { default: Anthropic } = await import(moduleName);
|
|
440
553
|
if (aiSettings.provider === "ollama") {
|
|
441
|
-
const baseURL =
|
|
554
|
+
const baseURL = resolveOllamaBaseUrl();
|
|
442
555
|
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
443
556
|
}
|
|
444
557
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
@@ -637,7 +750,7 @@ async function applyAgentDecision(payload, slugs, ticketId, decision) {
|
|
|
637
750
|
function getClient2(aiSettings) {
|
|
638
751
|
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
639
752
|
if (aiSettings.provider === "ollama") {
|
|
640
|
-
return new Anthropic({ apiKey: "ollama", baseURL:
|
|
753
|
+
return new Anthropic({ apiKey: "ollama", baseURL: resolveOllamaBaseUrl() });
|
|
641
754
|
}
|
|
642
755
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
643
756
|
}
|
|
@@ -737,7 +850,7 @@ async function getClient3(aiSettings) {
|
|
|
737
850
|
const moduleName = "@anthropic-ai/sdk";
|
|
738
851
|
const { default: Anthropic } = await import(moduleName);
|
|
739
852
|
if (aiSettings.provider === "ollama") {
|
|
740
|
-
const baseURL =
|
|
853
|
+
const baseURL = resolveOllamaBaseUrl();
|
|
741
854
|
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
742
855
|
}
|
|
743
856
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
@@ -1573,25 +1686,6 @@ function createPresenceGetEndpoint(slugs) {
|
|
|
1573
1686
|
}
|
|
1574
1687
|
|
|
1575
1688
|
// src/endpoints/settings.ts
|
|
1576
|
-
var PREF_KEY2 = "support-settings";
|
|
1577
|
-
var DEFAULT_SUPPORT_SETTINGS = {
|
|
1578
|
-
email: { fromAddress: "", fromName: "Support", replyToAddress: "" },
|
|
1579
|
-
ai: {
|
|
1580
|
-
provider: "anthropic",
|
|
1581
|
-
model: "claude-haiku-4-5-20251001",
|
|
1582
|
-
enableSentiment: true,
|
|
1583
|
-
enableSynthesis: true,
|
|
1584
|
-
enableSuggestion: true,
|
|
1585
|
-
enableRewrite: true
|
|
1586
|
-
},
|
|
1587
|
-
sla: {
|
|
1588
|
-
firstResponseMinutes: 120,
|
|
1589
|
-
resolutionMinutes: 1440,
|
|
1590
|
-
businessHoursOnly: true,
|
|
1591
|
-
escalationEmail: ""
|
|
1592
|
-
},
|
|
1593
|
-
autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 }
|
|
1594
|
-
};
|
|
1595
1689
|
function createSettingsGetEndpoint(slugs) {
|
|
1596
1690
|
return {
|
|
1597
1691
|
path: "/support/settings",
|
|
@@ -1600,23 +1694,8 @@ function createSettingsGetEndpoint(slugs) {
|
|
|
1600
1694
|
try {
|
|
1601
1695
|
const payload = req.payload;
|
|
1602
1696
|
requireAdmin(req, slugs);
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1605
|
-
limit: 1,
|
|
1606
|
-
depth: 0,
|
|
1607
|
-
overrideAccess: true
|
|
1608
|
-
});
|
|
1609
|
-
let settings = { ...DEFAULT_SUPPORT_SETTINGS };
|
|
1610
|
-
if (prefs.docs.length > 0) {
|
|
1611
|
-
const stored = prefs.docs[0].value;
|
|
1612
|
-
settings = {
|
|
1613
|
-
email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...stored.email },
|
|
1614
|
-
ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...stored.ai },
|
|
1615
|
-
sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...stored.sla },
|
|
1616
|
-
autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...stored.autoClose }
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
return Response.json(settings);
|
|
1697
|
+
const { settings, featuresConfigured } = await readSupportSettingsState(payload);
|
|
1698
|
+
return Response.json({ ...settings, featuresConfigured });
|
|
1620
1699
|
} catch (error) {
|
|
1621
1700
|
const authResponse = handleAuthError(error);
|
|
1622
1701
|
if (authResponse) return authResponse;
|
|
@@ -1635,30 +1714,31 @@ function createSettingsPostEndpoint(slugs) {
|
|
|
1635
1714
|
const payload = req.payload;
|
|
1636
1715
|
requireAdmin(req, slugs);
|
|
1637
1716
|
const body = await req.json();
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...body.ai },
|
|
1641
|
-
sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...body.sla },
|
|
1642
|
-
autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...body.autoClose }
|
|
1643
|
-
};
|
|
1717
|
+
const { settings: current } = await readSupportSettingsState(payload);
|
|
1718
|
+
const merged = mergeSupportSettings(body, current);
|
|
1644
1719
|
await payload.db.upsert({
|
|
1645
1720
|
collection: "payload-preferences",
|
|
1646
1721
|
data: {
|
|
1647
|
-
key:
|
|
1722
|
+
key: SUPPORT_SETTINGS_PREF_KEY,
|
|
1648
1723
|
user: { relationTo: req.user.collection, value: req.user.id },
|
|
1649
|
-
value:
|
|
1724
|
+
value: {
|
|
1725
|
+
...merged,
|
|
1726
|
+
// Persist the flags without the two projected ones, so `features`
|
|
1727
|
+
// and the `autoClose` block can never drift apart.
|
|
1728
|
+
features: stripProjectedFeatures(merged.features)
|
|
1729
|
+
}
|
|
1650
1730
|
},
|
|
1651
1731
|
req: { payload, user: req.user },
|
|
1652
1732
|
where: {
|
|
1653
1733
|
and: [
|
|
1654
|
-
{ key: { equals:
|
|
1734
|
+
{ key: { equals: SUPPORT_SETTINGS_PREF_KEY } },
|
|
1655
1735
|
{ "user.value": { equals: req.user.id } },
|
|
1656
1736
|
{ "user.relationTo": { equals: req.user.collection } }
|
|
1657
1737
|
]
|
|
1658
1738
|
}
|
|
1659
1739
|
});
|
|
1660
1740
|
invalidateSupportSettingsCache();
|
|
1661
|
-
return Response.json(merged);
|
|
1741
|
+
return Response.json({ ...merged, featuresConfigured: true });
|
|
1662
1742
|
} catch (error) {
|
|
1663
1743
|
const authResponse = handleAuthError(error);
|
|
1664
1744
|
if (authResponse) return authResponse;
|
|
@@ -1670,7 +1750,7 @@ function createSettingsPostEndpoint(slugs) {
|
|
|
1670
1750
|
}
|
|
1671
1751
|
|
|
1672
1752
|
// src/endpoints/signature.ts
|
|
1673
|
-
var
|
|
1753
|
+
var PREF_KEY2 = "email-signature";
|
|
1674
1754
|
function createSignatureGetEndpoint(slugs) {
|
|
1675
1755
|
return {
|
|
1676
1756
|
path: "/support/signature",
|
|
@@ -1680,7 +1760,7 @@ function createSignatureGetEndpoint(slugs) {
|
|
|
1680
1760
|
const payload = req.payload;
|
|
1681
1761
|
requireAdmin(req, slugs);
|
|
1682
1762
|
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1683
|
-
where: { key: { equals: `${
|
|
1763
|
+
where: { key: { equals: `${PREF_KEY2}-${req.user.id}` } },
|
|
1684
1764
|
limit: 1,
|
|
1685
1765
|
depth: 0,
|
|
1686
1766
|
overrideAccess: true
|
|
@@ -1705,7 +1785,7 @@ function createSignaturePostEndpoint(slugs) {
|
|
|
1705
1785
|
const payload = req.payload;
|
|
1706
1786
|
requireAdmin(req, slugs);
|
|
1707
1787
|
const { signature } = await req.json();
|
|
1708
|
-
const key = `${
|
|
1788
|
+
const key = `${PREF_KEY2}-${req.user.id}`;
|
|
1709
1789
|
const existing = await dbFind(payload, "payload-preferences", {
|
|
1710
1790
|
where: { key: { equals: key } },
|
|
1711
1791
|
limit: 1,
|
|
@@ -1740,7 +1820,6 @@ function createSignaturePostEndpoint(slugs) {
|
|
|
1740
1820
|
}
|
|
1741
1821
|
|
|
1742
1822
|
// src/endpoints/round-robin-config.ts
|
|
1743
|
-
var PREF_KEY4 = "support-round-robin";
|
|
1744
1823
|
function createRoundRobinConfigGetEndpoint(slugs) {
|
|
1745
1824
|
return {
|
|
1746
1825
|
path: "/support/round-robin-config",
|
|
@@ -1749,14 +1828,8 @@ function createRoundRobinConfigGetEndpoint(slugs) {
|
|
|
1749
1828
|
try {
|
|
1750
1829
|
const payload = req.payload;
|
|
1751
1830
|
requireAdmin(req, slugs);
|
|
1752
|
-
const
|
|
1753
|
-
|
|
1754
|
-
limit: 1,
|
|
1755
|
-
depth: 0,
|
|
1756
|
-
overrideAccess: true
|
|
1757
|
-
});
|
|
1758
|
-
const enabled = prefs.docs.length > 0 ? prefs.docs[0].value?.enabled === true : false;
|
|
1759
|
-
return Response.json({ enabled });
|
|
1831
|
+
const { settings } = await readSupportSettingsState(payload);
|
|
1832
|
+
return Response.json({ enabled: settings.features.roundRobin });
|
|
1760
1833
|
} catch (error) {
|
|
1761
1834
|
const authResponse = handleAuthError(error);
|
|
1762
1835
|
if (authResponse) return authResponse;
|
|
@@ -1775,28 +1848,31 @@ function createRoundRobinConfigPostEndpoint(slugs) {
|
|
|
1775
1848
|
const payload = req.payload;
|
|
1776
1849
|
requireAdmin(req, slugs);
|
|
1777
1850
|
const { enabled } = await req.json();
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
});
|
|
1851
|
+
const { settings: current } = await readSupportSettingsState(payload);
|
|
1852
|
+
const merged = mergeSupportSettings(
|
|
1853
|
+
{ features: { ...current.features, roundRobin: !!enabled } },
|
|
1854
|
+
current
|
|
1855
|
+
);
|
|
1784
1856
|
await payload.db.upsert({
|
|
1785
1857
|
collection: "payload-preferences",
|
|
1786
1858
|
data: {
|
|
1787
|
-
key:
|
|
1859
|
+
key: SUPPORT_SETTINGS_PREF_KEY,
|
|
1788
1860
|
user: { relationTo: req.user.collection, value: req.user.id },
|
|
1789
|
-
value: {
|
|
1861
|
+
value: {
|
|
1862
|
+
...merged,
|
|
1863
|
+
features: stripProjectedFeatures(merged.features)
|
|
1864
|
+
}
|
|
1790
1865
|
},
|
|
1791
1866
|
req: { payload, user: req.user },
|
|
1792
1867
|
where: {
|
|
1793
1868
|
and: [
|
|
1794
|
-
{ key: { equals:
|
|
1869
|
+
{ key: { equals: SUPPORT_SETTINGS_PREF_KEY } },
|
|
1795
1870
|
{ "user.value": { equals: req.user.id } },
|
|
1796
1871
|
{ "user.relationTo": { equals: req.user.collection } }
|
|
1797
1872
|
]
|
|
1798
1873
|
}
|
|
1799
1874
|
});
|
|
1875
|
+
invalidateSupportSettingsCache();
|
|
1800
1876
|
return Response.json({ enabled: !!enabled });
|
|
1801
1877
|
} catch (error) {
|
|
1802
1878
|
const authResponse = handleAuthError(error);
|
|
@@ -4058,7 +4134,7 @@ function createInvoiceEndpoint(slugs) {
|
|
|
4058
4134
|
function getClient4(aiSettings) {
|
|
4059
4135
|
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
4060
4136
|
if (aiSettings.provider === "ollama") {
|
|
4061
|
-
const baseURL =
|
|
4137
|
+
const baseURL = resolveOllamaBaseUrl();
|
|
4062
4138
|
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
4063
4139
|
}
|
|
4064
4140
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
@@ -5751,62 +5827,73 @@ function createImportConversationEndpoint(slugs, store) {
|
|
|
5751
5827
|
}
|
|
5752
5828
|
};
|
|
5753
5829
|
}
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5830
|
+
function dispatchWebhook(data, event, payload, slugs) {
|
|
5831
|
+
const done = _dispatch(data, event, payload, slugs);
|
|
5832
|
+
return done;
|
|
5833
|
+
}
|
|
5834
|
+
async function _dispatch(data, event, payload, slugs) {
|
|
5757
5835
|
try {
|
|
5758
|
-
const endpoints = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5836
|
+
const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5759
5837
|
where: {
|
|
5760
|
-
|
|
5761
|
-
|
|
5838
|
+
and: [
|
|
5839
|
+
{ active: { equals: true } },
|
|
5840
|
+
{ events: { contains: event } }
|
|
5841
|
+
]
|
|
5762
5842
|
},
|
|
5763
5843
|
limit: 50,
|
|
5764
5844
|
depth: 0,
|
|
5765
5845
|
overrideAccess: true
|
|
5766
5846
|
});
|
|
5767
|
-
if (endpoints.
|
|
5768
|
-
const
|
|
5847
|
+
if (endpoints.length === 0) return;
|
|
5848
|
+
const body = JSON.stringify({ event, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5769
5849
|
await Promise.allSettled(
|
|
5770
|
-
endpoints.
|
|
5771
|
-
try {
|
|
5772
|
-
const res = await fetch(endpoint.url, {
|
|
5773
|
-
method: "POST",
|
|
5774
|
-
headers: {
|
|
5775
|
-
"Content-Type": "application/json",
|
|
5776
|
-
...endpoint.secret ? { "X-Webhook-Secret": endpoint.secret } : {}
|
|
5777
|
-
},
|
|
5778
|
-
body: JSON.stringify({ event, data, timestamp }),
|
|
5779
|
-
signal: AbortSignal.timeout(1e4)
|
|
5780
|
-
});
|
|
5781
|
-
try {
|
|
5782
|
-
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5783
|
-
id: endpoint.id,
|
|
5784
|
-
data: {
|
|
5785
|
-
lastTriggeredAt: timestamp,
|
|
5786
|
-
lastStatus: res.status
|
|
5787
|
-
},
|
|
5788
|
-
overrideAccess: true
|
|
5789
|
-
});
|
|
5790
|
-
} catch {
|
|
5791
|
-
}
|
|
5792
|
-
} catch (error) {
|
|
5793
|
-
console.warn(`[support] Webhook delivery failed: ${endpoint.url}`, error);
|
|
5794
|
-
try {
|
|
5795
|
-
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5796
|
-
id: endpoint.id,
|
|
5797
|
-
data: {
|
|
5798
|
-
lastTriggeredAt: timestamp,
|
|
5799
|
-
lastStatus: 0
|
|
5800
|
-
},
|
|
5801
|
-
overrideAccess: true
|
|
5802
|
-
});
|
|
5803
|
-
} catch {
|
|
5804
|
-
}
|
|
5805
|
-
}
|
|
5806
|
-
})
|
|
5850
|
+
endpoints.map((endpoint) => _sendToEndpoint(endpoint, body, payload, slugs))
|
|
5807
5851
|
);
|
|
5808
|
-
} catch (
|
|
5809
|
-
console.error(
|
|
5852
|
+
} catch (err) {
|
|
5853
|
+
console.error(`[webhook] Failed to fetch endpoints for event ${event}:`, err);
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
5857
|
+
try {
|
|
5858
|
+
const headers = {
|
|
5859
|
+
"Content-Type": "application/json",
|
|
5860
|
+
"User-Agent": "PayloadSupport-Webhook/1.0"
|
|
5861
|
+
};
|
|
5862
|
+
if (endpoint.secret) {
|
|
5863
|
+
const signature = crypto3.createHmac("sha256", endpoint.secret).update(body).digest("hex");
|
|
5864
|
+
headers["X-Webhook-Signature"] = signature;
|
|
5865
|
+
}
|
|
5866
|
+
const response = await fetch(endpoint.url, {
|
|
5867
|
+
method: "POST",
|
|
5868
|
+
headers,
|
|
5869
|
+
body,
|
|
5870
|
+
signal: AbortSignal.timeout(1e4)
|
|
5871
|
+
// 10s timeout
|
|
5872
|
+
});
|
|
5873
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5874
|
+
id: endpoint.id,
|
|
5875
|
+
data: {
|
|
5876
|
+
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5877
|
+
lastStatus: response.status
|
|
5878
|
+
},
|
|
5879
|
+
overrideAccess: true
|
|
5880
|
+
});
|
|
5881
|
+
if (!response.ok) {
|
|
5882
|
+
console.warn(`[webhook] ${endpoint.name || endpoint.url} returned ${response.status}`);
|
|
5883
|
+
}
|
|
5884
|
+
} catch (err) {
|
|
5885
|
+
console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
|
|
5886
|
+
try {
|
|
5887
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5888
|
+
id: endpoint.id,
|
|
5889
|
+
data: {
|
|
5890
|
+
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5891
|
+
lastStatus: 0
|
|
5892
|
+
},
|
|
5893
|
+
overrideAccess: true
|
|
5894
|
+
});
|
|
5895
|
+
} catch {
|
|
5896
|
+
}
|
|
5810
5897
|
}
|
|
5811
5898
|
}
|
|
5812
5899
|
|
|
@@ -5893,13 +5980,18 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5893
5980
|
overrideAccess: true
|
|
5894
5981
|
});
|
|
5895
5982
|
}
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5983
|
+
dispatchWebhook(
|
|
5984
|
+
{
|
|
5985
|
+
ticketId,
|
|
5986
|
+
messageId: msg.id,
|
|
5987
|
+
authorType: msg.authorType,
|
|
5988
|
+
scheduled: true,
|
|
5989
|
+
body: msg.body?.length > 500 ? msg.body.slice(0, 500) + "..." : msg.body
|
|
5990
|
+
},
|
|
5991
|
+
"ticket_replied",
|
|
5992
|
+
payload,
|
|
5993
|
+
slugs
|
|
5994
|
+
);
|
|
5903
5995
|
results.processed++;
|
|
5904
5996
|
} catch (err) {
|
|
5905
5997
|
console.error(`[process-scheduled] Error processing message ${message.id}:`, err);
|
|
@@ -6869,74 +6961,6 @@ async function createAdminNotification(payload, data, collectionSlug = "admin-no
|
|
|
6869
6961
|
console.error("[notification] Failed to create:", err);
|
|
6870
6962
|
}
|
|
6871
6963
|
}
|
|
6872
|
-
function dispatchWebhook(data, event, payload, slugs) {
|
|
6873
|
-
void _dispatch(data, event, payload, slugs);
|
|
6874
|
-
}
|
|
6875
|
-
async function _dispatch(data, event, payload, slugs) {
|
|
6876
|
-
try {
|
|
6877
|
-
const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
|
|
6878
|
-
where: {
|
|
6879
|
-
and: [
|
|
6880
|
-
{ active: { equals: true } },
|
|
6881
|
-
{ events: { contains: event } }
|
|
6882
|
-
]
|
|
6883
|
-
},
|
|
6884
|
-
limit: 50,
|
|
6885
|
-
depth: 0,
|
|
6886
|
-
overrideAccess: true
|
|
6887
|
-
});
|
|
6888
|
-
if (endpoints.length === 0) return;
|
|
6889
|
-
const body = JSON.stringify({ event, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6890
|
-
for (const endpoint of endpoints) {
|
|
6891
|
-
void _sendToEndpoint(endpoint, body, payload, slugs);
|
|
6892
|
-
}
|
|
6893
|
-
} catch (err) {
|
|
6894
|
-
console.error(`[webhook] Failed to fetch endpoints for event ${event}:`, err);
|
|
6895
|
-
}
|
|
6896
|
-
}
|
|
6897
|
-
async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
6898
|
-
try {
|
|
6899
|
-
const headers = {
|
|
6900
|
-
"Content-Type": "application/json",
|
|
6901
|
-
"User-Agent": "PayloadSupport-Webhook/1.0"
|
|
6902
|
-
};
|
|
6903
|
-
if (endpoint.secret) {
|
|
6904
|
-
const signature = crypto3.createHmac("sha256", endpoint.secret).update(body).digest("hex");
|
|
6905
|
-
headers["X-Webhook-Signature"] = signature;
|
|
6906
|
-
}
|
|
6907
|
-
const response = await fetch(endpoint.url, {
|
|
6908
|
-
method: "POST",
|
|
6909
|
-
headers,
|
|
6910
|
-
body,
|
|
6911
|
-
signal: AbortSignal.timeout(1e4)
|
|
6912
|
-
// 10s timeout
|
|
6913
|
-
});
|
|
6914
|
-
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6915
|
-
id: endpoint.id,
|
|
6916
|
-
data: {
|
|
6917
|
-
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6918
|
-
lastStatus: response.status
|
|
6919
|
-
},
|
|
6920
|
-
overrideAccess: true
|
|
6921
|
-
});
|
|
6922
|
-
if (!response.ok) {
|
|
6923
|
-
console.warn(`[webhook] ${endpoint.name || endpoint.url} returned ${response.status}`);
|
|
6924
|
-
}
|
|
6925
|
-
} catch (err) {
|
|
6926
|
-
console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
|
|
6927
|
-
try {
|
|
6928
|
-
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6929
|
-
id: endpoint.id,
|
|
6930
|
-
data: {
|
|
6931
|
-
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6932
|
-
lastStatus: 0
|
|
6933
|
-
},
|
|
6934
|
-
overrideAccess: true
|
|
6935
|
-
});
|
|
6936
|
-
} catch {
|
|
6937
|
-
}
|
|
6938
|
-
}
|
|
6939
|
-
}
|
|
6940
6964
|
|
|
6941
6965
|
// src/hooks/ticketStatusEmail.ts
|
|
6942
6966
|
var STATUS_LABELS = {
|
|
@@ -7553,12 +7577,28 @@ function createAssignTicketNumber(slugs, options) {
|
|
|
7553
7577
|
}
|
|
7554
7578
|
function createAssignClientOnCreate(slugs) {
|
|
7555
7579
|
return async ({ data, operation, req }) => {
|
|
7556
|
-
if (operation === "create" && req.user?.collection === slugs.supportClients
|
|
7580
|
+
if (operation === "create" && req.user?.collection === slugs.supportClients) {
|
|
7557
7581
|
data.client = req.user.id;
|
|
7558
7582
|
}
|
|
7559
7583
|
return data;
|
|
7560
7584
|
};
|
|
7561
7585
|
}
|
|
7586
|
+
var CLIENT_CREATABLE_TICKET_FIELDS = ["subject", "category", "priority", "project"];
|
|
7587
|
+
function createRestrictClientCreate(slugs) {
|
|
7588
|
+
return async ({ data, operation, req }) => {
|
|
7589
|
+
if (operation !== "create") return data;
|
|
7590
|
+
if (req.user?.collection !== slugs.supportClients) return data;
|
|
7591
|
+
const incoming = data ?? {};
|
|
7592
|
+
const sanitized = {};
|
|
7593
|
+
for (const field2 of CLIENT_CREATABLE_TICKET_FIELDS) {
|
|
7594
|
+
if (incoming[field2] !== void 0) sanitized[field2] = incoming[field2];
|
|
7595
|
+
}
|
|
7596
|
+
sanitized.status = "open";
|
|
7597
|
+
sanitized.client = req.user.id;
|
|
7598
|
+
sanitized.source = "portal";
|
|
7599
|
+
return sanitized;
|
|
7600
|
+
};
|
|
7601
|
+
}
|
|
7562
7602
|
var autoPaidAt = async ({ data, operation, originalDoc }) => {
|
|
7563
7603
|
if (operation !== "update") return data;
|
|
7564
7604
|
if (data.paymentStatus === "paid" && originalDoc?.paymentStatus !== "paid" && !data.paidAt) {
|
|
@@ -7584,16 +7624,8 @@ function createAutoAssignAdmin(slugs) {
|
|
|
7584
7624
|
const { payload } = req;
|
|
7585
7625
|
let roundRobinEnabled = false;
|
|
7586
7626
|
try {
|
|
7587
|
-
const
|
|
7588
|
-
|
|
7589
|
-
where: { key: { equals: "support-round-robin" } },
|
|
7590
|
-
limit: 1,
|
|
7591
|
-
depth: 0,
|
|
7592
|
-
overrideAccess: true
|
|
7593
|
-
});
|
|
7594
|
-
if (prefs.docs.length > 0) {
|
|
7595
|
-
roundRobinEnabled = prefs.docs[0].value?.enabled === true;
|
|
7596
|
-
}
|
|
7627
|
+
const settings = await readSupportSettings(payload);
|
|
7628
|
+
roundRobinEnabled = settings.features.roundRobin === true;
|
|
7597
7629
|
} catch (err) {
|
|
7598
7630
|
console.warn("[support] Failed to read round-robin preferences:", err);
|
|
7599
7631
|
}
|
|
@@ -7802,54 +7834,6 @@ function createNotifyClientOnResolve(slugs) {
|
|
|
7802
7834
|
return doc;
|
|
7803
7835
|
};
|
|
7804
7836
|
}
|
|
7805
|
-
function createFireTicketWebhooks(slugs) {
|
|
7806
|
-
return async ({ doc, previousDoc, operation, req }) => {
|
|
7807
|
-
const { payload } = req;
|
|
7808
|
-
if (operation === "create") {
|
|
7809
|
-
fireWebhooks(payload, slugs, "ticket_created", {
|
|
7810
|
-
id: doc.id,
|
|
7811
|
-
ticketNumber: doc.ticketNumber,
|
|
7812
|
-
subject: doc.subject,
|
|
7813
|
-
status: doc.status,
|
|
7814
|
-
priority: doc.priority,
|
|
7815
|
-
category: doc.category
|
|
7816
|
-
});
|
|
7817
|
-
return doc;
|
|
7818
|
-
}
|
|
7819
|
-
if (operation === "update" && previousDoc) {
|
|
7820
|
-
if (previousDoc.status !== doc.status && doc.status === "resolved") {
|
|
7821
|
-
fireWebhooks(payload, slugs, "ticket_resolved", {
|
|
7822
|
-
id: doc.id,
|
|
7823
|
-
ticketNumber: doc.ticketNumber,
|
|
7824
|
-
subject: doc.subject,
|
|
7825
|
-
previousStatus: previousDoc.status
|
|
7826
|
-
});
|
|
7827
|
-
const clientId = typeof doc.client === "object" ? doc.client?.id : doc.client;
|
|
7828
|
-
if (clientId) {
|
|
7829
|
-
payload.update({
|
|
7830
|
-
collection: "client-summaries",
|
|
7831
|
-
where: { client: { equals: clientId } },
|
|
7832
|
-
data: { generatedAt: (/* @__PURE__ */ new Date(0)).toISOString() },
|
|
7833
|
-
// Force cache expiry
|
|
7834
|
-
overrideAccess: true
|
|
7835
|
-
}).catch(() => {
|
|
7836
|
-
});
|
|
7837
|
-
}
|
|
7838
|
-
}
|
|
7839
|
-
const oldAssigned = typeof previousDoc.assignedTo === "object" ? previousDoc.assignedTo?.id : previousDoc.assignedTo;
|
|
7840
|
-
const newAssigned = typeof doc.assignedTo === "object" ? doc.assignedTo?.id : doc.assignedTo;
|
|
7841
|
-
if (newAssigned && oldAssigned !== newAssigned) {
|
|
7842
|
-
fireWebhooks(payload, slugs, "ticket_assigned", {
|
|
7843
|
-
id: doc.id,
|
|
7844
|
-
ticketNumber: doc.ticketNumber,
|
|
7845
|
-
subject: doc.subject,
|
|
7846
|
-
assignedTo: newAssigned
|
|
7847
|
-
});
|
|
7848
|
-
}
|
|
7849
|
-
}
|
|
7850
|
-
return doc;
|
|
7851
|
-
};
|
|
7852
|
-
}
|
|
7853
7837
|
function createNotifyAdminOnNewTicket(slugs, notificationSlug) {
|
|
7854
7838
|
return async ({ doc, operation, req }) => {
|
|
7855
7839
|
if (operation !== "create") return doc;
|
|
@@ -7870,21 +7854,66 @@ function createNotifyAdminOnNewTicket(slugs, notificationSlug) {
|
|
|
7870
7854
|
}
|
|
7871
7855
|
function createDispatchWebhookOnTicket(slugs) {
|
|
7872
7856
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
7857
|
+
const { payload } = req;
|
|
7873
7858
|
if (operation === "create") {
|
|
7874
7859
|
dispatchWebhook(
|
|
7875
|
-
{
|
|
7860
|
+
{
|
|
7861
|
+
ticketId: doc.id,
|
|
7862
|
+
id: doc.id,
|
|
7863
|
+
ticketNumber: doc.ticketNumber,
|
|
7864
|
+
subject: doc.subject,
|
|
7865
|
+
status: doc.status,
|
|
7866
|
+
priority: doc.priority,
|
|
7867
|
+
category: doc.category
|
|
7868
|
+
},
|
|
7876
7869
|
"ticket_created",
|
|
7877
|
-
|
|
7870
|
+
payload,
|
|
7878
7871
|
slugs
|
|
7879
7872
|
);
|
|
7873
|
+
return doc;
|
|
7880
7874
|
}
|
|
7881
|
-
if (operation === "update" && previousDoc
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7875
|
+
if (operation === "update" && previousDoc) {
|
|
7876
|
+
if (previousDoc.status !== doc.status && doc.status === "resolved") {
|
|
7877
|
+
dispatchWebhook(
|
|
7878
|
+
{
|
|
7879
|
+
ticketId: doc.id,
|
|
7880
|
+
id: doc.id,
|
|
7881
|
+
ticketNumber: doc.ticketNumber,
|
|
7882
|
+
subject: doc.subject,
|
|
7883
|
+
previousStatus: previousDoc.status
|
|
7884
|
+
},
|
|
7885
|
+
"ticket_resolved",
|
|
7886
|
+
payload,
|
|
7887
|
+
slugs
|
|
7888
|
+
);
|
|
7889
|
+
const clientId = typeof doc.client === "object" ? doc.client?.id : doc.client;
|
|
7890
|
+
if (clientId) {
|
|
7891
|
+
payload.update({
|
|
7892
|
+
collection: "client-summaries",
|
|
7893
|
+
where: { client: { equals: clientId } },
|
|
7894
|
+
data: { generatedAt: (/* @__PURE__ */ new Date(0)).toISOString() },
|
|
7895
|
+
// Force cache expiry
|
|
7896
|
+
overrideAccess: true
|
|
7897
|
+
}).catch(() => {
|
|
7898
|
+
});
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7901
|
+
const oldAssigned = typeof previousDoc.assignedTo === "object" ? previousDoc.assignedTo?.id : previousDoc.assignedTo;
|
|
7902
|
+
const newAssigned = typeof doc.assignedTo === "object" ? doc.assignedTo?.id : doc.assignedTo;
|
|
7903
|
+
if (newAssigned && oldAssigned !== newAssigned) {
|
|
7904
|
+
dispatchWebhook(
|
|
7905
|
+
{
|
|
7906
|
+
ticketId: doc.id,
|
|
7907
|
+
id: doc.id,
|
|
7908
|
+
ticketNumber: doc.ticketNumber,
|
|
7909
|
+
subject: doc.subject,
|
|
7910
|
+
assignedTo: newAssigned
|
|
7911
|
+
},
|
|
7912
|
+
"ticket_assigned",
|
|
7913
|
+
payload,
|
|
7914
|
+
slugs
|
|
7915
|
+
);
|
|
7916
|
+
}
|
|
7888
7917
|
}
|
|
7889
7918
|
return doc;
|
|
7890
7919
|
};
|
|
@@ -8010,7 +8039,10 @@ function createTicketsCollection(slugs, options) {
|
|
|
8010
8039
|
{ label: "Paiement partiel", value: "partial" },
|
|
8011
8040
|
{ label: "Pay\xE9", value: "paid" }
|
|
8012
8041
|
],
|
|
8013
|
-
|
|
8042
|
+
// `slugs.users`, not the literal 'users': the users collection slug is
|
|
8043
|
+
// configurable, and a hard-coded one silently opens this field to everybody
|
|
8044
|
+
// on a host that renamed it.
|
|
8045
|
+
access: { update: ({ req }) => req.user?.collection === slugs.users },
|
|
8014
8046
|
admin: { width: "50%", condition: (data) => !!data?.invoice }
|
|
8015
8047
|
},
|
|
8016
8048
|
{
|
|
@@ -8292,6 +8324,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
8292
8324
|
{ name: "totalTimeMinutes", type: "number", defaultValue: 0, label: "Temps total (minutes)", admin: { readOnly: true, position: "sidebar" } }
|
|
8293
8325
|
],
|
|
8294
8326
|
hooks: {
|
|
8327
|
+
beforeValidate: [createRestrictClientCreate(slugs)],
|
|
8295
8328
|
beforeChange: [
|
|
8296
8329
|
createAssignTicketNumber(slugs, options?.ticketNumber),
|
|
8297
8330
|
createAssignClientOnCreate(slugs),
|
|
@@ -8312,7 +8345,6 @@ function createTicketsCollection(slugs, options) {
|
|
|
8312
8345
|
createNotifyClientOnResolve(slugs),
|
|
8313
8346
|
createTicketStatusEmail(slugs),
|
|
8314
8347
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
8315
|
-
createFireTicketWebhooks(slugs),
|
|
8316
8348
|
createDispatchWebhookOnTicket(slugs),
|
|
8317
8349
|
createApplyAutomationRules(slugs)
|
|
8318
8350
|
],
|
|
@@ -8493,6 +8525,21 @@ async function queueClientNotification(payload, slugs, item) {
|
|
|
8493
8525
|
}
|
|
8494
8526
|
|
|
8495
8527
|
// src/collections/TicketMessages.ts
|
|
8528
|
+
function createRestrictClientTicketTarget(slugs) {
|
|
8529
|
+
return async ({ data, operation, req, originalDoc }) => {
|
|
8530
|
+
if (req.user?.collection !== slugs.supportClients) return data;
|
|
8531
|
+
const raw = data.ticket ?? (operation === "update" ? originalDoc?.ticket : void 0);
|
|
8532
|
+
const targetId = raw && typeof raw === "object" ? raw.id : raw;
|
|
8533
|
+
if (targetId === void 0 || targetId === null || targetId === "") {
|
|
8534
|
+
throw new APIError("Ticket cible requis.", 400);
|
|
8535
|
+
}
|
|
8536
|
+
const accessible = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
8537
|
+
if (!accessible.some((id) => String(id) === String(targetId))) {
|
|
8538
|
+
throw new APIError("Ticket inaccessible.", 403);
|
|
8539
|
+
}
|
|
8540
|
+
return data;
|
|
8541
|
+
};
|
|
8542
|
+
}
|
|
8496
8543
|
function createAssignAuthor(slugs) {
|
|
8497
8544
|
return async ({ data, operation, req }) => {
|
|
8498
8545
|
if (operation === "create" && req.user?.collection === slugs.supportClients) {
|
|
@@ -8727,21 +8774,6 @@ function createNotifyAdminOnClientMessage(slugs, notificationSlug) {
|
|
|
8727
8774
|
return doc;
|
|
8728
8775
|
};
|
|
8729
8776
|
}
|
|
8730
|
-
function createFireMessageWebhooks(slugs) {
|
|
8731
|
-
return async ({ doc, operation, req }) => {
|
|
8732
|
-
if (operation !== "create") return doc;
|
|
8733
|
-
if (doc.scheduledAt && !doc.scheduledSent) return doc;
|
|
8734
|
-
if (doc.isInternal) return doc;
|
|
8735
|
-
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8736
|
-
fireWebhooks(req.payload, slugs, "ticket_replied", {
|
|
8737
|
-
ticketId,
|
|
8738
|
-
messageId: doc.id,
|
|
8739
|
-
authorType: doc.authorType,
|
|
8740
|
-
body: doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body
|
|
8741
|
-
});
|
|
8742
|
-
return doc;
|
|
8743
|
-
};
|
|
8744
|
-
}
|
|
8745
8777
|
function createDispatchWebhookOnReply(slugs) {
|
|
8746
8778
|
return async ({ doc, operation, req }) => {
|
|
8747
8779
|
if (operation !== "create") return doc;
|
|
@@ -8749,7 +8781,12 @@ function createDispatchWebhookOnReply(slugs) {
|
|
|
8749
8781
|
if (doc.scheduledAt && !doc.scheduledSent) return doc;
|
|
8750
8782
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8751
8783
|
dispatchWebhook(
|
|
8752
|
-
{
|
|
8784
|
+
{
|
|
8785
|
+
ticketId,
|
|
8786
|
+
messageId: doc.id,
|
|
8787
|
+
authorType: doc.authorType,
|
|
8788
|
+
body: doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body
|
|
8789
|
+
},
|
|
8753
8790
|
"ticket_replied",
|
|
8754
8791
|
req.payload,
|
|
8755
8792
|
slugs
|
|
@@ -8932,7 +8969,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
8932
8969
|
] : []
|
|
8933
8970
|
],
|
|
8934
8971
|
hooks: {
|
|
8935
|
-
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
8972
|
+
beforeChange: [createRestrictClientTicketTarget(slugs), createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
8936
8973
|
afterChange: [
|
|
8937
8974
|
...options?.capabilities?.sms ? [createSmsNotification(slugs, options.capabilities.sms)] : [],
|
|
8938
8975
|
createAutoUpdateStatus(slugs),
|
|
@@ -8941,7 +8978,6 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
8941
8978
|
createCheckSlaOnReply(slugs, notificationSlug),
|
|
8942
8979
|
createSyncTicketReplyToChat(slugs),
|
|
8943
8980
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
8944
|
-
createFireMessageWebhooks(slugs),
|
|
8945
8981
|
createDispatchWebhookOnReply(slugs),
|
|
8946
8982
|
createNotifyMentions(slugs),
|
|
8947
8983
|
...options?.capabilities?.threadCleanup ? [createThreadCleanup(options.capabilities.threadCleanup)] : []
|
|
@@ -9034,7 +9070,11 @@ function createEnforce2FA(slugs) {
|
|
|
9034
9070
|
return user;
|
|
9035
9071
|
};
|
|
9036
9072
|
}
|
|
9073
|
+
function createAdminOnlyFieldUpdate(slugs) {
|
|
9074
|
+
return ({ req }) => req.user?.collection === slugs.users;
|
|
9075
|
+
}
|
|
9037
9076
|
function createSupportClientsCollection(slugs, options) {
|
|
9077
|
+
const adminOnlyFieldUpdate = createAdminOnlyFieldUpdate(slugs);
|
|
9038
9078
|
return {
|
|
9039
9079
|
slug: slugs.supportClients,
|
|
9040
9080
|
labels: {
|
|
@@ -9164,7 +9204,8 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9164
9204
|
label: "Opportunites commerciales",
|
|
9165
9205
|
access: {
|
|
9166
9206
|
// Admin-only: never serialize commercial notes onto the client's own doc.
|
|
9167
|
-
read: ({ req }) => req.user?.collection === slugs.users
|
|
9207
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
9208
|
+
update: adminOnlyFieldUpdate
|
|
9168
9209
|
},
|
|
9169
9210
|
admin: {
|
|
9170
9211
|
description: "Notes commerciales (visible uniquement par les admins)"
|
|
@@ -9174,6 +9215,7 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9174
9215
|
name: "tier",
|
|
9175
9216
|
type: "select",
|
|
9176
9217
|
label: "Plan",
|
|
9218
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9177
9219
|
options: [
|
|
9178
9220
|
{ label: "Free", value: "free" },
|
|
9179
9221
|
{ label: "Basic", value: "basic" },
|
|
@@ -9187,6 +9229,7 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9187
9229
|
type: "relationship",
|
|
9188
9230
|
relationTo: slugs.users,
|
|
9189
9231
|
label: "Agent referent",
|
|
9232
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9190
9233
|
admin: { position: "sidebar" }
|
|
9191
9234
|
},
|
|
9192
9235
|
{
|
|
@@ -9202,17 +9245,20 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9202
9245
|
{
|
|
9203
9246
|
name: "twoFactorCode",
|
|
9204
9247
|
type: "text",
|
|
9248
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9205
9249
|
admin: { hidden: true }
|
|
9206
9250
|
},
|
|
9207
9251
|
{
|
|
9208
9252
|
name: "twoFactorExpiry",
|
|
9209
9253
|
type: "date",
|
|
9254
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9210
9255
|
admin: { hidden: true }
|
|
9211
9256
|
},
|
|
9212
9257
|
{
|
|
9213
9258
|
// Set by /support/2fa "verify"; gates login via the beforeLogin hook.
|
|
9214
9259
|
name: "twoFactorVerifiedAt",
|
|
9215
9260
|
type: "date",
|
|
9261
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9216
9262
|
admin: { hidden: true }
|
|
9217
9263
|
},
|
|
9218
9264
|
{
|
|
@@ -9220,6 +9266,7 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9220
9266
|
name: "googleId",
|
|
9221
9267
|
type: "text",
|
|
9222
9268
|
index: true,
|
|
9269
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9223
9270
|
admin: { hidden: true }
|
|
9224
9271
|
},
|
|
9225
9272
|
{
|
|
@@ -9290,7 +9337,8 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9290
9337
|
label: "Notes internes",
|
|
9291
9338
|
access: {
|
|
9292
9339
|
// Admin-only: never serialize internal notes onto the client's own doc.
|
|
9293
|
-
read: ({ req }) => req.user?.collection === slugs.users
|
|
9340
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
9341
|
+
update: adminOnlyFieldUpdate
|
|
9294
9342
|
},
|
|
9295
9343
|
admin: {
|
|
9296
9344
|
description: "Visible uniquement par les admins",
|
|
@@ -9363,36 +9411,49 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9363
9411
|
}
|
|
9364
9412
|
|
|
9365
9413
|
// src/collections/TimeEntries.ts
|
|
9414
|
+
async function recalculateTicketTime(payload, slugs, ticketId) {
|
|
9415
|
+
let totalMinutes = 0;
|
|
9416
|
+
let page = 1;
|
|
9417
|
+
let hasMore = true;
|
|
9418
|
+
while (hasMore) {
|
|
9419
|
+
const entries = await payload.find({
|
|
9420
|
+
collection: slugs.timeEntries,
|
|
9421
|
+
where: { ticket: { equals: ticketId } },
|
|
9422
|
+
limit: 100,
|
|
9423
|
+
page,
|
|
9424
|
+
depth: 0,
|
|
9425
|
+
overrideAccess: true,
|
|
9426
|
+
select: { duration: true }
|
|
9427
|
+
});
|
|
9428
|
+
for (const entry of entries.docs) {
|
|
9429
|
+
totalMinutes += entry.duration || 0;
|
|
9430
|
+
}
|
|
9431
|
+
hasMore = entries.hasNextPage ?? false;
|
|
9432
|
+
page++;
|
|
9433
|
+
}
|
|
9434
|
+
await payload.update({
|
|
9435
|
+
collection: slugs.tickets,
|
|
9436
|
+
id: ticketId,
|
|
9437
|
+
data: { totalTimeMinutes: totalMinutes },
|
|
9438
|
+
overrideAccess: true
|
|
9439
|
+
});
|
|
9440
|
+
}
|
|
9366
9441
|
function createRecalculateTicketTime(slugs) {
|
|
9367
9442
|
return async ({ doc, req }) => {
|
|
9368
9443
|
if (!doc.ticket) return;
|
|
9369
|
-
const { payload } = req;
|
|
9370
9444
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
select: { duration: true }
|
|
9383
|
-
});
|
|
9384
|
-
for (const entry of entries.docs) {
|
|
9385
|
-
totalMinutes += entry.duration || 0;
|
|
9386
|
-
}
|
|
9387
|
-
hasMore = entries.hasNextPage ?? false;
|
|
9388
|
-
page++;
|
|
9445
|
+
await recalculateTicketTime(req.payload, slugs, ticketId);
|
|
9446
|
+
};
|
|
9447
|
+
}
|
|
9448
|
+
function createRecalculateTicketTimeOnDelete(slugs) {
|
|
9449
|
+
return async ({ doc, req }) => {
|
|
9450
|
+
if (!doc?.ticket) return;
|
|
9451
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
9452
|
+
try {
|
|
9453
|
+
await recalculateTicketTime(req.payload, slugs, ticketId);
|
|
9454
|
+
} catch (err) {
|
|
9455
|
+
console.error("[support] Failed to recalculate ticket time after delete:", err);
|
|
9389
9456
|
}
|
|
9390
|
-
await payload.update({
|
|
9391
|
-
collection: slugs.tickets,
|
|
9392
|
-
id: ticketId,
|
|
9393
|
-
data: { totalTimeMinutes: totalMinutes },
|
|
9394
|
-
overrideAccess: true
|
|
9395
|
-
});
|
|
9396
9457
|
};
|
|
9397
9458
|
}
|
|
9398
9459
|
function createTimeEntriesCollection(slugs) {
|
|
@@ -9454,7 +9515,8 @@ function createTimeEntriesCollection(slugs) {
|
|
|
9454
9515
|
}
|
|
9455
9516
|
],
|
|
9456
9517
|
hooks: {
|
|
9457
|
-
afterChange: [createRecalculateTicketTime(slugs)]
|
|
9518
|
+
afterChange: [createRecalculateTicketTime(slugs)],
|
|
9519
|
+
afterDelete: [createRecalculateTicketTimeOnDelete(slugs)]
|
|
9458
9520
|
},
|
|
9459
9521
|
access: {
|
|
9460
9522
|
create: ({ req }) => req.user?.collection === slugs.users,
|
|
@@ -10880,7 +10942,12 @@ function createTicketCollaboratorsCollection(slugs) {
|
|
|
10880
10942
|
}
|
|
10881
10943
|
return false;
|
|
10882
10944
|
},
|
|
10883
|
-
|
|
10945
|
+
// Staff only. This collection is the source of truth for
|
|
10946
|
+
// resolveAccessibleTicketIds(), which widens `read` on tickets, messages
|
|
10947
|
+
// and the activity log — so letting any authenticated principal POST here
|
|
10948
|
+
// let a support client grant themselves access to any ticket by id.
|
|
10949
|
+
// The /invite endpoint is unaffected: it writes with overrideAccess: true.
|
|
10950
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
10884
10951
|
update: () => false,
|
|
10885
10952
|
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10886
10953
|
},
|
|
@@ -11221,4 +11288,4 @@ function supportPlugin(config) {
|
|
|
11221
11288
|
};
|
|
11222
11289
|
}
|
|
11223
11290
|
|
|
11224
|
-
export { DEFAULT_FEATURES, DEFAULT_INBOUND_EMAIL_LIMITS, DEFAULT_SETTINGS, DEFAULT_SLUGS, DEFAULT_USER_PREFS, MemoryRateLimitStore, PayloadRateLimitStore, RateLimiter, calculateBusinessHoursDeadline, createAdminNotification, createAssignSlaDeadlines, createAuthLogsCollection, createCannedResponsesCollection, createChatMessagesCollection, createCheckSlaOnReply, createCheckSlaOnResolve, createEmailLogsCollection, createKnowledgeBaseCollection, createLoginEndpoint, createMacrosCollection, createOAuthGoogleEndpoint, createPendingEmailsCollection, createSatisfactionSurveysCollection, createSlaPoliciesCollection, createSupportClientsCollection, createTicketActivityLogCollection, createTicketCollaboratorsCollection, createTicketMessagesCollection, createTicketStatusEmail, createTicketStatusesCollection, createTicketsCollection, createTimeEntriesCollection, createTrackOpenEndpoint, createWebhookEndpointsCollection, dispatchWebhook, generateTicketSynthesis, readSupportSettings, readUserPrefs, resolveSlugs, supportPlugin, validateInboundEmailPayload, verifySecret };
|
|
11291
|
+
export { DEFAULT_FEATURES, DEFAULT_INBOUND_EMAIL_LIMITS, DEFAULT_SETTINGS, DEFAULT_SLUGS, DEFAULT_TICKETING_FEATURES, DEFAULT_USER_PREFS, MemoryRateLimitStore, PayloadRateLimitStore, RateLimiter, calculateBusinessHoursDeadline, createAdminNotification, createAssignSlaDeadlines, createAuthLogsCollection, createCannedResponsesCollection, createChatMessagesCollection, createCheckSlaOnReply, createCheckSlaOnResolve, createEmailLogsCollection, createKnowledgeBaseCollection, createLoginEndpoint, createMacrosCollection, createOAuthGoogleEndpoint, createPendingEmailsCollection, createSatisfactionSurveysCollection, createSlaPoliciesCollection, createSupportClientsCollection, createTicketActivityLogCollection, createTicketCollaboratorsCollection, createTicketMessagesCollection, createTicketStatusEmail, createTicketStatusesCollection, createTicketsCollection, createTimeEntriesCollection, createTrackOpenEndpoint, createWebhookEndpointsCollection, dispatchWebhook, generateTicketSynthesis, normalizeFeatures, projectAutoClose, readSupportSettings, readSupportSettingsState, readUserPrefs, resolveSlugs, stripProjectedFeatures, supportPlugin, validateInboundEmailPayload, verifySecret };
|