@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.
Files changed (199) hide show
  1. package/README.md +357 -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/constants.cjs +27 -0
  7. package/dist/components/TicketConversation/constants.js +26 -1
  8. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
  9. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
  10. package/dist/components/TicketConversation/index.cjs +10 -147
  11. package/dist/components/TicketConversation/index.js +4 -141
  12. package/dist/index.cjs +1763 -677
  13. package/dist/index.d.cts +4 -0
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.js +1760 -677
  16. package/dist/views/BillingView/client.cjs +40 -54
  17. package/dist/views/BillingView/client.js +40 -54
  18. package/dist/views/ChatView/client.cjs +4 -3
  19. package/dist/views/ChatView/client.js +4 -3
  20. package/dist/views/CrmView/client.cjs +7 -7
  21. package/dist/views/CrmView/client.js +7 -7
  22. package/dist/views/EmailTrackingView/client.cjs +6 -5
  23. package/dist/views/EmailTrackingView/client.js +6 -5
  24. package/dist/views/ImportConversationView/client.cjs +4 -4
  25. package/dist/views/ImportConversationView/client.js +4 -4
  26. package/dist/views/LogsView/client.cjs +3 -2
  27. package/dist/views/LogsView/client.js +3 -2
  28. package/dist/views/NewTicketView/client.cjs +1 -1
  29. package/dist/views/NewTicketView/client.js +1 -1
  30. package/dist/views/PendingEmailsView/client.cjs +7 -6
  31. package/dist/views/PendingEmailsView/client.js +7 -6
  32. package/dist/views/SupportDashboardView/client.cjs +19 -22
  33. package/dist/views/SupportDashboardView/client.js +19 -22
  34. package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
  35. package/dist/views/TicketDetailView/NextActionItem.js +34 -0
  36. package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
  37. package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
  38. package/dist/views/TicketDetailView/client.cjs +59 -190
  39. package/dist/views/TicketDetailView/client.js +52 -183
  40. package/dist/views/TicketDetailView/constants.cjs +20 -0
  41. package/dist/views/TicketDetailView/constants.js +16 -0
  42. package/dist/views/TicketDetailView/helpers.cjs +23 -0
  43. package/dist/views/TicketDetailView/helpers.js +20 -0
  44. package/dist/views/TicketDetailView/types.cjs +2 -0
  45. package/dist/views/TicketDetailView/types.js +1 -0
  46. package/dist/views/TicketInboxView/client.cjs +14 -10
  47. package/dist/views/TicketInboxView/client.js +14 -10
  48. package/dist/views/TicketingSettingsView/client.cjs +101 -96
  49. package/dist/views/TicketingSettingsView/client.js +92 -88
  50. package/dist/views/TimeDashboardView/client.cjs +2 -2
  51. package/dist/views/TimeDashboardView/client.js +2 -2
  52. package/dist/views/shared/StatusPill.cjs +23 -12
  53. package/dist/views/shared/StatusPill.js +23 -12
  54. package/dist/views/shared/dateLocale.cjs +5 -0
  55. package/dist/views/shared/dateLocale.js +3 -0
  56. package/package.json +28 -15
  57. package/src/__tests__/generateTrackingToken.test.ts +53 -0
  58. package/src/__tests__/integration/ai-agent.test.ts +46 -0
  59. package/src/__tests__/integration/auth.test.ts +43 -0
  60. package/src/__tests__/integration/automation-rules.test.ts +68 -0
  61. package/src/__tests__/integration/buildTestPayload.ts +52 -0
  62. package/src/__tests__/integration/channels.test.ts +52 -0
  63. package/src/__tests__/integration/core-behaviors.test.ts +58 -0
  64. package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
  65. package/src/__tests__/integration/digest.test.ts +48 -0
  66. package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
  67. package/src/__tests__/integration/isolation.test.ts +81 -0
  68. package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
  69. package/src/__tests__/integration/nps.test.ts +37 -0
  70. package/src/__tests__/integration/per-team-sla.test.ts +44 -0
  71. package/src/__tests__/integration/push.test.ts +75 -0
  72. package/src/__tests__/integration/sanitization.test.ts +35 -0
  73. package/src/__tests__/integration/sla-pause.test.ts +39 -0
  74. package/src/__tests__/integration/smoke.test.ts +26 -0
  75. package/src/__tests__/integration/snooze.test.ts +68 -0
  76. package/src/__tests__/integration/teams.test.ts +38 -0
  77. package/src/__tests__/rateLimiter.test.ts +89 -0
  78. package/src/__tests__/sanitizeHtml.test.ts +165 -0
  79. package/src/__tests__/sla.test.ts +258 -0
  80. package/src/collections/AutomationRules.ts +66 -0
  81. package/src/collections/NotificationQueue.ts +30 -0
  82. package/src/collections/PushSubscription.ts +28 -0
  83. package/src/collections/SatisfactionSurveys.ts +11 -2
  84. package/src/collections/SlaPolicies.ts +10 -0
  85. package/src/collections/SupportClients.ts +58 -1
  86. package/src/collections/SupportTeam.ts +27 -0
  87. package/src/collections/TicketActivityLog.ts +9 -4
  88. package/src/collections/TicketMessages.ts +140 -31
  89. package/src/collections/Tickets.ts +31 -73
  90. package/src/collections/index.ts +4 -0
  91. package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
  92. package/src/components/TicketConversation/SkeletonText.tsx +23 -0
  93. package/src/components/TicketConversation/constants.ts +29 -0
  94. package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
  95. package/src/components/TicketConversation/index.tsx +4 -152
  96. package/src/endpoints/admin-chat-stream.ts +3 -4
  97. package/src/endpoints/admin-chat.ts +11 -20
  98. package/src/endpoints/admin-stats.ts +77 -35
  99. package/src/endpoints/ai-agent.ts +36 -0
  100. package/src/endpoints/apply-macro.ts +8 -14
  101. package/src/endpoints/auth-2fa.ts +12 -12
  102. package/src/endpoints/auto-close.ts +6 -10
  103. package/src/endpoints/billing.ts +17 -4
  104. package/src/endpoints/bulk-action.ts +7 -12
  105. package/src/endpoints/channels.ts +89 -0
  106. package/src/endpoints/chat-stream.ts +3 -4
  107. package/src/endpoints/chat.ts +7 -12
  108. package/src/endpoints/chatbot.ts +2 -2
  109. package/src/endpoints/client-intelligence.ts +4 -4
  110. package/src/endpoints/delete-account.ts +9 -16
  111. package/src/endpoints/email-stats.ts +2 -2
  112. package/src/endpoints/escalate.ts +2 -2
  113. package/src/endpoints/export-csv.ts +2 -2
  114. package/src/endpoints/export-data.ts +5 -8
  115. package/src/endpoints/import-conversation.ts +6 -10
  116. package/src/endpoints/index.ts +22 -1
  117. package/src/endpoints/invite-collaborator.ts +30 -15
  118. package/src/endpoints/invoice.ts +127 -0
  119. package/src/endpoints/kb-search.ts +2 -2
  120. package/src/endpoints/login.ts +12 -4
  121. package/src/endpoints/merge-clients.ts +14 -19
  122. package/src/endpoints/merge-tickets.ts +11 -18
  123. package/src/endpoints/oauth-google.ts +86 -39
  124. package/src/endpoints/pending-emails-process.ts +15 -25
  125. package/src/endpoints/process-digests.ts +93 -0
  126. package/src/endpoints/process-scheduled.ts +6 -10
  127. package/src/endpoints/process-snooze.ts +78 -0
  128. package/src/endpoints/purge-logs.ts +2 -2
  129. package/src/endpoints/push.ts +49 -0
  130. package/src/endpoints/resend-notification.ts +3 -4
  131. package/src/endpoints/round-robin-config.ts +3 -4
  132. package/src/endpoints/satisfaction.ts +21 -14
  133. package/src/endpoints/search.ts +5 -8
  134. package/src/endpoints/seed-kb.ts +3 -4
  135. package/src/endpoints/send-reminder.ts +6 -9
  136. package/src/endpoints/settings.ts +5 -2
  137. package/src/endpoints/signature.ts +3 -4
  138. package/src/endpoints/sla-check.ts +2 -2
  139. package/src/endpoints/split-ticket.ts +7 -12
  140. package/src/endpoints/statuses.ts +2 -2
  141. package/src/endpoints/ticket-feedback.ts +4 -6
  142. package/src/endpoints/ticket-synthesis.ts +2 -2
  143. package/src/endpoints/track-open.ts +16 -28
  144. package/src/endpoints/transfer-ticket.ts +21 -9
  145. package/src/endpoints/user-prefs.ts +3 -4
  146. package/src/hooks/automationRules.ts +84 -0
  147. package/src/hooks/checkSLA.ts +63 -17
  148. package/src/plugin.ts +8 -0
  149. package/src/portal/LiveChat.tsx +2 -63
  150. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
  151. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
  152. package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
  153. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
  154. package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
  155. package/src/portal/auth/tickets/detail/page.tsx +2 -2
  156. package/src/portal/helpers.ts +46 -0
  157. package/src/portal/login/page.tsx +3 -8
  158. package/src/portal/types.ts +15 -0
  159. package/src/types/lucide-react.d.ts +34 -0
  160. package/src/types/payload-config.d.ts +10 -0
  161. package/src/utils/adminNotification.ts +2 -2
  162. package/src/utils/aiAgent.ts +147 -0
  163. package/src/utils/auth.ts +5 -4
  164. package/src/utils/channels.ts +36 -0
  165. package/src/utils/db.ts +73 -0
  166. package/src/utils/emailTemplate.ts +8 -1
  167. package/src/utils/fireWebhooks.ts +4 -6
  168. package/src/utils/generateTicketSynthesis.ts +7 -12
  169. package/src/utils/invoicePdf.ts +88 -0
  170. package/src/utils/notificationQueue.ts +36 -0
  171. package/src/utils/push.ts +63 -0
  172. package/src/utils/readSettings.ts +22 -6
  173. package/src/utils/sanitizeHtml.ts +57 -0
  174. package/src/utils/slugs.ts +8 -0
  175. package/src/utils/teamAccess.ts +27 -0
  176. package/src/utils/ticketAccess.ts +53 -0
  177. package/src/utils/webhookDispatcher.ts +4 -6
  178. package/src/views/BillingView/client.tsx +49 -37
  179. package/src/views/ChatView/client.tsx +4 -3
  180. package/src/views/CrmView/client.tsx +7 -7
  181. package/src/views/EmailTrackingView/client.tsx +7 -6
  182. package/src/views/ImportConversationView/client.tsx +4 -4
  183. package/src/views/LogsView/client.tsx +3 -2
  184. package/src/views/NewTicketView/client.tsx +1 -1
  185. package/src/views/PendingEmailsView/client.tsx +7 -6
  186. package/src/views/SupportDashboardView/client.tsx +21 -21
  187. package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
  188. package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
  189. package/src/views/TicketDetailView/client.tsx +54 -179
  190. package/src/views/TicketDetailView/constants.ts +23 -0
  191. package/src/views/TicketDetailView/helpers.ts +17 -0
  192. package/src/views/TicketDetailView/types.ts +10 -0
  193. package/src/views/TicketInboxView/client.tsx +16 -10
  194. package/src/views/TicketingSettingsView/client.tsx +90 -87
  195. package/src/views/TimeDashboardView/client.tsx +2 -2
  196. package/src/views/shared/StatusPill.tsx +24 -12
  197. package/src/views/shared/dateLocale.ts +5 -0
  198. package/src/views/shared/locales/en.json +708 -0
  199. 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 { 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'
@@ -27,6 +28,7 @@ import { createAdminChatGetEndpoint, createAdminChatPostEndpoint } from './admin
27
28
  import { createAdminChatStreamEndpoint } from './admin-chat-stream'
28
29
  import { createAdminStatsEndpoint } from './admin-stats'
29
30
  import { createBillingEndpoint } from './billing'
31
+ import { createInvoiceEndpoint } from './invoice'
30
32
  import { createTicketSynthesisEndpoint } from './ticket-synthesis'
31
33
  import { createEmailStatsEndpoint } from './email-stats'
32
34
  import { createSatisfactionEndpoint } from './satisfaction'
@@ -43,6 +45,10 @@ import { createDeleteAccountEndpoint } from './delete-account'
43
45
  import { createMergeClientsEndpoint } from './merge-clients'
44
46
  import { createImportConversationEndpoint } from './import-conversation'
45
47
  import { createProcessScheduledEndpoint } from './process-scheduled'
48
+ import { createProcessSnoozeEndpoint } from './process-snooze'
49
+ import { createProcessDigestsEndpoint } from './process-digests'
50
+ import { createChannelsWebhookEndpoint } from './channels'
51
+ import { createVapidKeyEndpoint, createPushSubscribeEndpoint } from './push'
46
52
  import { createUserPrefsGetEndpoint, createUserPrefsPostEndpoint } from './user-prefs'
47
53
  import { createEscalateEndpoint } from './escalate'
48
54
  import { createTicketFeedbackEndpoint } from './ticket-feedback'
@@ -51,6 +57,7 @@ import { createInviteCollaboratorEndpoint } from './invite-collaborator'
51
57
 
52
58
  // Re-export all individual endpoint creators
53
59
  export { createAiEndpoint } from './ai'
60
+ export { createAiAgentEndpoint } from './ai-agent'
54
61
  export { createSearchEndpoint } from './search'
55
62
  export { createKbSearchEndpoint } from './kb-search'
56
63
  export { createBulkActionEndpoint } from './bulk-action'
@@ -74,6 +81,7 @@ export { createAdminChatGetEndpoint, createAdminChatPostEndpoint } from './admin
74
81
  export { createAdminChatStreamEndpoint } from './admin-chat-stream'
75
82
  export { createAdminStatsEndpoint } from './admin-stats'
76
83
  export { createBillingEndpoint } from './billing'
84
+ export { createInvoiceEndpoint } from './invoice'
77
85
  export { createTicketSynthesisEndpoint } from './ticket-synthesis'
78
86
  export { createEmailStatsEndpoint } from './email-stats'
79
87
  export { createSatisfactionEndpoint } from './satisfaction'
@@ -90,6 +98,10 @@ export { createDeleteAccountEndpoint } from './delete-account'
90
98
  export { createMergeClientsEndpoint } from './merge-clients'
91
99
  export { createImportConversationEndpoint } from './import-conversation'
92
100
  export { createProcessScheduledEndpoint } from './process-scheduled'
101
+ export { createProcessSnoozeEndpoint } from './process-snooze'
102
+ export { createProcessDigestsEndpoint } from './process-digests'
103
+ export { createChannelsWebhookEndpoint } from './channels'
104
+ export { createVapidKeyEndpoint, createPushSubscribeEndpoint } from './push'
93
105
  export { createUserPrefsGetEndpoint, createUserPrefsPostEndpoint } from './user-prefs'
94
106
  export type { UserPrefs } from './user-prefs'
95
107
  export { createEscalateEndpoint } from './escalate'
@@ -139,6 +151,7 @@ export function createSupportEndpoints(slugs: CollectionSlugs, options?: Support
139
151
  // Conditional endpoints based on feature flags
140
152
  if (!f || f.ai !== false) {
141
153
  endpoints.push(createAiEndpoint(slugs))
154
+ endpoints.push(createAiAgentEndpoint(slugs))
142
155
  endpoints.push(...createClientIntelligenceEndpoint(slugs))
143
156
  endpoints.push(createTicketSynthesisEndpoint(slugs))
144
157
  }
@@ -169,13 +182,21 @@ export function createSupportEndpoints(slugs: CollectionSlugs, options?: Support
169
182
  endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs))
170
183
  endpoints.push(createAdminChatStreamEndpoint(slugs))
171
184
  }
