@consilioweb/payload-support 0.15.0 → 1.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.
Files changed (202) hide show
  1. package/README.md +347 -435
  2. package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
  4. package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
  5. package/dist/components/TicketConversation/SkeletonText.js +20 -0
  6. package/dist/components/TicketConversation/components/ActionPanels.cjs +72 -0
  7. package/dist/components/TicketConversation/components/ActionPanels.js +73 -2
  8. package/dist/components/TicketConversation/components/QuickActions.cjs +22 -1
  9. package/dist/components/TicketConversation/components/QuickActions.js +22 -1
  10. package/dist/components/TicketConversation/constants.cjs +27 -0
  11. package/dist/components/TicketConversation/constants.js +26 -1
  12. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
  13. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
  14. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +59 -0
  15. package/dist/components/TicketConversation/hooks/useTicketActions.js +59 -0
  16. package/dist/components/TicketConversation/index.cjs +42 -149
  17. package/dist/components/TicketConversation/index.js +37 -144
  18. package/dist/index.cjs +1835 -741
  19. package/dist/index.d.cts +3 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +1833 -741
  22. package/dist/views/BillingView/client.cjs +40 -54
  23. package/dist/views/BillingView/client.js +40 -54
  24. package/dist/views/ChatView/client.cjs +4 -3
  25. package/dist/views/ChatView/client.js +4 -3
  26. package/dist/views/CrmView/client.cjs +7 -7
  27. package/dist/views/CrmView/client.js +7 -7
  28. package/dist/views/EmailTrackingView/client.cjs +6 -5
  29. package/dist/views/EmailTrackingView/client.js +6 -5
  30. package/dist/views/ImportConversationView/client.cjs +4 -4
  31. package/dist/views/ImportConversationView/client.js +4 -4
  32. package/dist/views/LogsView/client.cjs +3 -2
  33. package/dist/views/LogsView/client.js +3 -2
  34. package/dist/views/NewTicketView/client.cjs +1 -1
  35. package/dist/views/NewTicketView/client.js +1 -1
  36. package/dist/views/PendingEmailsView/client.cjs +7 -6
  37. package/dist/views/PendingEmailsView/client.js +7 -6
  38. package/dist/views/SupportDashboardView/client.cjs +19 -22
  39. package/dist/views/SupportDashboardView/client.js +19 -22
  40. package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
  41. package/dist/views/TicketDetailView/NextActionItem.js +34 -0
  42. package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
  43. package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
  44. package/dist/views/TicketDetailView/client.cjs +59 -190
  45. package/dist/views/TicketDetailView/client.js +52 -183
  46. package/dist/views/TicketDetailView/constants.cjs +20 -0
  47. package/dist/views/TicketDetailView/constants.js +16 -0
  48. package/dist/views/TicketDetailView/helpers.cjs +23 -0
  49. package/dist/views/TicketDetailView/helpers.js +20 -0
  50. package/dist/views/TicketDetailView/types.cjs +2 -0
  51. package/dist/views/TicketDetailView/types.js +1 -0
  52. package/dist/views/TicketInboxView/client.cjs +14 -10
  53. package/dist/views/TicketInboxView/client.js +14 -10
  54. package/dist/views/TicketingSettingsView/client.cjs +101 -96
  55. package/dist/views/TicketingSettingsView/client.js +92 -88
  56. package/dist/views/TimeDashboardView/client.cjs +2 -2
  57. package/dist/views/TimeDashboardView/client.js +2 -2
  58. package/dist/views/shared/StatusPill.cjs +23 -12
  59. package/dist/views/shared/StatusPill.js +23 -12
  60. package/dist/views/shared/dateLocale.cjs +5 -0
  61. package/dist/views/shared/dateLocale.js +3 -0
  62. package/package.json +23 -15
  63. package/src/__tests__/generateTrackingToken.test.ts +53 -0
  64. package/src/__tests__/integration/ai-agent.test.ts +46 -0
  65. package/src/__tests__/integration/auth.test.ts +43 -0
  66. package/src/__tests__/integration/automation-rules.test.ts +68 -0
  67. package/src/__tests__/integration/buildTestPayload.ts +52 -0
  68. package/src/__tests__/integration/channels.test.ts +52 -0
  69. package/src/__tests__/integration/core-behaviors.test.ts +58 -0
  70. package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
  71. package/src/__tests__/integration/digest.test.ts +48 -0
  72. package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
  73. package/src/__tests__/integration/isolation.test.ts +81 -0
  74. package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
  75. package/src/__tests__/integration/nps.test.ts +37 -0
  76. package/src/__tests__/integration/sanitization.test.ts +35 -0
  77. package/src/__tests__/integration/sla-pause.test.ts +39 -0
  78. package/src/__tests__/integration/smoke.test.ts +26 -0
  79. package/src/__tests__/integration/snooze.test.ts +68 -0
  80. package/src/__tests__/integration/teams.test.ts +38 -0
  81. package/src/__tests__/rateLimiter.test.ts +89 -0
  82. package/src/__tests__/sanitizeHtml.test.ts +165 -0
  83. package/src/__tests__/sla.test.ts +258 -0
  84. package/src/collections/AutomationRules.ts +66 -0
  85. package/src/collections/NotificationQueue.ts +30 -0
  86. package/src/collections/SatisfactionSurveys.ts +11 -2
  87. package/src/collections/SupportClients.ts +58 -1
  88. package/src/collections/SupportTeam.ts +27 -0
  89. package/src/collections/TicketActivityLog.ts +9 -4
  90. package/src/collections/TicketMessages.ts +137 -31
  91. package/src/collections/Tickets.ts +31 -72
  92. package/src/collections/index.ts +3 -0
  93. package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
  94. package/src/components/TicketConversation/SkeletonText.tsx +23 -0
  95. package/src/components/TicketConversation/components/ActionPanels.tsx +82 -1
  96. package/src/components/TicketConversation/components/QuickActions.tsx +25 -0
  97. package/src/components/TicketConversation/constants.ts +29 -0
  98. package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
  99. package/src/components/TicketConversation/hooks/useTicketActions.ts +51 -0
  100. package/src/components/TicketConversation/index.tsx +29 -157
  101. package/src/endpoints/admin-chat-stream.ts +3 -4
  102. package/src/endpoints/admin-chat.ts +11 -20
  103. package/src/endpoints/admin-stats.ts +70 -32
  104. package/src/endpoints/ai-agent.ts +36 -0
  105. package/src/endpoints/apply-macro.ts +8 -14
  106. package/src/endpoints/auth-2fa.ts +12 -12
  107. package/src/endpoints/auto-close.ts +38 -18
  108. package/src/endpoints/billing.ts +17 -4
  109. package/src/endpoints/bulk-action.ts +7 -12
  110. package/src/endpoints/channels.ts +89 -0
  111. package/src/endpoints/chat-stream.ts +3 -4
  112. package/src/endpoints/chat.ts +7 -12
  113. package/src/endpoints/chatbot.ts +2 -2
  114. package/src/endpoints/client-intelligence.ts +4 -4
  115. package/src/endpoints/delete-account.ts +9 -16
  116. package/src/endpoints/email-stats.ts +2 -2
  117. package/src/endpoints/escalate.ts +2 -2
  118. package/src/endpoints/export-csv.ts +2 -2
  119. package/src/endpoints/export-data.ts +5 -8
  120. package/src/endpoints/import-conversation.ts +6 -10
  121. package/src/endpoints/index.ts +24 -2
  122. package/src/endpoints/invite-collaborator.ts +30 -15
  123. package/src/endpoints/invoice.ts +127 -0
  124. package/src/endpoints/kb-search.ts +2 -2
  125. package/src/endpoints/login.ts +12 -4
  126. package/src/endpoints/merge-clients.ts +14 -19
  127. package/src/endpoints/merge-tickets.ts +11 -18
  128. package/src/endpoints/oauth-google.ts +86 -39
  129. package/src/endpoints/pending-emails-process.ts +15 -25
  130. package/src/endpoints/process-digests.ts +93 -0
  131. package/src/endpoints/process-scheduled.ts +6 -10
  132. package/src/endpoints/process-snooze.ts +78 -0
  133. package/src/endpoints/purge-logs.ts +2 -2
  134. package/src/endpoints/resend-notification.ts +3 -4
  135. package/src/endpoints/round-robin-config.ts +3 -4
  136. package/src/endpoints/satisfaction.ts +21 -14
  137. package/src/endpoints/search.ts +5 -8
  138. package/src/endpoints/seed-kb.ts +3 -4
  139. package/src/endpoints/send-reminder.ts +186 -0
  140. package/src/endpoints/settings.ts +5 -2
  141. package/src/endpoints/signature.ts +3 -4
  142. package/src/endpoints/sla-check.ts +2 -2
  143. package/src/endpoints/split-ticket.ts +7 -12
  144. package/src/endpoints/statuses.ts +2 -2
  145. package/src/endpoints/ticket-feedback.ts +4 -6
  146. package/src/endpoints/ticket-synthesis.ts +2 -2
  147. package/src/endpoints/track-open.ts +16 -28
  148. package/src/endpoints/transfer-ticket.ts +21 -9
  149. package/src/endpoints/user-prefs.ts +3 -4
  150. package/src/hooks/automationRules.ts +84 -0
  151. package/src/hooks/checkSLA.ts +48 -16
  152. package/src/plugin.ts +6 -0
  153. package/src/portal/LiveChat.tsx +2 -63
  154. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
  155. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
  156. package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
  157. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
  158. package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
  159. package/src/portal/auth/tickets/detail/page.tsx +2 -2
  160. package/src/portal/helpers.ts +46 -0
  161. package/src/portal/login/page.tsx +3 -8
  162. package/src/portal/types.ts +15 -0
  163. package/src/types/lucide-react.d.ts +34 -0
  164. package/src/types/payload-config.d.ts +10 -0
  165. package/src/utils/adminNotification.ts +2 -2
  166. package/src/utils/aiAgent.ts +147 -0
  167. package/src/utils/auth.ts +5 -4
  168. package/src/utils/channels.ts +36 -0
  169. package/src/utils/db.ts +73 -0
  170. package/src/utils/emailTemplate.ts +8 -1
  171. package/src/utils/fireWebhooks.ts +4 -6
  172. package/src/utils/generateTicketSynthesis.ts +7 -12
  173. package/src/utils/invoicePdf.ts +88 -0
  174. package/src/utils/notificationQueue.ts +36 -0
  175. package/src/utils/readSettings.ts +22 -6
  176. package/src/utils/sanitizeHtml.ts +57 -0
  177. package/src/utils/slugs.ts +6 -0
  178. package/src/utils/teamAccess.ts +27 -0
  179. package/src/utils/ticketAccess.ts +53 -0
  180. package/src/utils/webhookDispatcher.ts +4 -6
  181. package/src/views/BillingView/client.tsx +49 -37
  182. package/src/views/ChatView/client.tsx +4 -3
  183. package/src/views/CrmView/client.tsx +7 -7
  184. package/src/views/EmailTrackingView/client.tsx +7 -6
  185. package/src/views/ImportConversationView/client.tsx +4 -4
  186. package/src/views/LogsView/client.tsx +3 -2
  187. package/src/views/NewTicketView/client.tsx +1 -1
  188. package/src/views/PendingEmailsView/client.tsx +7 -6
  189. package/src/views/SupportDashboardView/client.tsx +21 -21
  190. package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
  191. package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
  192. package/src/views/TicketDetailView/client.tsx +54 -179
  193. package/src/views/TicketDetailView/constants.ts +23 -0
  194. package/src/views/TicketDetailView/helpers.ts +17 -0
  195. package/src/views/TicketDetailView/types.ts +10 -0
  196. package/src/views/TicketInboxView/client.tsx +16 -10
  197. package/src/views/TicketingSettingsView/client.tsx +90 -87
  198. package/src/views/TimeDashboardView/client.tsx +2 -2
  199. package/src/views/shared/StatusPill.tsx +24 -12
  200. package/src/views/shared/dateLocale.ts +5 -0
  201. package/src/views/shared/locales/en.json +708 -0
  202. package/src/views/shared/locales/fr.json +708 -0
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Endpoint } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
3
  import { requireClient, handleAuthError } from '../utils/auth'
