@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.cjs
CHANGED
|
@@ -377,14 +377,81 @@ function dbDelete(payload, slug, options) {
|
|
|
377
377
|
return payload.delete({ collection: slug, ...options });
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
// src/utils/features.ts
|
|
381
|
+
var DEFAULT_TICKETING_FEATURES = {
|
|
382
|
+
timeTracking: true,
|
|
383
|
+
ai: true,
|
|
384
|
+
satisfaction: true,
|
|
385
|
+
chat: true,
|
|
386
|
+
emailTracking: true,
|
|
387
|
+
canned: true,
|
|
388
|
+
merge: true,
|
|
389
|
+
snooze: true,
|
|
390
|
+
externalMessages: true,
|
|
391
|
+
clientHistory: true,
|
|
392
|
+
activityLog: true,
|
|
393
|
+
splitTicket: true,
|
|
394
|
+
scheduledReplies: true,
|
|
395
|
+
autoClose: true,
|
|
396
|
+
autoCloseDays: 7,
|
|
397
|
+
roundRobin: false
|
|
398
|
+
};
|
|
399
|
+
var BOOLEAN_FEATURE_KEYS = [
|
|
400
|
+
"timeTracking",
|
|
401
|
+
"ai",
|
|
402
|
+
"satisfaction",
|
|
403
|
+
"chat",
|
|
404
|
+
"emailTracking",
|
|
405
|
+
"canned",
|
|
406
|
+
"merge",
|
|
407
|
+
"snooze",
|
|
408
|
+
"externalMessages",
|
|
409
|
+
"clientHistory",
|
|
410
|
+
"activityLog",
|
|
411
|
+
"splitTicket",
|
|
412
|
+
"scheduledReplies",
|
|
413
|
+
"autoClose",
|
|
414
|
+
"roundRobin"
|
|
415
|
+
];
|
|
416
|
+
var PROJECTED_FEATURE_KEYS = ["autoClose", "autoCloseDays"];
|
|
417
|
+
function normalizeFeatures(raw) {
|
|
418
|
+
const out = { ...DEFAULT_TICKETING_FEATURES };
|
|
419
|
+
if (!raw || typeof raw !== "object") return out;
|
|
420
|
+
const src = raw;
|
|
421
|
+
for (const key of BOOLEAN_FEATURE_KEYS) {
|
|
422
|
+
if (typeof src[key] === "boolean") out[key] = src[key];
|
|
423
|
+
}
|
|
424
|
+
const days = src.autoCloseDays;
|
|
425
|
+
if (typeof days === "number" && Number.isFinite(days) && days >= 1) {
|
|
426
|
+
out.autoCloseDays = Math.floor(days);
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
function projectAutoClose(features, autoClose) {
|
|
431
|
+
if (!autoClose) return features;
|
|
432
|
+
return {
|
|
433
|
+
...features,
|
|
434
|
+
autoClose: typeof autoClose.enabled === "boolean" ? autoClose.enabled : features.autoClose,
|
|
435
|
+
autoCloseDays: typeof autoClose.daysBeforeClose === "number" && autoClose.daysBeforeClose >= 1 ? Math.floor(autoClose.daysBeforeClose) : features.autoCloseDays
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function stripProjectedFeatures(features) {
|
|
439
|
+
const out = { ...features };
|
|
440
|
+
for (const key of PROJECTED_FEATURE_KEYS) delete out[key];
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
|
|
380
444
|
// src/utils/readSettings.ts
|
|
381
|
-
var
|
|
445
|
+
var SUPPORT_SETTINGS_PREF_KEY = "support-settings";
|
|
446
|
+
var PREF_KEY = SUPPORT_SETTINGS_PREF_KEY;
|
|
382
447
|
var USER_PREFS_KEY_PREFIX = "support-user-prefs";
|
|
448
|
+
var LEGACY_ROUND_ROBIN_KEY = "support-round-robin";
|
|
383
449
|
var DEFAULT_SETTINGS = {
|
|
384
450
|
email: { fromAddress: "", fromName: "Support", replyToAddress: "" },
|
|
385
451
|
ai: { provider: "anthropic", model: "claude-haiku-4-5-20251001", enableSentiment: true, enableSynthesis: true, enableSuggestion: true, enableRewrite: true },
|
|
386
452
|
sla: { firstResponseMinutes: 120, resolutionMinutes: 1440, businessHoursOnly: true, escalationEmail: "" },
|
|
387
|
-
autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 }
|
|
453
|
+
autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 },
|
|
454
|
+
features: { ...DEFAULT_TICKETING_FEATURES }
|
|
388
455
|
};
|
|
389
456
|
var DEFAULT_USER_PREFS = {
|
|
390
457
|
locale: "fr",
|
|
@@ -395,32 +462,71 @@ var SETTINGS_TTL_MS = 6e4;
|
|
|
395
462
|
function invalidateSupportSettingsCache() {
|
|
396
463
|
settingsCache = null;
|
|
397
464
|
}
|
|
398
|
-
|
|
465
|
+
function mergeSupportSettings(stored, base = DEFAULT_SETTINGS) {
|
|
466
|
+
const autoClose = { ...base.autoClose, ...stored?.autoClose };
|
|
467
|
+
return {
|
|
468
|
+
email: { ...base.email, ...stored?.email },
|
|
469
|
+
ai: { ...base.ai, ...stored?.ai },
|
|
470
|
+
sla: { ...base.sla, ...stored?.sla },
|
|
471
|
+
autoClose,
|
|
472
|
+
// `autoClose` / `autoCloseDays` are projections of the block above, so they
|
|
473
|
+
// are recomputed here rather than trusted from whatever was persisted.
|
|
474
|
+
features: projectAutoClose(
|
|
475
|
+
normalizeFeatures({ ...base.features, ...stored?.features }),
|
|
476
|
+
autoClose
|
|
477
|
+
)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function readSupportSettingsState(payload) {
|
|
399
481
|
if (settingsCache && Date.now() - settingsCache.ts < SETTINGS_TTL_MS) {
|
|
400
482
|
return settingsCache.value;
|
|
401
483
|
}
|
|
402
|
-
let value = {
|
|
484
|
+
let value = {
|
|
485
|
+
settings: mergeSupportSettings(null),
|
|
486
|
+
featuresConfigured: false
|
|
487
|
+
};
|
|
403
488
|
try {
|
|
404
489
|
const prefs = await dbFind(payload, "payload-preferences", {
|
|
405
490
|
where: { key: { equals: PREF_KEY } },
|
|
491
|
+
// The upsert is scoped per admin user, so several rows can share the key.
|
|
492
|
+
// Sorting makes "last write wins" deterministic instead of arbitrary.
|
|
493
|
+
sort: "-updatedAt",
|
|
406
494
|
limit: 1,
|
|
407
495
|
depth: 0,
|
|
408
496
|
overrideAccess: true
|
|
409
497
|
});
|
|
410
498
|
if (prefs.docs.length > 0) {
|
|
411
499
|
const stored = prefs.docs[0].value;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
};
|
|
500
|
+
const featuresConfigured = !!stored.features && typeof stored.features === "object";
|
|
501
|
+
const settings = mergeSupportSettings(stored);
|
|
502
|
+
if (!featuresConfigured) {
|
|
503
|
+
settings.features.roundRobin = await readLegacyRoundRobin(payload);
|
|
504
|
+
}
|
|
505
|
+
value = { settings, featuresConfigured };
|
|
418
506
|
}
|
|
419
507
|
} catch {
|
|
420
508
|
}
|
|
421
509
|
settingsCache = { value, ts: Date.now() };
|
|
422
510
|
return value;
|
|
423
511
|
}
|
|
512
|
+
async function readSupportSettings(payload) {
|
|
513
|
+
return (await readSupportSettingsState(payload)).settings;
|
|
514
|
+
}
|
|
515
|
+
async function readLegacyRoundRobin(payload) {
|
|
516
|
+
try {
|
|
517
|
+
const prefs = await dbFind(payload, "payload-preferences", {
|
|
518
|
+
where: { key: { equals: LEGACY_ROUND_ROBIN_KEY } },
|
|
519
|
+
limit: 1,
|
|
520
|
+
depth: 0,
|
|
521
|
+
overrideAccess: true
|
|
522
|
+
});
|
|
523
|
+
if (prefs.docs.length > 0) {
|
|
524
|
+
return prefs.docs[0].value?.enabled === true;
|
|
525
|
+
}
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
return DEFAULT_TICKETING_FEATURES.roundRobin;
|
|
529
|
+
}
|
|
424
530
|
async function readUserPrefs(payload, userId) {
|
|
425
531
|
try {
|
|
426
532
|
const key = `${USER_PREFS_KEY_PREFIX}-${userId}`;
|
|
@@ -441,13 +547,20 @@ async function readUserPrefs(payload, userId) {
|
|
|
441
547
|
}
|
|
442
548
|
return { ...DEFAULT_USER_PREFS };
|
|
443
549
|
}
|
|
550
|
+
function resolveOllamaBaseUrl() {
|
|
551
|
+
const baseURL = process.env.OLLAMA_API_URL;
|
|
552
|
+
if (!baseURL) {
|
|
553
|
+
throw new payload.APIError("provider 'ollama' requires OLLAMA_API_URL", 500);
|
|
554
|
+
}
|
|
555
|
+
return baseURL;
|
|
556
|
+
}
|
|
444
557
|
|
|
445
558
|
// src/endpoints/ai.ts
|
|
446
559
|
async function getClient(aiSettings) {
|
|
447
560
|
const moduleName = "@anthropic-ai/sdk";
|
|
448
561
|
const { default: Anthropic } = await import(moduleName);
|
|
449
562
|
if (aiSettings.provider === "ollama") {
|
|
450
|
-
const baseURL =
|
|
563
|
+
const baseURL = resolveOllamaBaseUrl();
|
|
451
564
|
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
452
565
|
}
|
|
453
566
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
@@ -646,7 +759,7 @@ async function applyAgentDecision(payload, slugs, ticketId, decision) {
|
|
|
646
759
|
function getClient2(aiSettings) {
|
|
647
760
|
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
648
761
|
if (aiSettings.provider === "ollama") {
|
|
649
|
-
return new Anthropic({ apiKey: "ollama", baseURL:
|
|
762
|
+
return new Anthropic({ apiKey: "ollama", baseURL: resolveOllamaBaseUrl() });
|
|
650
763
|
}
|
|
651
764
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
652
765
|
}
|
|
@@ -746,7 +859,7 @@ async function getClient3(aiSettings) {
|
|
|
746
859
|
const moduleName = "@anthropic-ai/sdk";
|
|
747
860
|
const { default: Anthropic } = await import(moduleName);
|
|
748
861
|
if (aiSettings.provider === "ollama") {
|
|
749
|
-
const baseURL =
|
|
862
|
+
const baseURL = resolveOllamaBaseUrl();
|
|
750
863
|
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
751
864
|
}
|
|
752
865
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
@@ -1582,25 +1695,6 @@ function createPresenceGetEndpoint(slugs) {
|
|
|
1582
1695
|
}
|
|
1583
1696
|
|
|
1584
1697
|
// src/endpoints/settings.ts
|
|
1585
|
-
var PREF_KEY2 = "support-settings";
|
|
1586
|
-
var DEFAULT_SUPPORT_SETTINGS = {
|
|
1587
|
-
email: { fromAddress: "", fromName: "Support", replyToAddress: "" },
|
|
1588
|
-
ai: {
|
|
1589
|
-
provider: "anthropic",
|
|
1590
|
-
model: "claude-haiku-4-5-20251001",
|
|
1591
|
-
enableSentiment: true,
|
|
1592
|
-
enableSynthesis: true,
|
|
1593
|
-
enableSuggestion: true,
|
|
1594
|
-
enableRewrite: true
|
|
1595
|
-
},
|
|
1596
|
-
sla: {
|
|
1597
|
-
firstResponseMinutes: 120,
|
|
1598
|
-
resolutionMinutes: 1440,
|
|
1599
|
-
businessHoursOnly: true,
|
|
1600
|
-
escalationEmail: ""
|
|
1601
|
-
},
|
|
1602
|
-
autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 }
|
|
1603
|
-
};
|
|
1604
1698
|
function createSettingsGetEndpoint(slugs) {
|
|
1605
1699
|
return {
|
|
1606
1700
|
path: "/support/settings",
|
|
@@ -1609,23 +1703,8 @@ function createSettingsGetEndpoint(slugs) {
|
|
|
1609
1703
|
try {
|
|
1610
1704
|
const payload = req.payload;
|
|
1611
1705
|
requireAdmin(req, slugs);
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
limit: 1,
|
|
1615
|
-
depth: 0,
|
|
1616
|
-
overrideAccess: true
|
|
1617
|
-
});
|
|
1618
|
-
let settings = { ...DEFAULT_SUPPORT_SETTINGS };
|
|
1619
|
-
if (prefs.docs.length > 0) {
|
|
1620
|
-
const stored = prefs.docs[0].value;
|
|
1621
|
-
settings = {
|
|
1622
|
-
email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...stored.email },
|
|
1623
|
-
ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...stored.ai },
|
|
1624
|
-
sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...stored.sla },
|
|
1625
|
-
autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...stored.autoClose }
|
|
1626
|
-
};
|
|
1627
|
-
}
|
|
1628
|
-
return Response.json(settings);
|
|
1706
|
+
const { settings, featuresConfigured } = await readSupportSettingsState(payload);
|
|
1707
|
+
return Response.json({ ...settings, featuresConfigured });
|
|
1629
1708
|
} catch (error) {
|
|
1630
1709
|
const authResponse = handleAuthError(error);
|
|
1631
1710
|
if (authResponse) return authResponse;
|
|
@@ -1644,30 +1723,31 @@ function createSettingsPostEndpoint(slugs) {
|
|
|
1644
1723
|
const payload = req.payload;
|
|
1645
1724
|
requireAdmin(req, slugs);
|
|
1646
1725
|
const body = await req.json();
|
|
1647
|
-
const
|
|
1648
|
-
|
|
1649
|
-
ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...body.ai },
|
|
1650
|
-
sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...body.sla },
|
|
1651
|
-
autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...body.autoClose }
|
|
1652
|
-
};
|
|
1726
|
+
const { settings: current } = await readSupportSettingsState(payload);
|
|
1727
|
+
const merged = mergeSupportSettings(body, current);
|
|
1653
1728
|
await payload.db.upsert({
|
|
1654
1729
|
collection: "payload-preferences",
|
|
1655
1730
|
data: {
|
|
1656
|
-
key:
|
|
1731
|
+
key: SUPPORT_SETTINGS_PREF_KEY,
|
|
1657
1732
|
user: { relationTo: req.user.collection, value: req.user.id },
|
|
1658
|
-
value:
|
|
1733
|
+
value: {
|
|
1734
|
+
...merged,
|
|
1735
|
+
// Persist the flags without the two projected ones, so `features`
|
|
1736
|
+
// and the `autoClose` block can never drift apart.
|
|
1737
|
+
features: stripProjectedFeatures(merged.features)
|
|
1738
|
+
}
|
|
1659
1739
|
},
|
|
1660
1740
|
req: { payload, user: req.user },
|
|
1661
1741
|
where: {
|
|
1662
1742
|
and: [
|
|
1663
|
-
{ key: { equals:
|
|
1743
|
+
{ key: { equals: SUPPORT_SETTINGS_PREF_KEY } },
|
|
1664
1744
|
{ "user.value": { equals: req.user.id } },
|
|
1665
1745
|
{ "user.relationTo": { equals: req.user.collection } }
|
|
1666
1746
|
]
|
|
1667
1747
|
}
|
|
1668
1748
|
});
|
|
1669
1749
|
invalidateSupportSettingsCache();
|
|
1670
|
-
return Response.json(merged);
|
|
1750
|
+
return Response.json({ ...merged, featuresConfigured: true });
|
|
1671
1751
|
} catch (error) {
|
|
1672
1752
|
const authResponse = handleAuthError(error);
|
|
1673
1753
|
if (authResponse) return authResponse;
|
|
@@ -1679,7 +1759,7 @@ function createSettingsPostEndpoint(slugs) {
|
|
|
1679
1759
|
}
|
|
1680
1760
|
|
|
1681
1761
|
// src/endpoints/signature.ts
|
|
1682
|
-
var
|
|
1762
|
+
var PREF_KEY2 = "email-signature";
|
|
1683
1763
|
function createSignatureGetEndpoint(slugs) {
|
|
1684
1764
|
return {
|
|
1685
1765
|
path: "/support/signature",
|
|
@@ -1689,7 +1769,7 @@ function createSignatureGetEndpoint(slugs) {
|
|
|
1689
1769
|
const payload = req.payload;
|
|
1690
1770
|
requireAdmin(req, slugs);
|
|
1691
1771
|
const prefs = await dbFind(payload, "payload-preferences", {
|
|
1692
|
-
where: { key: { equals: `${
|
|
1772
|
+
where: { key: { equals: `${PREF_KEY2}-${req.user.id}` } },
|
|
1693
1773
|
limit: 1,
|
|
1694
1774
|
depth: 0,
|
|
1695
1775
|
overrideAccess: true
|
|
@@ -1714,7 +1794,7 @@ function createSignaturePostEndpoint(slugs) {
|
|
|
1714
1794
|
const payload = req.payload;
|
|
1715
1795
|
requireAdmin(req, slugs);
|
|
1716
1796
|
const { signature } = await req.json();
|
|
1717
|
-
const key = `${
|
|
1797
|
+
const key = `${PREF_KEY2}-${req.user.id}`;
|
|
1718
1798
|
const existing = await dbFind(payload, "payload-preferences", {
|
|
1719
1799
|
where: { key: { equals: key } },
|
|
1720
1800
|
limit: 1,
|
|
@@ -1749,7 +1829,6 @@ function createSignaturePostEndpoint(slugs) {
|
|
|
1749
1829
|
}
|
|
1750
1830
|
|
|
1751
1831
|
// src/endpoints/round-robin-config.ts
|
|
1752
|
-
var PREF_KEY4 = "support-round-robin";
|
|
1753
1832
|
function createRoundRobinConfigGetEndpoint(slugs) {
|
|
1754
1833
|
return {
|
|
1755
1834
|
path: "/support/round-robin-config",
|
|
@@ -1758,14 +1837,8 @@ function createRoundRobinConfigGetEndpoint(slugs) {
|
|
|
1758
1837
|
try {
|
|
1759
1838
|
const payload = req.payload;
|
|
1760
1839
|
requireAdmin(req, slugs);
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
limit: 1,
|
|
1764
|
-
depth: 0,
|
|
1765
|
-
overrideAccess: true
|
|
1766
|
-
});
|
|
1767
|
-
const enabled = prefs.docs.length > 0 ? prefs.docs[0].value?.enabled === true : false;
|
|
1768
|
-
return Response.json({ enabled });
|
|
1840
|
+
const { settings } = await readSupportSettingsState(payload);
|
|
1841
|
+
return Response.json({ enabled: settings.features.roundRobin });
|
|
1769
1842
|
} catch (error) {
|
|
1770
1843
|
const authResponse = handleAuthError(error);
|
|
1771
1844
|
if (authResponse) return authResponse;
|
|
@@ -1784,28 +1857,31 @@ function createRoundRobinConfigPostEndpoint(slugs) {
|
|
|
1784
1857
|
const payload = req.payload;
|
|
1785
1858
|
requireAdmin(req, slugs);
|
|
1786
1859
|
const { enabled } = await req.json();
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
});
|
|
1860
|
+
const { settings: current } = await readSupportSettingsState(payload);
|
|
1861
|
+
const merged = mergeSupportSettings(
|
|
1862
|
+
{ features: { ...current.features, roundRobin: !!enabled } },
|
|
1863
|
+
current
|
|
1864
|
+
);
|
|
1793
1865
|
await payload.db.upsert({
|
|
1794
1866
|
collection: "payload-preferences",
|
|
1795
1867
|
data: {
|
|
1796
|
-
key:
|
|
1868
|
+
key: SUPPORT_SETTINGS_PREF_KEY,
|
|
1797
1869
|
user: { relationTo: req.user.collection, value: req.user.id },
|
|
1798
|
-
value: {
|
|
1870
|
+
value: {
|
|
1871
|
+
...merged,
|
|
1872
|
+
features: stripProjectedFeatures(merged.features)
|
|
1873
|
+
}
|
|
1799
1874
|
},
|
|
1800
1875
|
req: { payload, user: req.user },
|
|
1801
1876
|
where: {
|
|
1802
1877
|
and: [
|
|
1803
|
-
{ key: { equals:
|
|
1878
|
+
{ key: { equals: SUPPORT_SETTINGS_PREF_KEY } },
|
|
1804
1879
|
{ "user.value": { equals: req.user.id } },
|
|
1805
1880
|
{ "user.relationTo": { equals: req.user.collection } }
|
|
1806
1881
|
]
|
|
1807
1882
|
}
|
|
1808
1883
|
});
|
|
1884
|
+
invalidateSupportSettingsCache();
|
|
1809
1885
|
return Response.json({ enabled: !!enabled });
|
|
1810
1886
|
} catch (error) {
|
|
1811
1887
|
const authResponse = handleAuthError(error);
|
|
@@ -4067,7 +4143,7 @@ function createInvoiceEndpoint(slugs) {
|
|
|
4067
4143
|
function getClient4(aiSettings) {
|
|
4068
4144
|
const Anthropic = __require("@anthropic-ai/sdk").default;
|
|
4069
4145
|
if (aiSettings.provider === "ollama") {
|
|
4070
|
-
const baseURL =
|
|
4146
|
+
const baseURL = resolveOllamaBaseUrl();
|
|
4071
4147
|
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
4072
4148
|
}
|
|
4073
4149
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
@@ -5760,62 +5836,73 @@ function createImportConversationEndpoint(slugs, store) {
|
|
|
5760
5836
|
}
|
|
5761
5837
|
};
|
|
5762
5838
|
}
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5839
|
+
function dispatchWebhook(data, event, payload, slugs) {
|
|
5840
|
+
const done = _dispatch(data, event, payload, slugs);
|
|
5841
|
+
return done;
|
|
5842
|
+
}
|
|
5843
|
+
async function _dispatch(data, event, payload, slugs) {
|
|
5766
5844
|
try {
|
|
5767
|
-
const endpoints = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5845
|
+
const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
|
|
5768
5846
|
where: {
|
|
5769
|
-
|
|
5770
|
-
|
|
5847
|
+
and: [
|
|
5848
|
+
{ active: { equals: true } },
|
|
5849
|
+
{ events: { contains: event } }
|
|
5850
|
+
]
|
|
5771
5851
|
},
|
|
5772
5852
|
limit: 50,
|
|
5773
5853
|
depth: 0,
|
|
5774
5854
|
overrideAccess: true
|
|
5775
5855
|
});
|
|
5776
|
-
if (endpoints.
|
|
5777
|
-
const
|
|
5856
|
+
if (endpoints.length === 0) return;
|
|
5857
|
+
const body = JSON.stringify({ event, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5778
5858
|
await Promise.allSettled(
|
|
5779
|
-
endpoints.
|
|
5780
|
-
try {
|
|
5781
|
-
const res = await fetch(endpoint.url, {
|
|
5782
|
-
method: "POST",
|
|
5783
|
-
headers: {
|
|
5784
|
-
"Content-Type": "application/json",
|
|
5785
|
-
...endpoint.secret ? { "X-Webhook-Secret": endpoint.secret } : {}
|
|
5786
|
-
},
|
|
5787
|
-
body: JSON.stringify({ event, data, timestamp }),
|
|
5788
|
-
signal: AbortSignal.timeout(1e4)
|
|
5789
|
-
});
|
|
5790
|
-
try {
|
|
5791
|
-
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5792
|
-
id: endpoint.id,
|
|
5793
|
-
data: {
|
|
5794
|
-
lastTriggeredAt: timestamp,
|
|
5795
|
-
lastStatus: res.status
|
|
5796
|
-
},
|
|
5797
|
-
overrideAccess: true
|
|
5798
|
-
});
|
|
5799
|
-
} catch {
|
|
5800
|
-
}
|
|
5801
|
-
} catch (error) {
|
|
5802
|
-
console.warn(`[support] Webhook delivery failed: ${endpoint.url}`, error);
|
|
5803
|
-
try {
|
|
5804
|
-
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5805
|
-
id: endpoint.id,
|
|
5806
|
-
data: {
|
|
5807
|
-
lastTriggeredAt: timestamp,
|
|
5808
|
-
lastStatus: 0
|
|
5809
|
-
},
|
|
5810
|
-
overrideAccess: true
|
|
5811
|
-
});
|
|
5812
|
-
} catch {
|
|
5813
|
-
}
|
|
5814
|
-
}
|
|
5815
|
-
})
|
|
5859
|
+
endpoints.map((endpoint) => _sendToEndpoint(endpoint, body, payload, slugs))
|
|
5816
5860
|
);
|
|
5817
|
-
} catch (
|
|
5818
|
-
console.error(
|
|
5861
|
+
} catch (err) {
|
|
5862
|
+
console.error(`[webhook] Failed to fetch endpoints for event ${event}:`, err);
|
|
5863
|
+
}
|
|
5864
|
+
}
|
|
5865
|
+
async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
5866
|
+
try {
|
|
5867
|
+
const headers = {
|
|
5868
|
+
"Content-Type": "application/json",
|
|
5869
|
+
"User-Agent": "PayloadSupport-Webhook/1.0"
|
|
5870
|
+
};
|
|
5871
|
+
if (endpoint.secret) {
|
|
5872
|
+
const signature = crypto3__default.default.createHmac("sha256", endpoint.secret).update(body).digest("hex");
|
|
5873
|
+
headers["X-Webhook-Signature"] = signature;
|
|
5874
|
+
}
|
|
5875
|
+
const response = await fetch(endpoint.url, {
|
|
5876
|
+
method: "POST",
|
|
5877
|
+
headers,
|
|
5878
|
+
body,
|
|
5879
|
+
signal: AbortSignal.timeout(1e4)
|
|
5880
|
+
// 10s timeout
|
|
5881
|
+
});
|
|
5882
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5883
|
+
id: endpoint.id,
|
|
5884
|
+
data: {
|
|
5885
|
+
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5886
|
+
lastStatus: response.status
|
|
5887
|
+
},
|
|
5888
|
+
overrideAccess: true
|
|
5889
|
+
});
|
|
5890
|
+
if (!response.ok) {
|
|
5891
|
+
console.warn(`[webhook] ${endpoint.name || endpoint.url} returned ${response.status}`);
|
|
5892
|
+
}
|
|
5893
|
+
} catch (err) {
|
|
5894
|
+
console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
|
|
5895
|
+
try {
|
|
5896
|
+
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
5897
|
+
id: endpoint.id,
|
|
5898
|
+
data: {
|
|
5899
|
+
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5900
|
+
lastStatus: 0
|
|
5901
|
+
},
|
|
5902
|
+
overrideAccess: true
|
|
5903
|
+
});
|
|
5904
|
+
} catch {
|
|
5905
|
+
}
|
|
5819
5906
|
}
|
|
5820
5907
|
}
|
|
5821
5908
|
|
|
@@ -5902,13 +5989,18 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5902
5989
|
overrideAccess: true
|
|
5903
5990
|
});
|
|
5904
5991
|
}
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5992
|
+
dispatchWebhook(
|
|
5993
|
+
{
|
|
5994
|
+
ticketId,
|
|
5995
|
+
messageId: msg.id,
|
|
5996
|
+
authorType: msg.authorType,
|
|
5997
|
+
scheduled: true,
|
|
5998
|
+
body: msg.body?.length > 500 ? msg.body.slice(0, 500) + "..." : msg.body
|
|
5999
|
+
},
|
|
6000
|
+
"ticket_replied",
|
|
6001
|
+
payload,
|
|
6002
|
+
slugs
|
|
6003
|
+
);
|
|
5912
6004
|
results.processed++;
|
|
5913
6005
|
} catch (err) {
|
|
5914
6006
|
console.error(`[process-scheduled] Error processing message ${message.id}:`, err);
|
|
@@ -6878,74 +6970,6 @@ async function createAdminNotification(payload, data, collectionSlug = "admin-no
|
|
|
6878
6970
|
console.error("[notification] Failed to create:", err);
|
|
6879
6971
|
}
|
|
6880
6972
|
}
|
|
6881
|
-
function dispatchWebhook(data, event, payload, slugs) {
|
|
6882
|
-
void _dispatch(data, event, payload, slugs);
|
|
6883
|
-
}
|
|
6884
|
-
async function _dispatch(data, event, payload, slugs) {
|
|
6885
|
-
try {
|
|
6886
|
-
const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
|
|
6887
|
-
where: {
|
|
6888
|
-
and: [
|
|
6889
|
-
{ active: { equals: true } },
|
|
6890
|
-
{ events: { contains: event } }
|
|
6891
|
-
]
|
|
6892
|
-
},
|
|
6893
|
-
limit: 50,
|
|
6894
|
-
depth: 0,
|
|
6895
|
-
overrideAccess: true
|
|
6896
|
-
});
|
|
6897
|
-
if (endpoints.length === 0) return;
|
|
6898
|
-
const body = JSON.stringify({ event, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6899
|
-
for (const endpoint of endpoints) {
|
|
6900
|
-
void _sendToEndpoint(endpoint, body, payload, slugs);
|
|
6901
|
-
}
|
|
6902
|
-
} catch (err) {
|
|
6903
|
-
console.error(`[webhook] Failed to fetch endpoints for event ${event}:`, err);
|
|
6904
|
-
}
|
|
6905
|
-
}
|
|
6906
|
-
async function _sendToEndpoint(endpoint, body, payload, slugs) {
|
|
6907
|
-
try {
|
|
6908
|
-
const headers = {
|
|
6909
|
-
"Content-Type": "application/json",
|
|
6910
|
-
"User-Agent": "PayloadSupport-Webhook/1.0"
|
|
6911
|
-
};
|
|
6912
|
-
if (endpoint.secret) {
|
|
6913
|
-
const signature = crypto3__default.default.createHmac("sha256", endpoint.secret).update(body).digest("hex");
|
|
6914
|
-
headers["X-Webhook-Signature"] = signature;
|
|
6915
|
-
}
|
|
6916
|
-
const response = await fetch(endpoint.url, {
|
|
6917
|
-
method: "POST",
|
|
6918
|
-
headers,
|
|
6919
|
-
body,
|
|
6920
|
-
signal: AbortSignal.timeout(1e4)
|
|
6921
|
-
// 10s timeout
|
|
6922
|
-
});
|
|
6923
|
-
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6924
|
-
id: endpoint.id,
|
|
6925
|
-
data: {
|
|
6926
|
-
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6927
|
-
lastStatus: response.status
|
|
6928
|
-
},
|
|
6929
|
-
overrideAccess: true
|
|
6930
|
-
});
|
|
6931
|
-
if (!response.ok) {
|
|
6932
|
-
console.warn(`[webhook] ${endpoint.name || endpoint.url} returned ${response.status}`);
|
|
6933
|
-
}
|
|
6934
|
-
} catch (err) {
|
|
6935
|
-
console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
|
|
6936
|
-
try {
|
|
6937
|
-
await dbUpdate(payload, slugs.webhookEndpoints, {
|
|
6938
|
-
id: endpoint.id,
|
|
6939
|
-
data: {
|
|
6940
|
-
lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6941
|
-
lastStatus: 0
|
|
6942
|
-
},
|
|
6943
|
-
overrideAccess: true
|
|
6944
|
-
});
|
|
6945
|
-
} catch {
|
|
6946
|
-
}
|
|
6947
|
-
}
|
|
6948
|
-
}
|
|
6949
6973
|
|
|
6950
6974
|
// src/hooks/ticketStatusEmail.ts
|
|
6951
6975
|
var STATUS_LABELS = {
|
|
@@ -7562,12 +7586,28 @@ function createAssignTicketNumber(slugs, options) {
|
|
|
7562
7586
|
}
|
|
7563
7587
|
function createAssignClientOnCreate(slugs) {
|
|
7564
7588
|
return async ({ data, operation, req }) => {
|
|
7565
|
-
if (operation === "create" && req.user?.collection === slugs.supportClients
|
|
7589
|
+
if (operation === "create" && req.user?.collection === slugs.supportClients) {
|
|
7566
7590
|
data.client = req.user.id;
|
|
7567
7591
|
}
|
|
7568
7592
|
return data;
|
|
7569
7593
|
};
|
|
7570
7594
|
}
|
|
7595
|
+
var CLIENT_CREATABLE_TICKET_FIELDS = ["subject", "category", "priority", "project"];
|
|
7596
|
+
function createRestrictClientCreate(slugs) {
|
|
7597
|
+
return async ({ data, operation, req }) => {
|
|
7598
|
+
if (operation !== "create") return data;
|
|
7599
|
+
if (req.user?.collection !== slugs.supportClients) return data;
|
|
7600
|
+
const incoming = data ?? {};
|
|
7601
|
+
const sanitized = {};
|
|
7602
|
+
for (const field2 of CLIENT_CREATABLE_TICKET_FIELDS) {
|
|
7603
|
+
if (incoming[field2] !== void 0) sanitized[field2] = incoming[field2];
|
|
7604
|
+
}
|
|
7605
|
+
sanitized.status = "open";
|
|
7606
|
+
sanitized.client = req.user.id;
|
|
7607
|
+
sanitized.source = "portal";
|
|
7608
|
+
return sanitized;
|
|
7609
|
+
};
|
|
7610
|
+
}
|
|
7571
7611
|
var autoPaidAt = async ({ data, operation, originalDoc }) => {
|
|
7572
7612
|
if (operation !== "update") return data;
|
|
7573
7613
|
if (data.paymentStatus === "paid" && originalDoc?.paymentStatus !== "paid" && !data.paidAt) {
|
|
@@ -7593,16 +7633,8 @@ function createAutoAssignAdmin(slugs) {
|
|
|
7593
7633
|
const { payload } = req;
|
|
7594
7634
|
let roundRobinEnabled = false;
|
|
7595
7635
|
try {
|
|
7596
|
-
const
|
|
7597
|
-
|
|
7598
|
-
where: { key: { equals: "support-round-robin" } },
|
|
7599
|
-
limit: 1,
|
|
7600
|
-
depth: 0,
|
|
7601
|
-
overrideAccess: true
|
|
7602
|
-
});
|
|
7603
|
-
if (prefs.docs.length > 0) {
|
|
7604
|
-
roundRobinEnabled = prefs.docs[0].value?.enabled === true;
|
|
7605
|
-
}
|
|
7636
|
+
const settings = await readSupportSettings(payload);
|
|
7637
|
+
roundRobinEnabled = settings.features.roundRobin === true;
|
|
7606
7638
|
} catch (err) {
|
|
7607
7639
|
console.warn("[support] Failed to read round-robin preferences:", err);
|
|
7608
7640
|
}
|
|
@@ -7811,54 +7843,6 @@ function createNotifyClientOnResolve(slugs) {
|
|
|
7811
7843
|
return doc;
|
|
7812
7844
|
};
|
|
7813
7845
|
}
|
|
7814
|
-
function createFireTicketWebhooks(slugs) {
|
|
7815
|
-
return async ({ doc, previousDoc, operation, req }) => {
|
|
7816
|
-
const { payload } = req;
|
|
7817
|
-
if (operation === "create") {
|
|
7818
|
-
fireWebhooks(payload, slugs, "ticket_created", {
|
|
7819
|
-
id: doc.id,
|
|
7820
|
-
ticketNumber: doc.ticketNumber,
|
|
7821
|
-
subject: doc.subject,
|
|
7822
|
-
status: doc.status,
|
|
7823
|
-
priority: doc.priority,
|
|
7824
|
-
category: doc.category
|
|
7825
|
-
});
|
|
7826
|
-
return doc;
|
|
7827
|
-
}
|
|
7828
|
-
if (operation === "update" && previousDoc) {
|
|
7829
|
-
if (previousDoc.status !== doc.status && doc.status === "resolved") {
|
|
7830
|
-
fireWebhooks(payload, slugs, "ticket_resolved", {
|
|
7831
|
-
id: doc.id,
|
|
7832
|
-
ticketNumber: doc.ticketNumber,
|
|
7833
|
-
subject: doc.subject,
|
|
7834
|
-
previousStatus: previousDoc.status
|
|
7835
|
-
});
|
|
7836
|
-
const clientId = typeof doc.client === "object" ? doc.client?.id : doc.client;
|
|
7837
|
-
if (clientId) {
|
|
7838
|
-
payload.update({
|
|
7839
|
-
collection: "client-summaries",
|
|
7840
|
-
where: { client: { equals: clientId } },
|
|
7841
|
-
data: { generatedAt: (/* @__PURE__ */ new Date(0)).toISOString() },
|
|
7842
|
-
// Force cache expiry
|
|
7843
|
-
overrideAccess: true
|
|
7844
|
-
}).catch(() => {
|
|
7845
|
-
});
|
|
7846
|
-
}
|
|
7847
|
-
}
|
|
7848
|
-
const oldAssigned = typeof previousDoc.assignedTo === "object" ? previousDoc.assignedTo?.id : previousDoc.assignedTo;
|
|
7849
|
-
const newAssigned = typeof doc.assignedTo === "object" ? doc.assignedTo?.id : doc.assignedTo;
|
|
7850
|
-
if (newAssigned && oldAssigned !== newAssigned) {
|
|
7851
|
-
fireWebhooks(payload, slugs, "ticket_assigned", {
|
|
7852
|
-
id: doc.id,
|
|
7853
|
-
ticketNumber: doc.ticketNumber,
|
|
7854
|
-
subject: doc.subject,
|
|
7855
|
-
assignedTo: newAssigned
|
|
7856
|
-
});
|
|
7857
|
-
}
|
|
7858
|
-
}
|
|
7859
|
-
return doc;
|
|
7860
|
-
};
|
|
7861
|
-
}
|
|
7862
7846
|
function createNotifyAdminOnNewTicket(slugs, notificationSlug) {
|
|
7863
7847
|
return async ({ doc, operation, req }) => {
|
|
7864
7848
|
if (operation !== "create") return doc;
|
|
@@ -7879,21 +7863,66 @@ function createNotifyAdminOnNewTicket(slugs, notificationSlug) {
|
|
|
7879
7863
|
}
|
|
7880
7864
|
function createDispatchWebhookOnTicket(slugs) {
|
|
7881
7865
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
7866
|
+
const { payload } = req;
|
|
7882
7867
|
if (operation === "create") {
|
|
7883
7868
|
dispatchWebhook(
|
|
7884
|
-
{
|
|
7869
|
+
{
|
|
7870
|
+
ticketId: doc.id,
|
|
7871
|
+
id: doc.id,
|
|
7872
|
+
ticketNumber: doc.ticketNumber,
|
|
7873
|
+
subject: doc.subject,
|
|
7874
|
+
status: doc.status,
|
|
7875
|
+
priority: doc.priority,
|
|
7876
|
+
category: doc.category
|
|
7877
|
+
},
|
|
7885
7878
|
"ticket_created",
|
|
7886
|
-
|
|
7879
|
+
payload,
|
|
7887
7880
|
slugs
|
|
7888
7881
|
);
|
|
7882
|
+
return doc;
|
|
7889
7883
|
}
|
|
7890
|
-
if (operation === "update" && previousDoc
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7884
|
+
if (operation === "update" && previousDoc) {
|
|
7885
|
+
if (previousDoc.status !== doc.status && doc.status === "resolved") {
|
|
7886
|
+
dispatchWebhook(
|
|
7887
|
+
{
|
|
7888
|
+
ticketId: doc.id,
|
|
7889
|
+
id: doc.id,
|
|
7890
|
+
ticketNumber: doc.ticketNumber,
|
|
7891
|
+
subject: doc.subject,
|
|
7892
|
+
previousStatus: previousDoc.status
|
|
7893
|
+
},
|
|
7894
|
+
"ticket_resolved",
|
|
7895
|
+
payload,
|
|
7896
|
+
slugs
|
|
7897
|
+
);
|
|
7898
|
+
const clientId = typeof doc.client === "object" ? doc.client?.id : doc.client;
|
|
7899
|
+
if (clientId) {
|
|
7900
|
+
payload.update({
|
|
7901
|
+
collection: "client-summaries",
|
|
7902
|
+
where: { client: { equals: clientId } },
|
|
7903
|
+
data: { generatedAt: (/* @__PURE__ */ new Date(0)).toISOString() },
|
|
7904
|
+
// Force cache expiry
|
|
7905
|
+
overrideAccess: true
|
|
7906
|
+
}).catch(() => {
|
|
7907
|
+
});
|
|
7908
|
+
}
|
|
7909
|
+
}
|
|
7910
|
+
const oldAssigned = typeof previousDoc.assignedTo === "object" ? previousDoc.assignedTo?.id : previousDoc.assignedTo;
|
|
7911
|
+
const newAssigned = typeof doc.assignedTo === "object" ? doc.assignedTo?.id : doc.assignedTo;
|
|
7912
|
+
if (newAssigned && oldAssigned !== newAssigned) {
|
|
7913
|
+
dispatchWebhook(
|
|
7914
|
+
{
|
|
7915
|
+
ticketId: doc.id,
|
|
7916
|
+
id: doc.id,
|
|
7917
|
+
ticketNumber: doc.ticketNumber,
|
|
7918
|
+
subject: doc.subject,
|
|
7919
|
+
assignedTo: newAssigned
|
|
7920
|
+
},
|
|
7921
|
+
"ticket_assigned",
|
|
7922
|
+
payload,
|
|
7923
|
+
slugs
|
|
7924
|
+
);
|
|
7925
|
+
}
|
|
7897
7926
|
}
|
|
7898
7927
|
return doc;
|
|
7899
7928
|
};
|
|
@@ -8019,7 +8048,10 @@ function createTicketsCollection(slugs, options) {
|
|
|
8019
8048
|
{ label: "Paiement partiel", value: "partial" },
|
|
8020
8049
|
{ label: "Pay\xE9", value: "paid" }
|
|
8021
8050
|
],
|
|
8022
|
-
|
|
8051
|
+
// `slugs.users`, not the literal 'users': the users collection slug is
|
|
8052
|
+
// configurable, and a hard-coded one silently opens this field to everybody
|
|
8053
|
+
// on a host that renamed it.
|
|
8054
|
+
access: { update: ({ req }) => req.user?.collection === slugs.users },
|
|
8023
8055
|
admin: { width: "50%", condition: (data) => !!data?.invoice }
|
|
8024
8056
|
},
|
|
8025
8057
|
{
|
|
@@ -8301,6 +8333,7 @@ function createTicketsCollection(slugs, options) {
|
|
|
8301
8333
|
{ name: "totalTimeMinutes", type: "number", defaultValue: 0, label: "Temps total (minutes)", admin: { readOnly: true, position: "sidebar" } }
|
|
8302
8334
|
],
|
|
8303
8335
|
hooks: {
|
|
8336
|
+
beforeValidate: [createRestrictClientCreate(slugs)],
|
|
8304
8337
|
beforeChange: [
|
|
8305
8338
|
createAssignTicketNumber(slugs, options?.ticketNumber),
|
|
8306
8339
|
createAssignClientOnCreate(slugs),
|
|
@@ -8321,7 +8354,6 @@ function createTicketsCollection(slugs, options) {
|
|
|
8321
8354
|
createNotifyClientOnResolve(slugs),
|
|
8322
8355
|
createTicketStatusEmail(slugs),
|
|
8323
8356
|
createNotifyAdminOnNewTicket(slugs, notificationSlug),
|
|
8324
|
-
createFireTicketWebhooks(slugs),
|
|
8325
8357
|
createDispatchWebhookOnTicket(slugs),
|
|
8326
8358
|
createApplyAutomationRules(slugs)
|
|
8327
8359
|
],
|
|
@@ -8502,6 +8534,21 @@ async function queueClientNotification(payload, slugs, item) {
|
|
|
8502
8534
|
}
|
|
8503
8535
|
|
|
8504
8536
|
// src/collections/TicketMessages.ts
|
|
8537
|
+
function createRestrictClientTicketTarget(slugs) {
|
|
8538
|
+
return async ({ data, operation, req, originalDoc }) => {
|
|
8539
|
+
if (req.user?.collection !== slugs.supportClients) return data;
|
|
8540
|
+
const raw = data.ticket ?? (operation === "update" ? originalDoc?.ticket : void 0);
|
|
8541
|
+
const targetId = raw && typeof raw === "object" ? raw.id : raw;
|
|
8542
|
+
if (targetId === void 0 || targetId === null || targetId === "") {
|
|
8543
|
+
throw new payload.APIError("Ticket cible requis.", 400);
|
|
8544
|
+
}
|
|
8545
|
+
const accessible = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
|
|
8546
|
+
if (!accessible.some((id) => String(id) === String(targetId))) {
|
|
8547
|
+
throw new payload.APIError("Ticket inaccessible.", 403);
|
|
8548
|
+
}
|
|
8549
|
+
return data;
|
|
8550
|
+
};
|
|
8551
|
+
}
|
|
8505
8552
|
function createAssignAuthor(slugs) {
|
|
8506
8553
|
return async ({ data, operation, req }) => {
|
|
8507
8554
|
if (operation === "create" && req.user?.collection === slugs.supportClients) {
|
|
@@ -8736,21 +8783,6 @@ function createNotifyAdminOnClientMessage(slugs, notificationSlug) {
|
|
|
8736
8783
|
return doc;
|
|
8737
8784
|
};
|
|
8738
8785
|
}
|
|
8739
|
-
function createFireMessageWebhooks(slugs) {
|
|
8740
|
-
return async ({ doc, operation, req }) => {
|
|
8741
|
-
if (operation !== "create") return doc;
|
|
8742
|
-
if (doc.scheduledAt && !doc.scheduledSent) return doc;
|
|
8743
|
-
if (doc.isInternal) return doc;
|
|
8744
|
-
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8745
|
-
fireWebhooks(req.payload, slugs, "ticket_replied", {
|
|
8746
|
-
ticketId,
|
|
8747
|
-
messageId: doc.id,
|
|
8748
|
-
authorType: doc.authorType,
|
|
8749
|
-
body: doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body
|
|
8750
|
-
});
|
|
8751
|
-
return doc;
|
|
8752
|
-
};
|
|
8753
|
-
}
|
|
8754
8786
|
function createDispatchWebhookOnReply(slugs) {
|
|
8755
8787
|
return async ({ doc, operation, req }) => {
|
|
8756
8788
|
if (operation !== "create") return doc;
|
|
@@ -8758,7 +8790,12 @@ function createDispatchWebhookOnReply(slugs) {
|
|
|
8758
8790
|
if (doc.scheduledAt && !doc.scheduledSent) return doc;
|
|
8759
8791
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8760
8792
|
dispatchWebhook(
|
|
8761
|
-
{
|
|
8793
|
+
{
|
|
8794
|
+
ticketId,
|
|
8795
|
+
messageId: doc.id,
|
|
8796
|
+
authorType: doc.authorType,
|
|
8797
|
+
body: doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body
|
|
8798
|
+
},
|
|
8762
8799
|
"ticket_replied",
|
|
8763
8800
|
req.payload,
|
|
8764
8801
|
slugs
|
|
@@ -8941,7 +8978,7 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
8941
8978
|
] : []
|
|
8942
8979
|
],
|
|
8943
8980
|
hooks: {
|
|
8944
|
-
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
8981
|
+
beforeChange: [createRestrictClientTicketTarget(slugs), createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
8945
8982
|
afterChange: [
|
|
8946
8983
|
...options?.capabilities?.sms ? [createSmsNotification(slugs, options.capabilities.sms)] : [],
|
|
8947
8984
|
createAutoUpdateStatus(slugs),
|
|
@@ -8950,7 +8987,6 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
8950
8987
|
createCheckSlaOnReply(slugs, notificationSlug),
|
|
8951
8988
|
createSyncTicketReplyToChat(slugs),
|
|
8952
8989
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
8953
|
-
createFireMessageWebhooks(slugs),
|
|
8954
8990
|
createDispatchWebhookOnReply(slugs),
|
|
8955
8991
|
createNotifyMentions(slugs),
|
|
8956
8992
|
...options?.capabilities?.threadCleanup ? [createThreadCleanup(options.capabilities.threadCleanup)] : []
|
|
@@ -9043,7 +9079,11 @@ function createEnforce2FA(slugs) {
|
|
|
9043
9079
|
return user;
|
|
9044
9080
|
};
|
|
9045
9081
|
}
|
|
9082
|
+
function createAdminOnlyFieldUpdate(slugs) {
|
|
9083
|
+
return ({ req }) => req.user?.collection === slugs.users;
|
|
9084
|
+
}
|
|
9046
9085
|
function createSupportClientsCollection(slugs, options) {
|
|
9086
|
+
const adminOnlyFieldUpdate = createAdminOnlyFieldUpdate(slugs);
|
|
9047
9087
|
return {
|
|
9048
9088
|
slug: slugs.supportClients,
|
|
9049
9089
|
labels: {
|
|
@@ -9173,7 +9213,8 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9173
9213
|
label: "Opportunites commerciales",
|
|
9174
9214
|
access: {
|
|
9175
9215
|
// Admin-only: never serialize commercial notes onto the client's own doc.
|
|
9176
|
-
read: ({ req }) => req.user?.collection === slugs.users
|
|
9216
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
9217
|
+
update: adminOnlyFieldUpdate
|
|
9177
9218
|
},
|
|
9178
9219
|
admin: {
|
|
9179
9220
|
description: "Notes commerciales (visible uniquement par les admins)"
|
|
@@ -9183,6 +9224,7 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9183
9224
|
name: "tier",
|
|
9184
9225
|
type: "select",
|
|
9185
9226
|
label: "Plan",
|
|
9227
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9186
9228
|
options: [
|
|
9187
9229
|
{ label: "Free", value: "free" },
|
|
9188
9230
|
{ label: "Basic", value: "basic" },
|
|
@@ -9196,6 +9238,7 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9196
9238
|
type: "relationship",
|
|
9197
9239
|
relationTo: slugs.users,
|
|
9198
9240
|
label: "Agent referent",
|
|
9241
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9199
9242
|
admin: { position: "sidebar" }
|
|
9200
9243
|
},
|
|
9201
9244
|
{
|
|
@@ -9211,17 +9254,20 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9211
9254
|
{
|
|
9212
9255
|
name: "twoFactorCode",
|
|
9213
9256
|
type: "text",
|
|
9257
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9214
9258
|
admin: { hidden: true }
|
|
9215
9259
|
},
|
|
9216
9260
|
{
|
|
9217
9261
|
name: "twoFactorExpiry",
|
|
9218
9262
|
type: "date",
|
|
9263
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9219
9264
|
admin: { hidden: true }
|
|
9220
9265
|
},
|
|
9221
9266
|
{
|
|
9222
9267
|
// Set by /support/2fa "verify"; gates login via the beforeLogin hook.
|
|
9223
9268
|
name: "twoFactorVerifiedAt",
|
|
9224
9269
|
type: "date",
|
|
9270
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9225
9271
|
admin: { hidden: true }
|
|
9226
9272
|
},
|
|
9227
9273
|
{
|
|
@@ -9229,6 +9275,7 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9229
9275
|
name: "googleId",
|
|
9230
9276
|
type: "text",
|
|
9231
9277
|
index: true,
|
|
9278
|
+
access: { update: adminOnlyFieldUpdate },
|
|
9232
9279
|
admin: { hidden: true }
|
|
9233
9280
|
},
|
|
9234
9281
|
{
|
|
@@ -9299,7 +9346,8 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9299
9346
|
label: "Notes internes",
|
|
9300
9347
|
access: {
|
|
9301
9348
|
// Admin-only: never serialize internal notes onto the client's own doc.
|
|
9302
|
-
read: ({ req }) => req.user?.collection === slugs.users
|
|
9349
|
+
read: ({ req }) => req.user?.collection === slugs.users,
|
|
9350
|
+
update: adminOnlyFieldUpdate
|
|
9303
9351
|
},
|
|
9304
9352
|
admin: {
|
|
9305
9353
|
description: "Visible uniquement par les admins",
|
|
@@ -9372,36 +9420,49 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9372
9420
|
}
|
|
9373
9421
|
|
|
9374
9422
|
// src/collections/TimeEntries.ts
|
|
9423
|
+
async function recalculateTicketTime(payload, slugs, ticketId) {
|
|
9424
|
+
let totalMinutes = 0;
|
|
9425
|
+
let page = 1;
|
|
9426
|
+
let hasMore = true;
|
|
9427
|
+
while (hasMore) {
|
|
9428
|
+
const entries = await payload.find({
|
|
9429
|
+
collection: slugs.timeEntries,
|
|
9430
|
+
where: { ticket: { equals: ticketId } },
|
|
9431
|
+
limit: 100,
|
|
9432
|
+
page,
|
|
9433
|
+
depth: 0,
|
|
9434
|
+
overrideAccess: true,
|
|
9435
|
+
select: { duration: true }
|
|
9436
|
+
});
|
|
9437
|
+
for (const entry of entries.docs) {
|
|
9438
|
+
totalMinutes += entry.duration || 0;
|
|
9439
|
+
}
|
|
9440
|
+
hasMore = entries.hasNextPage ?? false;
|
|
9441
|
+
page++;
|
|
9442
|
+
}
|
|
9443
|
+
await payload.update({
|
|
9444
|
+
collection: slugs.tickets,
|
|
9445
|
+
id: ticketId,
|
|
9446
|
+
data: { totalTimeMinutes: totalMinutes },
|
|
9447
|
+
overrideAccess: true
|
|
9448
|
+
});
|
|
9449
|
+
}
|
|
9375
9450
|
function createRecalculateTicketTime(slugs) {
|
|
9376
9451
|
return async ({ doc, req }) => {
|
|
9377
9452
|
if (!doc.ticket) return;
|
|
9378
|
-
const { payload } = req;
|
|
9379
9453
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
select: { duration: true }
|
|
9392
|
-
});
|
|
9393
|
-
for (const entry of entries.docs) {
|
|
9394
|
-
totalMinutes += entry.duration || 0;
|
|
9395
|
-
}
|
|
9396
|
-
hasMore = entries.hasNextPage ?? false;
|
|
9397
|
-
page++;
|
|
9454
|
+
await recalculateTicketTime(req.payload, slugs, ticketId);
|
|
9455
|
+
};
|
|
9456
|
+
}
|
|
9457
|
+
function createRecalculateTicketTimeOnDelete(slugs) {
|
|
9458
|
+
return async ({ doc, req }) => {
|
|
9459
|
+
if (!doc?.ticket) return;
|
|
9460
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
9461
|
+
try {
|
|
9462
|
+
await recalculateTicketTime(req.payload, slugs, ticketId);
|
|
9463
|
+
} catch (err) {
|
|
9464
|
+
console.error("[support] Failed to recalculate ticket time after delete:", err);
|
|
9398
9465
|
}
|
|
9399
|
-
await payload.update({
|
|
9400
|
-
collection: slugs.tickets,
|
|
9401
|
-
id: ticketId,
|
|
9402
|
-
data: { totalTimeMinutes: totalMinutes },
|
|
9403
|
-
overrideAccess: true
|
|
9404
|
-
});
|
|
9405
9466
|
};
|
|
9406
9467
|
}
|
|
9407
9468
|
function createTimeEntriesCollection(slugs) {
|
|
@@ -9463,7 +9524,8 @@ function createTimeEntriesCollection(slugs) {
|
|
|
9463
9524
|
}
|
|
9464
9525
|
],
|
|
9465
9526
|
hooks: {
|
|
9466
|
-
afterChange: [createRecalculateTicketTime(slugs)]
|
|
9527
|
+
afterChange: [createRecalculateTicketTime(slugs)],
|
|
9528
|
+
afterDelete: [createRecalculateTicketTimeOnDelete(slugs)]
|
|
9467
9529
|
},
|
|
9468
9530
|
access: {
|
|
9469
9531
|
create: ({ req }) => req.user?.collection === slugs.users,
|
|
@@ -10889,7 +10951,12 @@ function createTicketCollaboratorsCollection(slugs) {
|
|
|
10889
10951
|
}
|
|
10890
10952
|
return false;
|
|
10891
10953
|
},
|
|
10892
|
-
|
|
10954
|
+
// Staff only. This collection is the source of truth for
|
|
10955
|
+
// resolveAccessibleTicketIds(), which widens `read` on tickets, messages
|
|
10956
|
+
// and the activity log — so letting any authenticated principal POST here
|
|
10957
|
+
// let a support client grant themselves access to any ticket by id.
|
|
10958
|
+
// The /invite endpoint is unaffected: it writes with overrideAccess: true.
|
|
10959
|
+
create: ({ req }) => req.user?.collection === slugs.users,
|
|
10893
10960
|
update: () => false,
|
|
10894
10961
|
delete: ({ req }) => req.user?.collection === slugs.users
|
|
10895
10962
|
},
|
|
@@ -11234,6 +11301,7 @@ exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
|
|
|
11234
11301
|
exports.DEFAULT_INBOUND_EMAIL_LIMITS = DEFAULT_INBOUND_EMAIL_LIMITS;
|
|
11235
11302
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
11236
11303
|
exports.DEFAULT_SLUGS = DEFAULT_SLUGS;
|
|
11304
|
+
exports.DEFAULT_TICKETING_FEATURES = DEFAULT_TICKETING_FEATURES;
|
|
11237
11305
|
exports.DEFAULT_USER_PREFS = DEFAULT_USER_PREFS;
|
|
11238
11306
|
exports.MemoryRateLimitStore = MemoryRateLimitStore;
|
|
11239
11307
|
exports.PayloadRateLimitStore = PayloadRateLimitStore;
|
|
@@ -11266,9 +11334,13 @@ exports.createTrackOpenEndpoint = createTrackOpenEndpoint;
|
|
|
11266
11334
|
exports.createWebhookEndpointsCollection = createWebhookEndpointsCollection;
|
|
11267
11335
|
exports.dispatchWebhook = dispatchWebhook;
|
|
11268
11336
|
exports.generateTicketSynthesis = generateTicketSynthesis;
|
|
11337
|
+
exports.normalizeFeatures = normalizeFeatures;
|
|
11338
|
+
exports.projectAutoClose = projectAutoClose;
|
|
11269
11339
|
exports.readSupportSettings = readSupportSettings;
|
|
11340
|
+
exports.readSupportSettingsState = readSupportSettingsState;
|
|
11270
11341
|
exports.readUserPrefs = readUserPrefs;
|
|
11271
11342
|
exports.resolveSlugs = resolveSlugs;
|
|
11343
|
+
exports.stripProjectedFeatures = stripProjectedFeatures;
|
|
11272
11344
|
exports.supportPlugin = supportPlugin;
|
|
11273
11345
|
exports.validateInboundEmailPayload = validateInboundEmailPayload;
|
|
11274
11346
|
exports.verifySecret = verifySecret;
|