172
- if (!f || f.timeTracking !== false) endpoints.push(createBillingEndpoint(slugs))
185
+ if (!f || f.timeTracking !== false) {
186
+ endpoints.push(createBillingEndpoint(slugs))
187
+ endpoints.push(createInvoiceEndpoint(slugs))
188
+ }
173
189
  if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs))
174
190
  if (!f || f.emailTracking !== false) {
175
191
  endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs))
176
192
  }
177
193
  if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs))
178
194
  if (!f || f.scheduledReplies !== false) endpoints.push(createProcessScheduledEndpoint(slugs))
195
+ if (!f || f.snooze !== false) endpoints.push(createProcessSnoozeEndpoint(slugs))
196
+ endpoints.push(createProcessDigestsEndpoint(slugs))
197
+ endpoints.push(createChannelsWebhookEndpoint(slugs))
198
+ endpoints.push(createVapidKeyEndpoint())
199
+ endpoints.push(createPushSubscribeEndpoint(slugs))
179
200
 
180
201
  return endpoints
181
202
  }
@@ -4,9 +4,16 @@ import { handleAuthError, AuthError } from '../utils/auth'
4
4
  import { escapeHtml, emailWrapper, emailButton, emailParagraph } from '../utils/emailTemplate'
5
5
  import { readSupportSettings } from '../utils/readSettings'
