@consilioweb/payload-support 0.16.0 → 1.1.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 +357 -435
- package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
- package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
- package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
- package/dist/components/TicketConversation/SkeletonText.js +20 -0
- package/dist/components/TicketConversation/constants.cjs +27 -0
- package/dist/components/TicketConversation/constants.js +26 -1
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
- package/dist/components/TicketConversation/index.cjs +10 -147
- package/dist/components/TicketConversation/index.js +4 -141
- package/dist/index.cjs +1763 -677
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1760 -677
- package/dist/views/BillingView/client.cjs +40 -54
- package/dist/views/BillingView/client.js +40 -54
- package/dist/views/ChatView/client.cjs +4 -3
- package/dist/views/ChatView/client.js +4 -3
- package/dist/views/CrmView/client.cjs +7 -7
- package/dist/views/CrmView/client.js +7 -7
- package/dist/views/EmailTrackingView/client.cjs +6 -5
- package/dist/views/EmailTrackingView/client.js +6 -5
- package/dist/views/ImportConversationView/client.cjs +4 -4
- package/dist/views/ImportConversationView/client.js +4 -4
- package/dist/views/LogsView/client.cjs +3 -2
- package/dist/views/LogsView/client.js +3 -2
- package/dist/views/NewTicketView/client.cjs +1 -1
- package/dist/views/NewTicketView/client.js +1 -1
- package/dist/views/PendingEmailsView/client.cjs +7 -6
- package/dist/views/PendingEmailsView/client.js +7 -6
- package/dist/views/SupportDashboardView/client.cjs +19 -22
- package/dist/views/SupportDashboardView/client.js +19 -22
- package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
- package/dist/views/TicketDetailView/NextActionItem.js +34 -0
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
- package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
- package/dist/views/TicketDetailView/client.cjs +59 -190
- package/dist/views/TicketDetailView/client.js +52 -183
- package/dist/views/TicketDetailView/constants.cjs +20 -0
- package/dist/views/TicketDetailView/constants.js +16 -0
- package/dist/views/TicketDetailView/helpers.cjs +23 -0
- package/dist/views/TicketDetailView/helpers.js +20 -0
- package/dist/views/TicketDetailView/types.cjs +2 -0
- package/dist/views/TicketDetailView/types.js +1 -0
- package/dist/views/TicketInboxView/client.cjs +14 -10
- package/dist/views/TicketInboxView/client.js +14 -10
- package/dist/views/TicketingSettingsView/client.cjs +101 -96
- package/dist/views/TicketingSettingsView/client.js +92 -88
- package/dist/views/TimeDashboardView/client.cjs +2 -2
- package/dist/views/TimeDashboardView/client.js +2 -2
- package/dist/views/shared/StatusPill.cjs +23 -12
- package/dist/views/shared/StatusPill.js +23 -12
- package/dist/views/shared/dateLocale.cjs +5 -0
- package/dist/views/shared/dateLocale.js +3 -0
- package/package.json +28 -15
- package/src/__tests__/generateTrackingToken.test.ts +53 -0
- package/src/__tests__/integration/ai-agent.test.ts +46 -0
- package/src/__tests__/integration/auth.test.ts +43 -0
- package/src/__tests__/integration/automation-rules.test.ts +68 -0
- package/src/__tests__/integration/buildTestPayload.ts +52 -0
- package/src/__tests__/integration/channels.test.ts +52 -0
- package/src/__tests__/integration/core-behaviors.test.ts +58 -0
- package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
- package/src/__tests__/integration/digest.test.ts +48 -0
- package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
- package/src/__tests__/integration/isolation.test.ts +81 -0
- package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
- package/src/__tests__/integration/nps.test.ts +37 -0
- package/src/__tests__/integration/per-team-sla.test.ts +44 -0
- package/src/__tests__/integration/push.test.ts +75 -0
- package/src/__tests__/integration/sanitization.test.ts +35 -0
- package/src/__tests__/integration/sla-pause.test.ts +39 -0
- package/src/__tests__/integration/smoke.test.ts +26 -0
- package/src/__tests__/integration/snooze.test.ts +68 -0
- package/src/__tests__/integration/teams.test.ts +38 -0
- package/src/__tests__/rateLimiter.test.ts +89 -0
- package/src/__tests__/sanitizeHtml.test.ts +165 -0
- package/src/__tests__/sla.test.ts +258 -0
- package/src/collections/AutomationRules.ts +66 -0
- package/src/collections/NotificationQueue.ts +30 -0
- package/src/collections/PushSubscription.ts +28 -0
- package/src/collections/SatisfactionSurveys.ts +11 -2
- package/src/collections/SlaPolicies.ts +10 -0
- package/src/collections/SupportClients.ts +58 -1
- package/src/collections/SupportTeam.ts +27 -0
- package/src/collections/TicketActivityLog.ts +9 -4
- package/src/collections/TicketMessages.ts +140 -31
- package/src/collections/Tickets.ts +31 -73
- package/src/collections/index.ts +4 -0
- package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
- package/src/components/TicketConversation/SkeletonText.tsx +23 -0
- package/src/components/TicketConversation/constants.ts +29 -0
- package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
- package/src/components/TicketConversation/index.tsx +4 -152
- package/src/endpoints/admin-chat-stream.ts +3 -4
- package/src/endpoints/admin-chat.ts +11 -20
- package/src/endpoints/admin-stats.ts +77 -35
- package/src/endpoints/ai-agent.ts +36 -0
- package/src/endpoints/apply-macro.ts +8 -14
- package/src/endpoints/auth-2fa.ts +12 -12
- package/src/endpoints/auto-close.ts +6 -10
- package/src/endpoints/billing.ts +17 -4
- package/src/endpoints/bulk-action.ts +7 -12
- package/src/endpoints/channels.ts +89 -0
- package/src/endpoints/chat-stream.ts +3 -4
- package/src/endpoints/chat.ts +7 -12
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/client-intelligence.ts +4 -4
- package/src/endpoints/delete-account.ts +9 -16
- package/src/endpoints/email-stats.ts +2 -2
- package/src/endpoints/escalate.ts +2 -2
- package/src/endpoints/export-csv.ts +2 -2
- package/src/endpoints/export-data.ts +5 -8
- package/src/endpoints/import-conversation.ts +6 -10
- package/src/endpoints/index.ts +22 -1
- package/src/endpoints/invite-collaborator.ts +30 -15
- package/src/endpoints/invoice.ts +127 -0
- package/src/endpoints/kb-search.ts +2 -2
- package/src/endpoints/login.ts +12 -4
- package/src/endpoints/merge-clients.ts +14 -19
- package/src/endpoints/merge-tickets.ts +11 -18
- package/src/endpoints/oauth-google.ts +86 -39
- package/src/endpoints/pending-emails-process.ts +15 -25
- package/src/endpoints/process-digests.ts +93 -0
- package/src/endpoints/process-scheduled.ts +6 -10
- package/src/endpoints/process-snooze.ts +78 -0
- package/src/endpoints/purge-logs.ts +2 -2
- package/src/endpoints/push.ts +49 -0
- package/src/endpoints/resend-notification.ts +3 -4
- package/src/endpoints/round-robin-config.ts +3 -4
- package/src/endpoints/satisfaction.ts +21 -14
- package/src/endpoints/search.ts +5 -8
- package/src/endpoints/seed-kb.ts +3 -4
- package/src/endpoints/send-reminder.ts +6 -9
- package/src/endpoints/settings.ts +5 -2
- package/src/endpoints/signature.ts +3 -4
- package/src/endpoints/sla-check.ts +2 -2
- package/src/endpoints/split-ticket.ts +7 -12
- package/src/endpoints/statuses.ts +2 -2
- package/src/endpoints/ticket-feedback.ts +4 -6
- package/src/endpoints/ticket-synthesis.ts +2 -2
- package/src/endpoints/track-open.ts +16 -28
- package/src/endpoints/transfer-ticket.ts +21 -9
- package/src/endpoints/user-prefs.ts +3 -4
- package/src/hooks/automationRules.ts +84 -0
- package/src/hooks/checkSLA.ts +63 -17
- package/src/plugin.ts +8 -0
- package/src/portal/LiveChat.tsx +2 -63
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
- package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
- package/src/portal/auth/tickets/detail/page.tsx +2 -2
- package/src/portal/helpers.ts +46 -0
- package/src/portal/login/page.tsx +3 -8
- package/src/portal/types.ts +15 -0
- package/src/types/lucide-react.d.ts +34 -0
- package/src/types/payload-config.d.ts +10 -0
- package/src/utils/adminNotification.ts +2 -2
- package/src/utils/aiAgent.ts +147 -0
- package/src/utils/auth.ts +5 -4
- package/src/utils/channels.ts +36 -0
- package/src/utils/db.ts +73 -0
- package/src/utils/emailTemplate.ts +8 -1
- package/src/utils/fireWebhooks.ts +4 -6
- package/src/utils/generateTicketSynthesis.ts +7 -12
- package/src/utils/invoicePdf.ts +88 -0
- package/src/utils/notificationQueue.ts +36 -0
- package/src/utils/push.ts +63 -0
- package/src/utils/readSettings.ts +22 -6
- package/src/utils/sanitizeHtml.ts +57 -0
- package/src/utils/slugs.ts +8 -0
- package/src/utils/teamAccess.ts +27 -0
- package/src/utils/ticketAccess.ts +53 -0
- package/src/utils/webhookDispatcher.ts +4 -6
- package/src/views/BillingView/client.tsx +49 -37
- package/src/views/ChatView/client.tsx +4 -3
- package/src/views/CrmView/client.tsx +7 -7
- package/src/views/EmailTrackingView/client.tsx +7 -6
- package/src/views/ImportConversationView/client.tsx +4 -4
- package/src/views/LogsView/client.tsx +3 -2
- package/src/views/NewTicketView/client.tsx +1 -1
- package/src/views/PendingEmailsView/client.tsx +7 -6
- package/src/views/SupportDashboardView/client.tsx +21 -21
- package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
- package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
- package/src/views/TicketDetailView/client.tsx +54 -179
- package/src/views/TicketDetailView/constants.ts +23 -0
- package/src/views/TicketDetailView/helpers.ts +17 -0
- package/src/views/TicketDetailView/types.ts +10 -0
- package/src/views/TicketInboxView/client.tsx +16 -10
- package/src/views/TicketingSettingsView/client.tsx +90 -87
- package/src/views/TimeDashboardView/client.tsx +2 -2
- package/src/views/shared/StatusPill.tsx +24 -12
- package/src/views/shared/dateLocale.ts +5 -0
- package/src/views/shared/locales/en.json +708 -0
- package/src/views/shared/locales/fr.json +708 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFind, dbCount } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* GET /api/support/admin-stats
|
|
@@ -17,13 +18,16 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
17
18
|
|
|
18
19
|
requireAdmin(req, slugs)
|
|
19
20
|
|
|
21
|
+
// Optional per-team dashboard scoping (?teamId=) — applied to ticket counts.
|
|
22
|
+
const teamId = new URL(req.url!).searchParams.get('teamId')
|
|
23
|
+
const teamAnd = teamId ? [{ team: { equals: Number(teamId) } }] : []
|
|
24
|
+
|
|
20
25
|
// ── Status counts via individual count queries ──
|
|
21
26
|
const statuses = ['open', 'waiting_client', 'resolved'] as const
|
|
22
27
|
const statusCounts = await Promise.all(
|
|
23
28
|
statuses.map(async (status) => {
|
|
24
|
-
const result = await payload.
|
|
25
|
-
|
|
26
|
-
where: { status: { equals: status } },
|
|
29
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
30
|
+
where: { and: [{ status: { equals: status } }, ...teamAnd] },
|
|
27
31
|
overrideAccess: true,
|
|
28
32
|
})
|
|
29
33
|
return [status, result.totalDocs] as const
|
|
@@ -36,9 +40,8 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
36
40
|
const priorities = ['low', 'normal', 'high', 'urgent'] as const
|
|
37
41
|
const priorityCounts = await Promise.all(
|
|
38
42
|
priorities.map(async (priority) => {
|
|
39
|
-
const result = await payload.
|
|
40
|
-
|
|
41
|
-
where: { priority: { equals: priority } },
|
|
43
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
44
|
+
where: { and: [{ priority: { equals: priority } }, ...teamAnd] },
|
|
42
45
|
overrideAccess: true,
|
|
43
46
|
})
|
|
44
47
|
return [priority, result.totalDocs] as const
|
|
@@ -50,9 +53,8 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
50
53
|
const categories = ['bug', 'content', 'feature', 'question', 'hosting'] as const
|
|
51
54
|
const categoryCounts = await Promise.all(
|
|
52
55
|
categories.map(async (category) => {
|
|
53
|
-
const result = await payload.
|
|
54
|
-
|
|
55
|
-
where: { category: { equals: category } },
|
|
56
|
+
const result = await dbCount(payload, slugs.tickets, {
|
|
57
|
+
where: { and: [{ category: { equals: category } }, ...teamAnd] },
|
|
56
58
|
overrideAccess: true,
|
|
57
59
|
})
|
|
58
60
|
return [category, result.totalDocs] as const
|
|
@@ -68,18 +70,38 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
68
70
|
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
69
71
|
|
|
70
72
|
const [created7, created30] = await Promise.all([
|
|
71
|
-
payload.
|
|
72
|
-
collection: slugs.tickets as any,
|
|
73
|
+
dbCount(payload, slugs.tickets, {
|
|
73
74
|
where: { createdAt: { greater_than_equal: sevenDaysAgo.toISOString() } },
|
|
74
75
|
overrideAccess: true,
|
|
75
76
|
}),
|
|
76
|
-
payload.
|
|
77
|
-
collection: slugs.tickets as any,
|
|
77
|
+
dbCount(payload, slugs.tickets, {
|
|
78
78
|
where: { createdAt: { greater_than_equal: thirtyDaysAgo.toISOString() } },
|
|
79
79
|
overrideAccess: true,
|
|
80
80
|
}),
|
|
81
81
|
])
|
|
82
82
|
|
|
83
|
+
// Real per-day ticket volume for the last 7 days (chronological), replacing
|
|
84
|
+
// the previous synthetic distribution in the dashboard chart.
|
|
85
|
+
const volumeByDay = await Promise.all(
|
|
86
|
+
Array.from({ length: 7 }, (_, i) => i).map(async (offset) => {
|
|
87
|
+
const dayStart = new Date(now)
|
|
88
|
+
dayStart.setHours(0, 0, 0, 0)
|
|
89
|
+
dayStart.setDate(dayStart.getDate() - (6 - offset))
|
|
90
|
+
const dayEnd = new Date(dayStart)
|
|
91
|
+
dayEnd.setDate(dayEnd.getDate() + 1)
|
|
92
|
+
const r = await dbCount(payload, slugs.tickets, {
|
|
93
|
+
where: {
|
|
94
|
+
and: [
|
|
95
|
+
{ createdAt: { greater_than_equal: dayStart.toISOString() } },
|
|
96
|
+
{ createdAt: { less_than: dayEnd.toISOString() } },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
overrideAccess: true,
|
|
100
|
+
})
|
|
101
|
+
return { date: dayStart.toISOString(), count: r.totalDocs }
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
|
|
83
105
|
// ── Averages: paginate with limit:100, only fetch needed fields ──
|
|
84
106
|
const PAGE_SIZE = 100
|
|
85
107
|
const MAX_PAGES = 50
|
|
@@ -93,8 +115,7 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
93
115
|
let hasMore = true
|
|
94
116
|
|
|
95
117
|
while (hasMore && page <= MAX_PAGES) {
|
|
96
|
-
const batch = await payload.
|
|
97
|
-
collection: slugs.tickets as any,
|
|
118
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
98
119
|
limit: PAGE_SIZE,
|
|
99
120
|
page,
|
|
100
121
|
depth: 0,
|
|
@@ -124,46 +145,64 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
124
145
|
page++
|
|
125
146
|
}
|
|
126
147
|
|
|
127
|
-
// ── Satisfaction:
|
|
128
|
-
const surveyCount = await payload.
|
|
129
|
-
collection: slugs.satisfactionSurveys as any,
|
|
148
|
+
// ── Satisfaction (CSAT) + NPS: count + paginated aggregation ──
|
|
149
|
+
const surveyCount = await dbCount(payload, slugs.satisfactionSurveys, {
|
|
130
150
|
overrideAccess: true,
|
|
131
151
|
})
|
|
132
152
|
|
|
133
153
|
let satisfactionAvg = 0
|
|
154
|
+
let satisfactionCount = 0
|
|
155
|
+
let npsScore: number | null = null
|
|
156
|
+
let npsCount = 0
|
|
134
157
|
if (surveyCount.totalDocs > 0) {
|
|
135
|
-
let
|
|
158
|
+
let ratingSum = 0
|
|
159
|
+
let ratingN = 0
|
|
160
|
+
let promoters = 0
|
|
161
|
+
let detractors = 0
|
|
162
|
+
let npsN = 0
|
|
136
163
|
let surveyPage = 1
|
|
137
164
|
let surveyHasMore = true
|
|
138
165
|
while (surveyHasMore && surveyPage <= MAX_PAGES) {
|
|
139
|
-
const batch = await payload.
|
|
140
|
-
collection: slugs.satisfactionSurveys as any,
|
|
166
|
+
const batch = await dbFind(payload, slugs.satisfactionSurveys, {
|
|
141
167
|
limit: PAGE_SIZE,
|
|
142
168
|
page: surveyPage,
|
|
143
169
|
depth: 0,
|
|
144
170
|
overrideAccess: true,
|
|
145
|
-
select: { rating: true },
|
|
171
|
+
select: { rating: true, nps: true },
|
|
146
172
|
})
|
|
147
173
|
for (const s of batch.docs) {
|
|
148
|
-
|
|
174
|
+
const r = (s as { rating?: number }).rating
|
|
175
|
+
if (typeof r === 'number' && r > 0) { ratingSum += r; ratingN++ }
|
|
176
|
+
const n = (s as { nps?: number }).nps
|
|
177
|
+
if (typeof n === 'number') {
|
|
178
|
+
npsN++
|
|
179
|
+
if (n >= 9) promoters++
|
|
180
|
+
else if (n <= 6) detractors++
|
|
181
|
+
}
|
|
149
182
|
}
|
|
150
183
|
surveyHasMore = batch.hasNextPage ?? false
|
|
151
184
|
surveyPage++
|
|
152
185
|
}
|
|
153
|
-
satisfactionAvg = Math.round((
|
|
186
|
+
satisfactionAvg = ratingN > 0 ? Math.round((ratingSum / ratingN) * 10) / 10 : 0
|
|
187
|
+
satisfactionCount = ratingN
|
|
188
|
+
if (npsN > 0) {
|
|
189
|
+
// NPS = %promoters - %detractors, on a -100..100 scale.
|
|
190
|
+
npsScore = Math.round(((promoters - detractors) / npsN) * 100)
|
|
191
|
+
npsCount = npsN
|
|
192
|
+
}
|
|
154
193
|
}
|
|
155
194
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
payload.
|
|
162
|
-
collection: slugs.pendingEmails as any,
|
|
195
|
+
const clientCount = await dbCount(payload, slugs.supportClients, { overrideAccess: true })
|
|
196
|
+
// pendingEmails is an optional feature — tolerate its collection being absent
|
|
197
|
+
// (otherwise the whole stats endpoint 500s on installs without it).
|
|
198
|
+
let pendingEmailsTotal = 0
|
|
199
|
+
try {
|
|
200
|
+
const pe = await dbCount(payload, slugs.pendingEmails, {
|
|
163
201
|
where: { status: { equals: 'pending' } },
|
|
164
202
|
overrideAccess: true,
|
|
165
|
-
})
|
|
166
|
-
|
|
203
|
+
})
|
|
204
|
+
pendingEmailsTotal = pe.totalDocs
|
|
205
|
+
} catch { /* pendingEmails feature disabled — count stays 0 */ }
|
|
167
206
|
|
|
168
207
|
return new Response(JSON.stringify({
|
|
169
208
|
total,
|
|
@@ -172,6 +211,7 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
172
211
|
byCategory,
|
|
173
212
|
createdLast7Days: created7.totalDocs,
|
|
174
213
|
createdLast30Days: created30.totalDocs,
|
|
214
|
+
volumeByDay,
|
|
175
215
|
avgResponseTimeHours: responseTimeCount > 0
|
|
176
216
|
? Math.round((totalResponseTimeMs / responseTimeCount / (1000 * 60 * 60)) * 10) / 10
|
|
177
217
|
: null,
|
|
@@ -180,9 +220,11 @@ export function createAdminStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
180
220
|
: null,
|
|
181
221
|
totalTimeMinutes,
|
|
182
222
|
satisfactionAvg,
|
|
183
|
-
satisfactionCount
|
|
223
|
+
satisfactionCount,
|
|
224
|
+
npsScore,
|
|
225
|
+
npsCount,
|
|
184
226
|
clientCount: clientCount.totalDocs,
|
|
185
|
-
pendingEmailsCount:
|
|
227
|
+
pendingEmailsCount: pendingEmailsTotal,
|
|
186
228
|
}), {
|
|
187
229
|
headers: {
|
|
188
230
|
'Content-Type': 'application/json',
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload'
|
|
2
|
+
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { runAiAgent } from '../utils/aiAgent'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* POST /api/support/ai-agent body: { ticketId, confidenceThreshold? }
|
|
8
|
+
*
|
|
9
|
+
* Runs the autonomous AI agent on a ticket: it either posts a KB-grounded answer
|
|
10
|
+
* (when confident) or escalates to a human with an internal note. Admin-only.
|
|
11
|
+
*/
|
|
12
|
+
export function createAiAgentEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
13
|
+
return {
|
|
14
|
+
path: '/support/ai-agent',
|
|
15
|
+
method: 'post',
|
|
16
|
+
handler: async (req) => {
|
|
17
|
+
try {
|
|
18
|
+
requireAdmin(req, slugs)
|
|
19
|
+
let body: { ticketId?: number | string; confidenceThreshold?: number } = {}
|
|
20
|
+
try { body = (await req.json!()) as typeof body } catch { /* no body */ }
|
|
21
|
+
if (!body.ticketId) {
|
|
22
|
+
return Response.json({ error: 'ticketId requis.' }, { status: 400 })
|
|
23
|
+
}
|
|
24
|
+
const result = await runAiAgent(req.payload, slugs, body.ticketId, {
|
|
25
|
+
confidenceThreshold: body.confidenceThreshold,
|
|
26
|
+
})
|
|
27
|
+
return Response.json({ ok: true, ...result })
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const authResponse = handleAuthError(error)
|
|
30
|
+
if (authResponse) return authResponse
|
|
31
|
+
console.error('[ai-agent] Error:', error)
|
|
32
|
+
return Response.json({ error: 'Erreur interne' }, { status: 500 })
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFindByID, dbUpdate, dbCreate } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
interface MacroAction {
|
|
6
7
|
type: 'set_status' | 'set_priority' | 'add_tag' | 'send_reply' | 'assign'
|
|
@@ -28,8 +29,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
28
29
|
return Response.json({ error: 'macroId and ticketId are required' }, { status: 400 })
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const macro = await payload.
|
|
32
|
-
collection: slugs.macros as any,
|
|
32
|
+
const macro = await dbFindByID(payload, slugs.macros, {
|
|
33
33
|
id: macroId,
|
|
34
34
|
depth: 0,
|
|
35
35
|
overrideAccess: true,
|
|
@@ -38,8 +38,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
38
38
|
if (!macro) return Response.json({ error: 'Macro not found' }, { status: 404 })
|
|
39
39
|
if (!macro.isActive) return Response.json({ error: 'Macro is disabled' }, { status: 400 })
|
|
40
40
|
|
|
41
|
-
const ticket = await payload.
|
|
42
|
-
collection: slugs.tickets as any,
|
|
41
|
+
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
43
42
|
id: ticketId,
|
|
44
43
|
depth: 0,
|
|
45
44
|
overrideAccess: true,
|
|
@@ -54,8 +53,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
54
53
|
try {
|
|
55
54
|
switch (action.type) {
|
|
56
55
|
case 'set_status':
|
|
57
|
-
await payload.
|
|
58
|
-
collection: slugs.tickets as any,
|
|
56
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
59
57
|
id: ticketId,
|
|
60
58
|
data: { status: action.value },
|
|
61
59
|
overrideAccess: true,
|
|
@@ -64,8 +62,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
64
62
|
break
|
|
65
63
|
|
|
66
64
|
case 'set_priority':
|
|
67
|
-
await payload.
|
|
68
|
-
collection: slugs.tickets as any,
|
|
65
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
69
66
|
id: ticketId,
|
|
70
67
|
data: { priority: action.value },
|
|
71
68
|
overrideAccess: true,
|
|
@@ -78,8 +75,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
78
75
|
if (!currentTags.includes(action.value)) {
|
|
79
76
|
currentTags.push(action.value)
|
|
80
77
|
}
|
|
81
|
-
await payload.
|
|
82
|
-
collection: slugs.tickets as any,
|
|
78
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
83
79
|
id: ticketId,
|
|
84
80
|
data: { tags: currentTags },
|
|
85
81
|
overrideAccess: true,
|
|
@@ -89,8 +85,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
89
85
|
}
|
|
90
86
|
|
|
91
87
|
case 'send_reply':
|
|
92
|
-
await payload.
|
|
93
|
-
collection: slugs.ticketMessages as any,
|
|
88
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
94
89
|
data: {
|
|
95
90
|
ticket: ticketId,
|
|
96
91
|
body: action.value,
|
|
@@ -108,8 +103,7 @@ export function createApplyMacroEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
108
103
|
appliedActions.push({ type: action.type, value: action.value, success: false, error: 'Invalid user ID' })
|
|
109
104
|
break
|
|
110
105
|
}
|
|
111
|
-
await payload.
|
|
112
|
-
collection: slugs.tickets as any,
|
|
106
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
113
107
|
id: ticketId,
|
|
114
108
|
data: { assignedTo: userId },
|
|
115
109
|
overrideAccess: true,
|
|
@@ -3,6 +3,7 @@ import type { CollectionSlugs } from '../utils/slugs'
|
|
|
3
3
|
import crypto, { createHmac } from 'crypto'
|
|
4
4
|
import { RateLimiter } from '../utils/rateLimiter'
|
|
5
5
|
import { escapeHtml } from '../utils/emailTemplate'
|
|
6
|
+
import { dbFind, dbUpdate } from '../utils/db'
|
|
6
7
|
|
|
7
8
|
const sendLimiter = new RateLimiter(60 * 60 * 1000, 3) // 3 per hour
|
|
8
9
|
const verifyLimiter = new RateLimiter(15 * 60 * 1000, 5) // 5 per 15 min
|
|
@@ -14,7 +15,11 @@ function generateSecureCode(): string {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
function hashCode(code: string): string {
|
|
17
|
-
const secret = process.env.PAYLOAD_SECRET
|
|
18
|
+
const secret = process.env.PAYLOAD_SECRET
|
|
19
|
+
if (!secret) {
|
|
20
|
+
// Fail closed: never hash 2FA codes with an insecure, source-visible fallback.
|
|
21
|
+
throw new Error('[support][2fa] PAYLOAD_SECRET is not set — refusing to operate 2FA with an insecure fallback secret')
|
|
22
|
+
}
|
|
18
23
|
return createHmac('sha256', secret).update(code).digest('hex')
|
|
19
24
|
}
|
|
20
25
|
|
|
@@ -48,8 +53,7 @@ export function createAuth2faEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
48
53
|
return Response.json(genericSendResponse)
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
const clients = await payload.
|
|
52
|
-
collection: slugs.supportClients as any,
|
|
56
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
53
57
|
where: { email: { equals: email } },
|
|
54
58
|
limit: 1,
|
|
55
59
|
depth: 0,
|
|
@@ -65,8 +69,7 @@ export function createAuth2faEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
65
69
|
const twoFactorCode = hashCode(plainCode)
|
|
66
70
|
const twoFactorExpiry = new Date(Date.now() + 10 * 60 * 1000).toISOString()
|
|
67
71
|
|
|
68
|
-
await payload.
|
|
69
|
-
collection: slugs.supportClients as any,
|
|
72
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
70
73
|
id: client.id,
|
|
71
74
|
data: { twoFactorCode, twoFactorExpiry },
|
|
72
75
|
overrideAccess: true,
|
|
@@ -102,8 +105,7 @@ export function createAuth2faEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
102
105
|
)
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
const clients = await payload.
|
|
106
|
-
collection: slugs.supportClients as any,
|
|
108
|
+
const clients = await dbFind(payload, slugs.supportClients, {
|
|
107
109
|
where: { email: { equals: email } },
|
|
108
110
|
limit: 1,
|
|
109
111
|
depth: 0,
|
|
@@ -123,8 +125,7 @@ export function createAuth2faEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
if (new Date() > new Date(storedExpiry)) {
|
|
126
|
-
await payload.
|
|
127
|
-
collection: slugs.supportClients as any,
|
|
128
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
128
129
|
id: client.id,
|
|
129
130
|
data: { twoFactorCode: '', twoFactorExpiry: '' },
|
|
130
131
|
overrideAccess: true,
|
|
@@ -141,10 +142,9 @@ export function createAuth2faEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
141
142
|
return Response.json({ error: 'Code incorrect' }, { status: 400 })
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
await payload.
|
|
145
|
-
collection: slugs.supportClients as any,
|
|
145
|
+
await dbUpdate(payload, slugs.supportClients, {
|
|
146
146
|
id: client.id,
|
|
147
|
-
data: { twoFactorCode: '', twoFactorExpiry: '' },
|
|
147
|
+
data: { twoFactorCode: '', twoFactorExpiry: '', twoFactorVerifiedAt: new Date().toISOString() },
|
|
148
148
|
overrideAccess: true,
|
|
149
149
|
})
|
|
150
150
|
|
|
@@ -2,6 +2,7 @@ import type { Endpoint } from 'payload'
|
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { escapeHtml } from '../utils/emailTemplate'
|
|
4
4
|
import { readSupportSettings } from '../utils/readSettings'
|
|
5
|
+
import { dbFind, dbCreate, dbUpdate } from '../utils/db'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* GET /api/support/auto-close
|
|
@@ -40,8 +41,7 @@ export function createAutoCloseEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
40
41
|
// Step 1: Find tickets needing a reminder
|
|
41
42
|
const remindCutoff = new Date(now.getTime() - REMIND_AFTER_DAYS * 24 * 60 * 60 * 1000)
|
|
42
43
|
|
|
43
|
-
const ticketsToRemind = await payload.
|
|
44
|
-
collection: slugs.tickets as any,
|
|
44
|
+
const ticketsToRemind = await dbFind(payload, slugs.tickets, {
|
|
45
45
|
where: {
|
|
46
46
|
and: [
|
|
47
47
|
{ status: { equals: 'waiting_client' } },
|
|
@@ -76,8 +76,7 @@ export function createAutoCloseEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
76
76
|
</div>`,
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
await payload.
|
|
80
|
-
collection: slugs.tickets as any,
|
|
79
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
81
80
|
id: t.id,
|
|
82
81
|
data: { autoCloseRemindedAt: now.toISOString() },
|
|
83
82
|
overrideAccess: true,
|
|
@@ -99,8 +98,7 @@ export function createAutoCloseEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
99
98
|
// the past means no reply was received.
|
|
100
99
|
const closeCutoff = new Date(now.getTime() - CLOSE_AFTER_REMIND_DAYS * 24 * 60 * 60 * 1000)
|
|
101
100
|
|
|
102
|
-
const ticketsToClose = await payload.
|
|
103
|
-
collection: slugs.tickets as any,
|
|
101
|
+
const ticketsToClose = await dbFind(payload, slugs.tickets, {
|
|
104
102
|
where: {
|
|
105
103
|
and: [
|
|
106
104
|
{ status: { equals: 'waiting_client' } },
|
|
@@ -142,8 +140,7 @@ export function createAutoCloseEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
142
140
|
? 'Ticket résolu automatiquement — relance manuelle restée sans réponse du client'
|
|
143
141
|
: `Ticket résolu automatiquement — sans réponse client depuis ${closeTotalDays} jours`
|
|
144
142
|
|
|
145
|
-
await payload.
|
|
146
|
-
collection: slugs.ticketMessages as any,
|
|
143
|
+
await dbCreate(payload, slugs.ticketMessages, {
|
|
147
144
|
data: {
|
|
148
145
|
ticket: t.id,
|
|
149
146
|
body: noteBody,
|
|
@@ -154,8 +151,7 @@ export function createAutoCloseEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
154
151
|
overrideAccess: true,
|
|
155
152
|
})
|
|
156
153
|
|
|
157
|
-
await payload.
|
|
158
|
-
collection: slugs.tickets as any,
|
|
154
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
159
155
|
id: t.id,
|
|
160
156
|
// Clear the armed deadline so a later manual reopen can't be
|
|
161
157
|
// closed instantly by a stale timestamp.
|
package/src/endpoints/billing.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint, Where } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbFind } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
const PAGE_SIZE = 500
|
|
6
7
|
const MAX_PAGES = 50
|
|
@@ -75,13 +76,26 @@ export function createBillingEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
75
76
|
let ticketHasMore = true
|
|
76
77
|
|
|
77
78
|
while (ticketHasMore && ticketPage <= MAX_PAGES) {
|
|
78
|
-
const batch = await payload.
|
|
79
|
-
collection: slugs.tickets as any,
|
|
79
|
+
const batch = await dbFind(payload, slugs.tickets, {
|
|
80
80
|
where: ticketWhere,
|
|
81
81
|
limit: PAGE_SIZE,
|
|
82
82
|
page: ticketPage,
|
|
83
|
+
// depth 2 is required: we read ticket.project.client.company below.
|
|
83
84
|
depth: 2,
|
|
84
85
|
overrideAccess: true,
|
|
86
|
+
// Bound the payload to only the fields the report consumes (skips body,
|
|
87
|
+
// richText, SLA fields, etc.) while keeping project/client populated.
|
|
88
|
+
select: {
|
|
89
|
+
ticketNumber: true,
|
|
90
|
+
subject: true,
|
|
91
|
+
status: true,
|
|
92
|
+
billedAmount: true,
|
|
93
|
+
aiSummary: true,
|
|
94
|
+
aiSummaryGeneratedAt: true,
|
|
95
|
+
aiSummaryStatus: true,
|
|
96
|
+
project: true,
|
|
97
|
+
client: true,
|
|
98
|
+
},
|
|
85
99
|
})
|
|
86
100
|
allTickets.push(...batch.docs as any[])
|
|
87
101
|
ticketHasMore = batch.hasNextPage ?? false
|
|
@@ -94,8 +108,7 @@ export function createBillingEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
94
108
|
let entryHasMore = true
|
|
95
109
|
|
|
96
110
|
while (entryHasMore && entryPage <= MAX_PAGES) {
|
|
97
|
-
const batch = await payload.
|
|
98
|
-
collection: slugs.timeEntries as any,
|
|
111
|
+
const batch = await dbFind(payload, slugs.timeEntries, {
|
|
99
112
|
where: {
|
|
100
113
|
and: [
|
|
101
114
|
{ date: { greater_than_equal: from } },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { dbUpdate, dbDelete } from '../utils/db'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* POST /api/support/bulk-action
|
|
@@ -32,16 +33,14 @@ export function createBulkActionEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
32
33
|
try {
|
|
33
34
|
switch (action) {
|
|
34
35
|
case 'close':
|
|
35
|
-
await payload.
|
|
36
|
-
collection: slugs.tickets as any,
|
|
36
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
37
37
|
id: ticketId,
|
|
38
38
|
data: { status: 'resolved', resolvedAt: new Date().toISOString() },
|
|
39
39
|
overrideAccess: true,
|
|
40
40
|
})
|
|
41
41
|
break
|
|
42
42
|
case 'reopen':
|
|
43
|
-
await payload.
|
|
44
|
-
collection: slugs.tickets as any,
|
|
43
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
45
44
|
id: ticketId,
|
|
46
45
|
data: { status: 'open' },
|
|
47
46
|
overrideAccess: true,
|
|
@@ -49,8 +48,7 @@ export function createBulkActionEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
49
48
|
break
|
|
50
49
|
case 'assign':
|
|
51
50
|
if (value) {
|
|
52
|
-
await payload.
|
|
53
|
-
collection: slugs.tickets as any,
|
|
51
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
54
52
|
id: ticketId,
|
|
55
53
|
data: { assignedTo: Number(value) },
|
|
56
54
|
overrideAccess: true,
|
|
@@ -59,8 +57,7 @@ export function createBulkActionEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
59
57
|
break
|
|
60
58
|
case 'set_priority':
|
|
61
59
|
if (value) {
|
|
62
|
-
await payload.
|
|
63
|
-
collection: slugs.tickets as any,
|
|
60
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
64
61
|
id: ticketId,
|
|
65
62
|
data: { priority: String(value) },
|
|
66
63
|
overrideAccess: true,
|
|
@@ -69,8 +66,7 @@ export function createBulkActionEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
69
66
|
break
|
|
70
67
|
case 'set_category':
|
|
71
68
|
if (value) {
|
|
72
|
-
await payload.
|
|
73
|
-
collection: slugs.tickets as any,
|
|
69
|
+
await dbUpdate(payload, slugs.tickets, {
|
|
74
70
|
id: ticketId,
|
|
75
71
|
data: { category: String(value) },
|
|
76
72
|
overrideAccess: true,
|
|
@@ -78,8 +74,7 @@ export function createBulkActionEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
78
74
|
}
|
|
79
75
|
break
|
|
80
76
|
case 'delete':
|
|
81
|
-
await payload.
|
|
82
|
-
collection: slugs.tickets as any,
|
|
77
|
+
await dbDelete(payload, slugs.tickets, {
|
|
83
78
|
id: ticketId,
|
|
84
79
|
overrideAccess: true,
|
|
85
80
|
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload'
|
|
2
|
+
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
import { dbFind, dbCreate } from '../utils/db'
|
|
4
|
+
import { randomBytes } from 'crypto'
|
|
5
|
+
|
|
6
|
+
const PROVIDERS = ['whatsapp', 'messenger'] as const
|
|
7
|
+
type Provider = (typeof PROVIDERS)[number]
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/support/channels/webhook
|
|
11
|
+
*
|
|
12
|
+
* Inbound social-channel webhook (WhatsApp / Messenger). Accepts a NORMALIZED
|
|
13
|
+
* payload `{ provider, from, name?, text }` — a thin adapter maps the provider's
|
|
14
|
+
* raw webhook (Meta X-Hub-Signature-256 etc.) to this shape at the edge. Creates
|
|
15
|
+
* or appends the client's open ticket for that channel.
|
|
16
|
+
*
|
|
17
|
+
* Protected by the `x-channel-secret` header (`CHANNELS_WEBHOOK_SECRET`).
|
|
18
|
+
*/
|
|
19
|
+
export function createChannelsWebhookEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
20
|
+
return {
|
|
21
|
+
path: '/support/channels/webhook',
|
|
22
|
+
method: 'post',
|
|
23
|
+
handler: async (req) => {
|
|
24
|
+
const secret = req.headers.get('x-channel-secret')
|
|
25
|
+
if (!process.env.CHANNELS_WEBHOOK_SECRET || secret !== process.env.CHANNELS_WEBHOOK_SECRET) {
|
|
26
|
+
return Response.json({ error: 'Non autorisé' }, { status: 401 })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let body: { provider?: string; from?: string; name?: string; text?: string }
|
|
30
|
+
try {
|
|
31
|
+
body = (await req.json!()) as typeof body
|
|
32
|
+
} catch {
|
|
33
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const provider = body.provider as Provider
|
|
37
|
+
const from = (body.from || '').trim()
|
|
38
|
+
const text = (body.text || '').trim()
|
|
39
|
+
if (!PROVIDERS.includes(provider) || !from || !text) {
|
|
40
|
+
return Response.json({ error: 'provider, from et text requis.' }, { status: 400 })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const payload = req.payload
|
|
45
|
+
// Resolve (or create) the channel client by a synthetic, stable identity.
|
|
46
|
+
const email = `${from}@${provider}.channel`
|
|
47
|
+
const existing = await dbFind(payload, slugs.supportClients, { where: { email: { equals: email } }, limit: 1, depth: 0, overrideAccess: true })
|
|
48
|
+
let clientId: number | string
|
|
49
|
+
if (existing.docs.length > 0) {
|
|
50
|
+
clientId = (existing.docs[0] as { id: number | string }).id
|
|
51
|
+
} else {
|
|
52
|
+
const created = await dbCreate(payload, slugs.supportClients, {
|
|
53
|
+
data: { email, firstName: body.name || provider, lastName: from.slice(0, 40), company: provider, password: randomBytes(16).toString('hex') },
|
|
54
|
+
overrideAccess: true,
|
|
55
|
+
})
|
|
56
|
+
clientId = (created as { id: number | string }).id
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Find an open ticket for this client + channel, else open a new one.
|
|
60
|
+
const open = await dbFind(payload, slugs.tickets, {
|
|
61
|
+
where: { and: [{ client: { equals: clientId } }, { source: { equals: provider } }, { status: { in: ['open', 'waiting_client'] } }] },
|
|
62
|
+
sort: '-createdAt', limit: 1, depth: 0, overrideAccess: true,
|
|
63
|
+
})
|
|
64
|
+
let ticketId: number | string
|
|
65
|
+
let createdTicket = false
|
|
66
|
+
if (open.docs.length > 0) {
|
|
67
|
+
ticketId = (open.docs[0] as { id: number | string }).id
|
|
68
|
+
} else {
|
|
69
|
+
const t = await dbCreate(payload, slugs.tickets, {
|
|
70
|
+
data: { subject: text.slice(0, 80), client: clientId, status: 'open', priority: 'normal', source: provider },
|
|
71
|
+
overrideAccess: true,
|
|
72
|
+
})
|
|
73
|
+
ticketId = (t as { id: number | string }).id
|
|
74
|
+
createdTicket = true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const msg = await dbCreate(payload, slugs.ticketMessages, {
|
|
78
|
+
data: { ticket: ticketId, body: text, authorType: 'client', authorClient: clientId },
|
|
79
|
+
overrideAccess: true,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return Response.json({ ok: true, ticketId, createdTicket, messageId: (msg as { id: number | string }).id })
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error('[channels] Error:', err)
|
|
85
|
+
return Response.json({ error: 'Erreur interne' }, { status: 500 })
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|