4
+ import { dbFind } from '../utils/db'
4
5
 
5
6
  /**
6
7
  * GET /api/support/chat-stream?session=xxx
@@ -30,8 +31,7 @@ export function createChatStreamEndpoint(slugs: CollectionSlugs): Endpoint {
30
31
  }
31
32
 
32
33
  // Verify session belongs to user
33
- const existing = await req.payload.find({
34
- collection: slugs.chatMessages as any,
34
+ const existing = await dbFind(req.payload, slugs.chatMessages, {
35
35
  where: {
36
36
  session: { equals: sessionId },
37
37
  client: { equals: userId },
@@ -57,8 +57,7 @@ export function createChatStreamEndpoint(slugs: CollectionSlugs): Endpoint {
57
57
  // Poll DB every 2 seconds for new messages
58
58
  const interval = setInterval(async () => {
59
59
  try {
60
- const messages = await req.payload.find({
61
- collection: slugs.chatMessages as any,
60
+ const messages = await dbFind(req.payload, slugs.chatMessages, {
62
61
  where: {
63
62
  session: { equals: sessionId },
64
63
  createdAt: { greater_than: lastCheck },
@@ -4,6 +4,7 @@ import type { CollectionSlugs } from '../utils/slugs'
4
4
  import crypto from 'crypto'
5
5
  import { RateLimiter } from '../utils/rateLimiter'
6
6
  import { requireClient, handleAuthError } from '../utils/auth'
7
+ import { dbFind, dbCreate } from '../utils/db'
7
8
 
8
9
  const chatSessionLimiter = new RateLimiter(3_600_000, 5) // 5 sessions per hour
9
10
  const chatMessageLimiter = new RateLimiter(60_000, 15) // 15 messages per minute
@@ -39,8 +40,7 @@ export function createChatGetEndpoint(slugs: CollectionSlugs): Endpoint {
39
40
  where.createdAt = { greater_than: after }
40
41
  }
41
42
 
42
- const messages = await payload.find({
43
- collection: slugs.chatMessages as any,
43
+ const messages = await dbFind(payload, slugs.chatMessages, {
44
44
  where,
45
45
  sort: 'createdAt',
46
46
  limit: 100,
@@ -95,8 +95,7 @@ export function createChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
95
95
 
96
96
  const sessionId = `chat_${crypto.randomBytes(16).toString('hex')}`
97
97
 
98
- const systemMsg = await payload.create({
99
- collection: slugs.chatMessages as any,
98
+ const systemMsg = await dbCreate(payload, slugs.chatMessages, {
100
99
  data: {
101
100
  session: sessionId,
102
101
  client: req.user.id,
@@ -122,8 +121,7 @@ export function createChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
122
121
  }
123
122
 
124
123
  // Verify session belongs to user
125
- const existing = await payload.find({
126
- collection: slugs.chatMessages as any,
124
+ const existing = await dbFind(payload, slugs.chatMessages, {
127
125
  where: { session: { equals: session }, client: { equals: req.user.id } },
128
126
  limit: 1,
129
127
  overrideAccess: true,
@@ -133,8 +131,7 @@ export function createChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
133
131
  return Response.json({ error: 'Session invalide' }, { status: 403 })
134
132
  }
135
133
 
136
- const newMsg = await payload.create({
137
- collection: slugs.chatMessages as any,
134
+ const newMsg = await dbCreate(payload, slugs.chatMessages, {
138
135
  data: {
139
136
  session,
140
137
  client: req.user.id,
@@ -150,8 +147,7 @@ export function createChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
150
147
 
151
148
  // Close session
152
149
  if (action === 'close' && session) {
153
- const existing = await payload.find({
154
- collection: slugs.chatMessages as any,
150
+ const existing = await dbFind(payload, slugs.chatMessages, {
155
151
  where: { session: { equals: session }, client: { equals: req.user.id } },
156
152
  limit: 1,
157
153
  overrideAccess: true,
@@ -161,8 +157,7 @@ export function createChatPostEndpoint(slugs: CollectionSlugs): Endpoint {
161
157
  return Response.json({ error: 'Session invalide' }, { status: 403 })
162
158
  }
163
159
 
164
- await payload.create({
165
- collection: slugs.chatMessages as any,
160
+ await dbCreate(payload, slugs.chatMessages, {
166
161
  data: {
167
162
  session,
168
163
  client: req.user.id,
@@ -1,6 +1,7 @@
1
1
  import type { Endpoint } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
3
  import { RateLimiter } from '../utils/rateLimiter'
4
+ import { dbFind } from '../utils/db'
4
5
 
5
6
  const chatbotLimiter = new RateLimiter(60_000, 10) // 10 requests per minute per IP
6
7
 
@@ -34,8 +35,7 @@ export function createChatbotEndpoint(slugs: CollectionSlugs): Endpoint {
34
35
 
35
36
  const payload = req.payload
36
37
 
37
- const articles = await payload.find({
38
- collection: slugs.knowledgeBase as any,
38
+ const articles = await dbFind(payload, slugs.knowledgeBase, {
39
39
  where: { published: { equals: true } },
40
40
  limit: 100,
41
41
  depth: 0,
@@ -1,4 +1,4 @@
1
- import type { Endpoint } from 'payload'
1
+ import type { Endpoint, PayloadRequest } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
3
  import { requireAdmin, handleAuthError } from '../utils/auth'
4
4
  import { readSupportSettings, type SupportSettings } from '../utils/readSettings'
@@ -26,7 +26,7 @@ const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
26
26
  * Force-refreshes the summary.
27
27
  */