6
6
  import { randomBytes } from 'crypto'
7
+ import { RateLimiter } from '../utils/rateLimiter'
8
+ import { dbFindByID, dbFind, dbCreate, dbUpdate, dbCount } from '../utils/db'
7
9
 
8
10
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
9
11
 
12
+ // Anti-abuse limits: an invite can create a placeholder account AND send an
13
+ // email, so cap both the volume per user and the collaborators per ticket.
14
+ const MAX_COLLABORATORS_PER_TICKET = 20
15
+ const inviteLimiter = new RateLimiter(60 * 60 * 1000, 10)
16
+
10
17
  /**
11
18
  * POST /api/support/tickets/:id/invite
12
19
  *
@@ -52,12 +59,11 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
52
59
 
53
60
  const payload = req.payload
54
61
 
55
- const ticket = await payload.findByID({
56
- collection: slugs.tickets as any,
62
+ const ticket = await dbFindByID(payload, slugs.tickets, {
57
63
  id: ticketId,
58
64
  depth: 1,
59
65
  overrideAccess: true,
60
- }) as any
66
+ })
61
67
  if (!ticket) return Response.json({ error: 'Ticket introuvable' }, { status: 404 })
62
68
 
63
69
  const isAdmin = req.user.collection === slugs.users
@@ -68,6 +74,21 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
68
74
  return Response.json({ error: 'Forbidden' }, { status: 403 })
69
75
  }
70
76
 
77
+ // Anti-abuse: cap invite volume per user (in-memory, fail-closed).
78
+ if (inviteLimiter.check(String(req.user.id))) {
79
+ return Response.json({ error: 'Trop d\'invitations. Réessayez plus tard.' }, { status: 429 })
80
+ }
81
+
82
+ // Cap collaborators per ticket to prevent mass placeholder-account creation.
83
+ const collabCount = await dbCount(payload, 'ticket-collaborators', { where: { ticket: { equals: ticketId } }, overrideAccess: true })
84
+ .catch(() => ({ totalDocs: 0 }))
85
+ if (collabCount.totalDocs >= MAX_COLLABORATORS_PER_TICKET) {
86
+ return Response.json(
87
+ { error: `Limite de ${MAX_COLLABORATORS_PER_TICKET} collaborateurs atteinte sur ce ticket` },
88
+ { status: 429 },
89
+ )
90
+ }
91
+
71
92
  // Prevent inviting the primary owner (no-op)
72
93
  const ownerEmail =
73
94
  typeof ticket.client === 'object' ? (ticket.client?.email as string | undefined) : undefined
@@ -77,8 +98,7 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
77
98
 
78
99
  // Find-or-create the invited support-client
79
100
  let inviteeId: number | string | null = null
80
- const existing = await payload.find({
81
- collection: slugs.supportClients as any,
101
+ const existing = await dbFind(payload, slugs.supportClients, {
82
102
  where: { email: { equals: email } },
83
103
  limit: 1,
84
104
  depth: 0,
@@ -90,8 +110,7 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
90
110
  // Create a placeholder client; password is random — the invitee will reset
91
111
  // via the forgotPassword flow triggered by the SupportClients afterChange hook.
92
112
  const tempPassword = randomBytes(16).toString('hex')
93
- const created = await payload.create({
94
- collection: slugs.supportClients as any,
113
+ const created = await dbCreate(payload, slugs.supportClients, {
95
114
  data: {
96
115
  email,
97
116
  firstName: 'Invité',
@@ -111,8 +130,7 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
111
130
  }
112
131
 
113
132
  // Upsert ticket-collaborator row (one per (ticket, client))
114
- const dupe = await payload.find({
115
- collection: 'ticket-collaborators' as any,
133
+ const dupe = await dbFind(payload, 'ticket-collaborators', {
116
134
  where: {
117
135
  and: [
118
136
  { ticket: { equals: ticketId } },
@@ -127,15 +145,13 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
127
145
  const invitationToken = randomBytes(24).toString('hex')
128
146
 
129
147
  if (dupe.docs.length > 0) {
130
- await payload.update({
131
- collection: 'ticket-collaborators' as any,
148
+ await dbUpdate(payload, 'ticket-collaborators', {
132
149
  id: (dupe.docs[0] as any).id,
133
150
  data: { role, invitationToken },
134
151
  overrideAccess: true,
135
152
  })
136
153
  } else {
137
- await payload.create({
138
- collection: 'ticket-collaborators' as any,
154
+ await dbCreate(payload, 'ticket-collaborators', {
139
155
  data: {
140
156
  ticket: ticketId,
141
157
  client: inviteeId,
@@ -187,8 +203,7 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs): Endpoi
187
203
 
188
204
  // Best-effort email log
189
205
  try {
190
- await payload.create({
191
- collection: slugs.emailLogs as any,
206
+ await dbCreate(payload, slugs.emailLogs, {
192
207
  data: {
193
208
  status: 'success',
194
209
  action: 'invite',