@consilioweb/payload-support 0.16.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 (193) 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/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 +1604 -671
  13. package/dist/index.d.cts +3 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +1602 -671
  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 +23 -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/sanitization.test.ts +35 -0
  71. package/src/__tests__/integration/sla-pause.test.ts +39 -0
  72. package/src/__tests__/integration/smoke.test.ts +26 -0
  73. package/src/__tests__/integration/snooze.test.ts +68 -0
  74. package/src/__tests__/integration/teams.test.ts +38 -0
  75. package/src/__tests__/rateLimiter.test.ts +89 -0
  76. package/src/__tests__/sanitizeHtml.test.ts +165 -0
  77. package/src/__tests__/sla.test.ts +258 -0
  78. package/src/collections/AutomationRules.ts +66 -0
  79. package/src/collections/NotificationQueue.ts +30 -0
  80. package/src/collections/SatisfactionSurveys.ts +11 -2
  81. package/src/collections/SupportClients.ts +58 -1
  82. package/src/collections/SupportTeam.ts +27 -0
  83. package/src/collections/TicketActivityLog.ts +9 -4
  84. package/src/collections/TicketMessages.ts +129 -31
  85. package/src/collections/Tickets.ts +31 -73
  86. package/src/collections/index.ts +3 -0
  87. package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
  88. package/src/components/TicketConversation/SkeletonText.tsx +23 -0
  89. package/src/components/TicketConversation/constants.ts +29 -0
  90. package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
  91. package/src/components/TicketConversation/index.tsx +4 -152
  92. package/src/endpoints/admin-chat-stream.ts +3 -4
  93. package/src/endpoints/admin-chat.ts +11 -20
  94. package/src/endpoints/admin-stats.ts +70 -32
  95. package/src/endpoints/ai-agent.ts +36 -0
  96. package/src/endpoints/apply-macro.ts +8 -14
  97. package/src/endpoints/auth-2fa.ts +12 -12
  98. package/src/endpoints/auto-close.ts +6 -10
  99. package/src/endpoints/billing.ts +17 -4
  100. package/src/endpoints/bulk-action.ts +7 -12
  101. package/src/endpoints/channels.ts +89 -0
  102. package/src/endpoints/chat-stream.ts +3 -4
  103. package/src/endpoints/chat.ts +7 -12
  104. package/src/endpoints/chatbot.ts +2 -2
  105. package/src/endpoints/client-intelligence.ts +4 -4
  106. package/src/endpoints/delete-account.ts +9 -16
  107. package/src/endpoints/email-stats.ts +2 -2
  108. package/src/endpoints/escalate.ts +2 -2
  109. package/src/endpoints/export-csv.ts +2 -2
  110. package/src/endpoints/export-data.ts +5 -8
  111. package/src/endpoints/import-conversation.ts +6 -10
  112. package/src/endpoints/index.ts +18 -1
  113. package/src/endpoints/invite-collaborator.ts +30 -15
  114. package/src/endpoints/invoice.ts +127 -0
  115. package/src/endpoints/kb-search.ts +2 -2
  116. package/src/endpoints/login.ts +12 -4
  117. package/src/endpoints/merge-clients.ts +14 -19
  118. package/src/endpoints/merge-tickets.ts +11 -18
  119. package/src/endpoints/oauth-google.ts +86 -39
  120. package/src/endpoints/pending-emails-process.ts +15 -25
  121. package/src/endpoints/process-digests.ts +93 -0
  122. package/src/endpoints/process-scheduled.ts +6 -10
  123. package/src/endpoints/process-snooze.ts +78 -0
  124. package/src/endpoints/purge-logs.ts +2 -2
  125. package/src/endpoints/resend-notification.ts +3 -4
  126. package/src/endpoints/round-robin-config.ts +3 -4
  127. package/src/endpoints/satisfaction.ts +21 -14
  128. package/src/endpoints/search.ts +5 -8
  129. package/src/endpoints/seed-kb.ts +3 -4
  130. package/src/endpoints/send-reminder.ts +6 -9
  131. package/src/endpoints/settings.ts +5 -2
  132. package/src/endpoints/signature.ts +3 -4
  133. package/src/endpoints/sla-check.ts +2 -2
  134. package/src/endpoints/split-ticket.ts +7 -12
  135. package/src/endpoints/statuses.ts +2 -2
  136. package/src/endpoints/ticket-feedback.ts +4 -6
  137. package/src/endpoints/ticket-synthesis.ts +2 -2
  138. package/src/endpoints/track-open.ts +16 -28
  139. package/src/endpoints/transfer-ticket.ts +21 -9
  140. package/src/endpoints/user-prefs.ts +3 -4
  141. package/src/hooks/automationRules.ts +84 -0
  142. package/src/hooks/checkSLA.ts +48 -16
  143. package/src/plugin.ts +6 -0
  144. package/src/portal/LiveChat.tsx +2 -63
  145. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
  146. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
  147. package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
  148. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
  149. package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
  150. package/src/portal/auth/tickets/detail/page.tsx +2 -2
  151. package/src/portal/helpers.ts +46 -0
  152. package/src/portal/login/page.tsx +3 -8
  153. package/src/portal/types.ts +15 -0
  154. package/src/types/lucide-react.d.ts +34 -0
  155. package/src/types/payload-config.d.ts +10 -0
  156. package/src/utils/adminNotification.ts +2 -2
  157. package/src/utils/aiAgent.ts +147 -0
  158. package/src/utils/auth.ts +5 -4
  159. package/src/utils/channels.ts +36 -0
  160. package/src/utils/db.ts +73 -0
  161. package/src/utils/emailTemplate.ts +8 -1
  162. package/src/utils/fireWebhooks.ts +4 -6
  163. package/src/utils/generateTicketSynthesis.ts +7 -12
  164. package/src/utils/invoicePdf.ts +88 -0
  165. package/src/utils/notificationQueue.ts +36 -0
  166. package/src/utils/readSettings.ts +22 -6
  167. package/src/utils/sanitizeHtml.ts +57 -0
  168. package/src/utils/slugs.ts +6 -0
  169. package/src/utils/teamAccess.ts +27 -0
  170. package/src/utils/ticketAccess.ts +53 -0
  171. package/src/utils/webhookDispatcher.ts +4 -6
  172. package/src/views/BillingView/client.tsx +49 -37
  173. package/src/views/ChatView/client.tsx +4 -3
  174. package/src/views/CrmView/client.tsx +7 -7
  175. package/src/views/EmailTrackingView/client.tsx +7 -6
  176. package/src/views/ImportConversationView/client.tsx +4 -4
  177. package/src/views/LogsView/client.tsx +3 -2
  178. package/src/views/NewTicketView/client.tsx +1 -1
  179. package/src/views/PendingEmailsView/client.tsx +7 -6
  180. package/src/views/SupportDashboardView/client.tsx +21 -21
  181. package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
  182. package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
  183. package/src/views/TicketDetailView/client.tsx +54 -179
  184. package/src/views/TicketDetailView/constants.ts +23 -0
  185. package/src/views/TicketDetailView/helpers.ts +17 -0
  186. package/src/views/TicketDetailView/types.ts +10 -0
  187. package/src/views/TicketInboxView/client.tsx +16 -10
  188. package/src/views/TicketingSettingsView/client.tsx +90 -87
  189. package/src/views/TimeDashboardView/client.tsx +2 -2
  190. package/src/views/shared/StatusPill.tsx +24 -12
  191. package/src/views/shared/dateLocale.ts +5 -0
  192. package/src/views/shared/locales/en.json +708 -0
  193. package/src/views/shared/locales/fr.json +708 -0
@@ -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 || 'payload-support-2fa'
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.find({
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.update({
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.find({
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.update({
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.update({
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.find({
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.update({
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.find({
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.create({
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.update({
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.
@@ -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.find({
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.find({
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.update({
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.update({
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.update({
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.update({
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.update({
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.delete({
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
+ }
@@ -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,