28
28
  export function createClientIntelligenceEndpoint(slugs: CollectionSlugs): Endpoint[] {
29
- const getHandler = async (req: any) => {
29
+ const getHandler = async (req: PayloadRequest) => {
30
30
  try {
31
31
  requireAdmin(req, slugs)
32
32
  const payload = req.payload
@@ -61,7 +61,7 @@ export function createClientIntelligenceEndpoint(slugs: CollectionSlugs): Endpoi
61
61
  }
62
62
  }
63
63
 
64
- const postHandler = async (req: any) => {
64
+ const postHandler = async (req: PayloadRequest) => {
65
65
  try {
66
66
  requireAdmin(req, slugs)
67
67
  const payload = req.payload
@@ -97,7 +97,7 @@ async function generateSummary(
97
97
  payload: any,
98
98
  clientId: string,
99
99
  slugs: CollectionSlugs,
100
- existingId?: number,
100
+ existingId?: number | string,
101
101
  ) {
102
102
  const aiSettings = (await readSupportSettings(payload)).ai
103
103
  if (!aiSettings.enableSynthesis) {
@@ -1,6 +1,7 @@
1
1
  import type { Endpoint } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
3
  import { requireClient, handleAuthError } from '../utils/auth'
4
+ import { dbFind, dbDelete } from '../utils/db'
4
5
 
5
6
  /**
6
7
  * POST /api/support/delete-account
@@ -44,8 +45,7 @@ export function createDeleteAccountEndpoint(slugs: CollectionSlugs): Endpoint {
44
45
  const clientId = req.user.id
45
46
 
46
47
  // 1. Find all ticket IDs for this client
47
- const tickets = await payload.find({
48
- collection: slugs.tickets as any,
48
+ const tickets = await dbFind(payload, slugs.tickets, {
49
49
  where: { client: { equals: clientId } },
50
50
  limit: 10000,
51
51
  depth: 0,
@@ -57,49 +57,42 @@ export function createDeleteAccountEndpoint(slugs: CollectionSlugs): Endpoint {
57
57
 
58
58
  // 2. Batch delete related data using where clauses
59
59
  if (ticketIds.length > 0) {
60
- await payload.delete({
61
- collection: slugs.ticketMessages as any,
60
+ await dbDelete(payload, slugs.ticketMessages, {
62
61
  where: { ticket: { in: ticketIds } },
63
62
  overrideAccess: true,
64
63
  })
65
64
 
66
- await payload.delete({
67
- collection: slugs.ticketActivityLog as any,
65
+ await dbDelete(payload, slugs.ticketActivityLog, {
68
66
  where: { ticket: { in: ticketIds } },
69
67
  overrideAccess: true,
70
68
  })
71
69
 
72
- await payload.delete({
73
- collection: slugs.timeEntries as any,
70
+ await dbDelete(payload, slugs.timeEntries, {
74
71
  where: { ticket: { in: ticketIds } },
75
72
  overrideAccess: true,
76
73
  })
77
74
 
78
75
  // Delete all tickets
79
- await payload.delete({
80
- collection: slugs.tickets as any,
76
+ await dbDelete(payload, slugs.tickets, {
81
77
  where: { client: { equals: clientId } },
82
78
  overrideAccess: true,
83
79
  })
84
80
  }
85
81
 
86
82
  // 3. Batch delete satisfaction surveys
87
- await payload.delete({
88
- collection: slugs.satisfactionSurveys as any,
83
+ await dbDelete(payload, slugs.satisfactionSurveys, {
89
84
  where: { client: { equals: clientId } },
90
85
  overrideAccess: true,
91
86
  })
92
87
 
93
88
  // 4. Batch delete chat messages
94
- await payload.delete({
95
- collection: slugs.chatMessages as any,
89
+ await dbDelete(payload, slugs.chatMessages, {
96
90
  where: { client: { equals: clientId } },
97
91
  overrideAccess: true,
98
92
  })
99
93
 
100
94
  // 5. Delete the client account
101
- await payload.delete({
102
- collection: slugs.supportClients as any,
95
+ await dbDelete(payload, slugs.supportClients, {
103
96
  id: clientId,
104
97
  overrideAccess: true,
105
98
  })
@@ -1,5 +1,6 @@
1
1
  import type { Endpoint } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
+ import { dbFind } from '../utils/db'
3
4
 
4
5
  /**
5
6
  * GET /api/support/email-stats?days=7
@@ -32,8 +33,7 @@ export function createEmailStatsEndpoint(slugs: CollectionSlugs): Endpoint {
32
33
  let hasMore = true
33
34
 
34
35
  while (hasMore && page <= MAX_PAGES) {
35
- const result = await payload.find({
36
- collection: slugs.emailLogs as any,
36
+ const result = await dbFind(payload, slugs.emailLogs, {
37
37
  where: { createdAt: { greater_than: cutoff } },
38
38
  sort: '-createdAt',
39
39
  limit: 500,
@@ -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 } from '../utils/db'
4
5
 
5
6
  /**
6
7
  * POST /api/support/tickets/:id/escalate
@@ -25,8 +26,7 @@ export function createEscalateEndpoint(slugs: CollectionSlugs): Endpoint {
25
26
  return Response.json({ error: 'invalid-id' }, { status: 400 })
26
27
  }
27
28
 
28
- const ticket = await req.payload.update({
29
- collection: slugs.tickets as any,
29
+ const ticket = await dbUpdate(req.payload, slugs.tickets, {
30
30
  id: ticketId,
31
31
  data: { status: 'escalated' },
32
32
  overrideAccess: true,
@@ -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 } from '../utils/db'
4
5
 
5
6
  /**
6
7
  * GET /api/support/export-csv
@@ -22,8 +23,7 @@ export function createExportCsvEndpoint(slugs: CollectionSlugs): Endpoint {
22
23
  const allDocs: Array<Record<string, unknown>> = []
23
24
 
24
25
  while (hasMore) {
25
- const batch = await payload.find({
26
- collection: slugs.tickets as any,
26
+ const batch = await dbFind(payload, slugs.tickets, {
27
27
  limit: PAGE_SIZE,
28
28
  page,
29
29
  depth: 1,
@@ -1,6 +1,7 @@
1
1
  import type { Endpoint } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
3
  import { requireClient, handleAuthError } from '../utils/auth'
4
+ import { dbFind, dbFindByID } from '../utils/db'
4
5
 
5
6
  /**
6
7
  * GET /api/support/export-data
@@ -17,21 +18,18 @@ export function createExportDataEndpoint(slugs: CollectionSlugs): Endpoint {
17
18
  requireClient(req, slugs)
18
19
 
19
20
  const [clientData, ticketsResult, messagesResult, surveysResult] = await Promise.all([
20
- payload.findByID({
21
- collection: slugs.supportClients as any,
21
+ dbFindByID(payload, slugs.supportClients, {
22
22
  id: req.user.id,
23
23
  depth: 0,
24
24
  overrideAccess: true,
25
25
  }),
26
- payload.find({
27
- collection: slugs.tickets as any,
26
+ dbFind(payload, slugs.tickets, {
28
27
  where: { client: { equals: req.user.id } },
29
28
  limit: 1000,
30
29
  depth: 0,
31
30
  overrideAccess: true,
32
31
  }),
33
- payload.find({
34
- collection: slugs.ticketMessages as any,
32
+ dbFind(payload, slugs.ticketMessages, {
35
33
  where: {
36
34
  'ticket.client': { equals: req.user.id },
37
35
  authorType: { equals: 'client' },
@@ -40,8 +38,7 @@ export function createExportDataEndpoint(slugs: CollectionSlugs): Endpoint {
40
38
  depth: 0,
41
39
  overrideAccess: true,
42
40
  }),
43
- payload.find({
44
- collection: slugs.satisfactionSurveys as any,
41
+ dbFind(payload, slugs.satisfactionSurveys, {
45
42
  where: { client: { equals: req.user.id } },
46
43
  limit: 500,
47
44
  depth: 0,
@@ -2,6 +2,7 @@ import type { Endpoint } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
3
  import { RateLimiter } from '../utils/rateLimiter'
4
4
  import { readSupportSettings } from '../utils/readSettings'
5
+ import { dbFind, dbCreate } from '../utils/db'
5
6
  import crypto from 'crypto'
6
7
 
7
8
  interface ParsedConversation {
@@ -215,8 +216,7 @@ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoi
215
216
  }
216
217
 
217
218
  // Find or create support client
218
- const clientResult = await payload.find({
219
- collection: slugs.supportClients as any,
219
+ const clientResult = await dbFind(payload, slugs.supportClients, {
220
220
  where: { email: { equals: conversation.client.email } },
221
221
  limit: 1,
222
222
  depth: 0,
@@ -230,8 +230,7 @@ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoi
230
230
  const nameParts = conversation.client.name.split(' ')
231
231
  const randomPassword = crypto.randomBytes(48).toString('base64url')
232
232
 
233
- client = await payload.create({
234
- collection: slugs.supportClients as any,
233
+ client = await dbCreate(payload, slugs.supportClients, {
235
234
  data: {
236
235
  email: conversation.client.email,
237
236
  password: randomPassword,
@@ -245,8 +244,7 @@ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoi
245
244
  }
246
245
 
247
246
  // Find admin user for authorAdmin
248
- const adminUsers = await payload.find({
249
- collection: slugs.users as any,
247
+ const adminUsers = await dbFind(payload, slugs.users, {
250
248
  limit: 1,
251
249
  depth: 0,
252
250
  overrideAccess: true,
@@ -254,8 +252,7 @@ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoi
254
252
  const adminUserId = adminUsers.docs[0]?.id
255
253
 
256
254
  // Create ticket
257
- const ticket = await payload.create({
258
- collection: slugs.tickets as any,
255
+ const ticket = await dbCreate(payload, slugs.tickets, {
259
256
  data: {
260
257
  subject: conversation.subject,
261
258
  client: client.id,
@@ -271,8 +268,7 @@ export function createImportConversationEndpoint(slugs: CollectionSlugs): Endpoi
271
268
  for (const msg of conversation.messages) {
272
269
  const isAdmin = msg.from === 'admin'
273
270
 
274
- await payload.create({
275
- collection: slugs.ticketMessages as any,
271
+ await dbCreate(payload, slugs.ticketMessages, {
276
272
  data: {
277
273
  ticket: ticket.id,
278
274
  body: msg.content,
@@ -3,6 +3,7 @@ import type { CollectionSlugs } from '../utils/slugs'
3
3
  import type { SupportFeatures } from '../types'
4
4
 
5
5
  import { createAiEndpoint } from './ai'
6
+ import { createAiAgentEndpoint } from './ai-agent'
6
7
  import { createClientIntelligenceEndpoint } from './client-intelligence'
7
8
  import { createSearchEndpoint } from './search'
8
9
  import { createKbSearchEndpoint } from './kb-search'
@@ -16,6 +17,7 @@ import { createSignatureGetEndpoint, createSignaturePostEndpoint } from './signa
16
17
  import { createRoundRobinConfigGetEndpoint, createRoundRobinConfigPostEndpoint } from './round-robin-config'
17
18
  import { createSlaCheckEndpoint } from './sla-check'
18
19
  import { createAutoCloseEndpoint } from './auto-close'
20
+ import { createSendReminderEndpoint } from './send-reminder'
19
21
  import { createStatusesEndpoint } from './statuses'
20
22
  import { createApplyMacroEndpoint } from './apply-macro'
21
23
  import { createPurgeLogsEndpoint } from './purge-logs'
@@ -26,6 +28,7 @@ import { createAdminChatGetEndpoint, createAdminChatPostEndpoint } from './admin
26
28
  import { createAdminChatStreamEndpoint } from './admin-chat-stream'
27
29
  import { createAdminStatsEndpoint } from './admin-stats'
28
30
  import { createBillingEndpoint } from './billing'
31
+ import { createInvoiceEndpoint } from './invoice'
29
32
  import { createTicketSynthesisEndpoint } from './ticket-synthesis'
30
33
  import { createEmailStatsEndpoint } from './email-stats'
31
34
  import { createSatisfactionEndpoint } from './satisfaction'
@@ -42,6 +45,9 @@ import { createDeleteAccountEndpoint } from './delete-account'
42
45
  import { createMergeClientsEndpoint } from './merge-clients'
43
46
  import { createImportConversationEndpoint } from './import-conversation'
44
47
  import { createProcessScheduledEndpoint } from './process-scheduled'
48
+ import { createProcessSnoozeEndpoint } from './process-snooze'
49
+ import { createProcessDigestsEndpoint } from './process-digests'
50
+ import { createChannelsWebhookEndpoint } from './channels'
45
51
  import { createUserPrefsGetEndpoint, createUserPrefsPostEndpoint } from './user-prefs'
46
52
  import { createEscalateEndpoint } from './escalate'
47
53
  import { createTicketFeedbackEndpoint } from './ticket-feedback'
@@ -50,6 +56,7 @@ import { createInviteCollaboratorEndpoint } from './invite-collaborator'
50
56
 
51
57
  // Re-export all individual endpoint creators
52
58
  export { createAiEndpoint } from './ai'
59
+ export { createAiAgentEndpoint } from './ai-agent'
53
60
  export { createSearchEndpoint } from './search'
54
61
  export { createKbSearchEndpoint } from './kb-search'
55
62
  export { createBulkActionEndpoint } from './bulk-action'
@@ -62,6 +69,7 @@ export { createSignatureGetEndpoint, createSignaturePostEndpoint } from './signa
62
69
  export { createRoundRobinConfigGetEndpoint, createRoundRobinConfigPostEndpoint } from './round-robin-config'
63
70
  export { createSlaCheckEndpoint } from './sla-check'
64
71
  export { createAutoCloseEndpoint } from './auto-close'
72
+ export { createSendReminderEndpoint } from './send-reminder'
65
73
  export { createStatusesEndpoint } from './statuses'
66
74
  export { createApplyMacroEndpoint } from './apply-macro'
67
75
  export { createPurgeLogsEndpoint } from './purge-logs'
@@ -72,6 +80,7 @@ export { createAdminChatGetEndpoint, createAdminChatPostEndpoint } from './admin
72
80
  export { createAdminChatStreamEndpoint } from './admin-chat-stream'
73
81
  export { createAdminStatsEndpoint } from './admin-stats'
74
82
  export { createBillingEndpoint } from './billing'
83
+ export { createInvoiceEndpoint } from './invoice'
75
84
  export { createTicketSynthesisEndpoint } from './ticket-synthesis'
76
85
  export { createEmailStatsEndpoint } from './email-stats'
77
86
  export { createSatisfactionEndpoint } from './satisfaction'
@@ -88,6 +97,9 @@ export { createDeleteAccountEndpoint } from './delete-account'
88
97
  export { createMergeClientsEndpoint } from './merge-clients'
89
98
  export { createImportConversationEndpoint } from './import-conversation'
90
99
  export { createProcessScheduledEndpoint } from './process-scheduled'
100
+ export { createProcessSnoozeEndpoint } from './process-snooze'
101
+ export { createProcessDigestsEndpoint } from './process-digests'
102
+ export { createChannelsWebhookEndpoint } from './channels'
91
103
  export { createUserPrefsGetEndpoint, createUserPrefsPostEndpoint } from './user-prefs'
92
104
  export type { UserPrefs } from './user-prefs'
93
105
  export { createEscalateEndpoint } from './escalate'
@@ -137,6 +149,7 @@ export function createSupportEndpoints(slugs: CollectionSlugs, options?: Support
137
149
  // Conditional endpoints based on feature flags
138
150
  if (!f || f.ai !== false) {
139
151
  endpoints.push(createAiEndpoint(slugs))
152
+ endpoints.push(createAiAgentEndpoint(slugs))
140
153
  endpoints.push(...createClientIntelligenceEndpoint(slugs))
141
154
  endpoints.push(createTicketSynthesisEndpoint(slugs))
142
155
  }
@@ -151,7 +164,10 @@ export function createSupportEndpoints(slugs: CollectionSlugs, options?: Support
151
164
  endpoints.push(createSignatureGetEndpoint(slugs), createSignaturePostEndpoint(slugs))
152
165
  }
153
166
  if (!f || f.sla !== false) endpoints.push(createSlaCheckEndpoint(slugs))
154
- if (!f || f.autoClose !== false) endpoints.push(createAutoCloseEndpoint(slugs))
167
+ if (!f || f.autoClose !== false) {
168
+ endpoints.push(createAutoCloseEndpoint(slugs))
169
+ endpoints.push(createSendReminderEndpoint(slugs))
170
+ }
155
171
  if (!f || f.customStatuses !== false) endpoints.push(createStatusesEndpoint(slugs))
156
172
  if (!f || f.macros !== false) endpoints.push(createApplyMacroEndpoint(slugs))
157
173
  if (!f || f.roundRobin !== false) {
@@ -164,13 +180,19 @@ export function createSupportEndpoints(slugs: CollectionSlugs, options?: Support
164
180
  endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs))
165
181
  endpoints.push(createAdminChatStreamEndpoint(slugs))
166
182
  }
167
- if (!f || f.timeTracking !== false) endpoints.push(createBillingEndpoint(slugs))
183
+ if (!f || f.timeTracking !== false) {
184
+ endpoints.push(createBillingEndpoint(slugs))
185
+ endpoints.push(createInvoiceEndpoint(slugs))
186
+ }
168
187
  if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs))
169
188
  if (!f || f.emailTracking !== false) {
170
189
  endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs))
171
190
  }
172
191
  if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs))
173
192
  if (!f || f.scheduledReplies !== false) endpoints.push(createProcessScheduledEndpoint(slugs))
193
+ if (!f || f.snooze !== false) endpoints.push(createProcessSnoozeEndpoint(slugs))
194
+ endpoints.push(createProcessDigestsEndpoint(slugs))
195
+ endpoints.push(createChannelsWebhookEndpoint(slugs))
174
196
 
175
197
  return endpoints
176
198
  }