@consilioweb/payload-support 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +656 -355
  2. package/dist/components/RichTextEditor/index.d.ts +19 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.d.ts +6 -0
  4. package/dist/components/TicketConversation/SkeletonText.d.ts +4 -0
  5. package/dist/components/TicketConversation/components/AISummaryPanel.d.ts +14 -0
  6. package/dist/components/TicketConversation/components/ActionPanels.d.ts +47 -0
  7. package/dist/components/TicketConversation/components/ActivityLog.d.ts +5 -0
  8. package/dist/components/TicketConversation/components/ClientBar.d.ts +5 -0
  9. package/dist/components/TicketConversation/components/ClientHistory.d.ts +24 -0
  10. package/dist/components/TicketConversation/components/CodeBlock.d.ts +26 -0
  11. package/dist/components/TicketConversation/components/CodeBlockInserter.d.ts +12 -0
  12. package/dist/components/TicketConversation/components/QuickActions.d.ts +32 -0
  13. package/dist/components/TicketConversation/components/TicketHeader.d.ts +19 -0
  14. package/dist/components/TicketConversation/components/TimeTrackingPanel.d.ts +24 -0
  15. package/dist/components/TicketConversation/config.d.ts +10 -0
  16. package/dist/components/TicketConversation/config.js +1 -40
  17. package/dist/components/TicketConversation/constants.d.ts +56 -0
  18. package/dist/components/TicketConversation/context.d.ts +53 -0
  19. package/dist/components/TicketConversation/hooks/useAI.d.ts +21 -0
  20. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.d.ts +3 -0
  21. package/dist/components/TicketConversation/hooks/useFeatures.d.ts +23 -0
  22. package/dist/components/TicketConversation/hooks/useFeatures.js +61 -0
  23. package/dist/components/TicketConversation/hooks/useMessageActions.d.ts +20 -0
  24. package/dist/components/TicketConversation/hooks/useReply.d.ts +27 -0
  25. package/dist/components/TicketConversation/hooks/useTicketActions.d.ts +58 -0
  26. package/dist/components/TicketConversation/hooks/useTimeTracking.d.ts +18 -0
  27. package/dist/components/TicketConversation/hooks/useTranslation.d.ts +33 -0
  28. package/dist/components/TicketConversation/hooks/useTranslation.js +22 -3
  29. package/dist/components/TicketConversation/index.d.ts +4 -0
  30. package/dist/components/TicketConversation/index.js +2 -2
  31. package/dist/components/TicketConversation/types.d.ts +56 -0
  32. package/dist/components/TicketConversation/utils.d.ts +2 -0
  33. package/dist/index.cjs +392 -320
  34. package/dist/index.d.cts +100 -3
  35. package/dist/index.d.ts +100 -3
  36. package/dist/index.js +388 -321
  37. package/dist/utils/features.d.ts +123 -0
  38. package/dist/utils/features.js +147 -0
  39. package/dist/views/BillingView/client.d.ts +2 -0
  40. package/dist/views/BillingView/index.d.ts +4 -0
  41. package/dist/views/ChatView/client.d.ts +2 -0
  42. package/dist/views/ChatView/client.js +9 -9
  43. package/dist/views/ChatView/index.d.ts +4 -0
  44. package/dist/views/CrmView/client.d.ts +2 -0
  45. package/dist/views/CrmView/client.js +6 -9
  46. package/dist/views/CrmView/index.d.ts +4 -0
  47. package/dist/views/EmailTrackingView/client.d.ts +2 -0
  48. package/dist/views/EmailTrackingView/index.d.ts +4 -0
  49. package/dist/views/ImportConversationView/client.d.ts +2 -0
  50. package/dist/views/ImportConversationView/index.d.ts +4 -0
  51. package/dist/views/LogsView/client.d.ts +2 -0
  52. package/dist/views/LogsView/index.d.ts +4 -0
  53. package/dist/views/NewTicketView/client.d.ts +2 -0
  54. package/dist/views/NewTicketView/index.d.ts +4 -0
  55. package/dist/views/PendingEmailsView/client.d.ts +2 -0
  56. package/dist/views/PendingEmailsView/index.d.ts +4 -0
  57. package/dist/views/SupportDashboardView/client.d.ts +2 -0
  58. package/dist/views/SupportDashboardView/client.js +1 -1
  59. package/dist/views/SupportDashboardView/index.d.ts +4 -0
  60. package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
  61. package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
  62. package/dist/views/TicketDetailView/client.d.ts +2 -0
  63. package/dist/views/TicketDetailView/client.js +2 -2
  64. package/dist/views/TicketDetailView/constants.d.ts +13 -0
  65. package/dist/views/TicketDetailView/helpers.d.ts +2 -0
  66. package/dist/views/TicketDetailView/index.d.ts +4 -0
  67. package/dist/views/TicketDetailView/types.d.ts +40 -0
  68. package/dist/views/TicketInboxView/client.d.ts +2 -0
  69. package/dist/views/TicketInboxView/client.js +5 -5
  70. package/dist/views/TicketInboxView/index.d.ts +4 -0
  71. package/dist/views/TicketingSettingsView/client.d.ts +2 -0
  72. package/dist/views/TicketingSettingsView/client.js +6 -5
  73. package/dist/views/TicketingSettingsView/index.d.ts +4 -0
  74. package/dist/views/TimeDashboardView/client.d.ts +2 -0
  75. package/dist/views/TimeDashboardView/index.d.ts +4 -0
  76. package/dist/views/shared/AdminViewHeader.d.ts +13 -0
  77. package/dist/views/shared/ErrorBoundary.d.ts +17 -0
  78. package/dist/views/shared/Skeleton.d.ts +21 -0
  79. package/dist/views/shared/StatusPill.d.ts +8 -0
  80. package/dist/views/shared/adminTokens.d.ts +19 -0
  81. package/dist/views/shared/config.d.ts +8 -0
  82. package/dist/views/shared/config.js +1 -40
  83. package/dist/views/shared/dateLocale.d.ts +5 -0
  84. package/dist/views/shared/index.d.ts +12 -0
  85. package/dist/views/shared/index.js +2 -1
  86. package/dist/views/shared/locales/en.json +708 -0
  87. package/dist/views/shared/locales/fr.json +708 -0
  88. package/dist/views/shared/sla.d.ts +14 -0
  89. package/dist/views.d.ts +13 -13
  90. package/package.json +26 -41
  91. package/src/collections/SupportClients.ts +29 -0
  92. package/src/collections/TicketCollaborators.ts +6 -1
  93. package/src/collections/TicketMessages.ts +46 -24
  94. package/src/collections/Tickets.ts +117 -79
  95. package/src/collections/TimeEntries.ts +57 -32
  96. package/src/components/TicketConversation/config.ts +16 -80
  97. package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
  98. package/src/components/TicketConversation/hooks/useTranslation.ts +38 -4
  99. package/src/components/TicketConversation/index.tsx +2 -2
  100. package/src/endpoints/ai.ts +2 -1
  101. package/src/endpoints/client-intelligence.ts +2 -1
  102. package/src/endpoints/process-scheduled.ts +13 -8
  103. package/src/endpoints/round-robin-config.ts +30 -20
  104. package/src/endpoints/settings.ts +28 -77
  105. package/src/index.ts +6 -2
  106. package/src/portal/LiveChat.tsx +33 -22
  107. package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
  108. package/src/types/lucide-react.d.ts +10 -2
  109. package/src/utils/aiAgent.ts +2 -1
  110. package/src/utils/aiProvider.ts +21 -0
  111. package/src/utils/features.ts +297 -0
  112. package/src/utils/fireWebhooks.ts +6 -0
  113. package/src/utils/generateTicketSynthesis.ts +2 -1
  114. package/src/utils/index.ts +4 -2
  115. package/src/utils/readSettings.ts +82 -9
  116. package/src/utils/webhookDispatcher.ts +19 -7
  117. package/src/views/ChatView/client.tsx +13 -9
  118. package/src/views/CrmView/client.tsx +10 -11
  119. package/src/views/SupportDashboardView/client.tsx +1 -1
  120. package/src/views/TicketDetailView/client.tsx +2 -2
  121. package/src/views/TicketInboxView/client.tsx +10 -5
  122. package/src/views/TicketingSettingsView/client.tsx +9 -7
  123. package/src/views/shared/config.ts +14 -80
  124. package/src/views/shared/index.ts +3 -1
  125. package/src/views/shared/locales/en.json +2 -2
  126. package/src/views/shared/locales/fr.json +2 -2
  127. package/dist/components/RichTextEditor/index.cjs +0 -279
  128. package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
  129. package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
  130. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
  131. package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
  132. package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
  133. package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
  134. package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
  135. package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
  136. package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
  137. package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
  138. package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
  139. package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
  140. package/dist/components/TicketConversation/config.cjs +0 -44
  141. package/dist/components/TicketConversation/constants.cjs +0 -127
  142. package/dist/components/TicketConversation/context.cjs +0 -13
  143. package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
  144. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
  145. package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
  146. package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
  147. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
  148. package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
  149. package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -78
  150. package/dist/components/TicketConversation/index.cjs +0 -1114
  151. package/dist/components/TicketConversation/types.cjs +0 -2
  152. package/dist/components/TicketConversation/utils.cjs +0 -27
  153. package/dist/views/BillingView/client.cjs +0 -469
  154. package/dist/views/BillingView/index.cjs +0 -34
  155. package/dist/views/ChatView/client.cjs +0 -301
  156. package/dist/views/ChatView/index.cjs +0 -34
  157. package/dist/views/CrmView/client.cjs +0 -458
  158. package/dist/views/CrmView/index.cjs +0 -34
  159. package/dist/views/EmailTrackingView/client.cjs +0 -171
  160. package/dist/views/EmailTrackingView/index.cjs +0 -34
  161. package/dist/views/ImportConversationView/client.cjs +0 -238
  162. package/dist/views/ImportConversationView/index.cjs +0 -34
  163. package/dist/views/LogsView/client.cjs +0 -148
  164. package/dist/views/LogsView/index.cjs +0 -32
  165. package/dist/views/NewTicketView/client.cjs +0 -213
  166. package/dist/views/NewTicketView/index.cjs +0 -32
  167. package/dist/views/PendingEmailsView/client.cjs +0 -471
  168. package/dist/views/PendingEmailsView/index.cjs +0 -34
  169. package/dist/views/SupportDashboardView/client.cjs +0 -437
  170. package/dist/views/SupportDashboardView/index.cjs +0 -34
  171. package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
  172. package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
  173. package/dist/views/TicketDetailView/client.cjs +0 -1412
  174. package/dist/views/TicketDetailView/constants.cjs +0 -20
  175. package/dist/views/TicketDetailView/helpers.cjs +0 -23
  176. package/dist/views/TicketDetailView/index.cjs +0 -34
  177. package/dist/views/TicketDetailView/types.cjs +0 -2
  178. package/dist/views/TicketInboxView/client.cjs +0 -359
  179. package/dist/views/TicketInboxView/index.cjs +0 -32
  180. package/dist/views/TicketingSettingsView/client.cjs +0 -740
  181. package/dist/views/TicketingSettingsView/index.cjs +0 -34
  182. package/dist/views/TimeDashboardView/client.cjs +0 -180
  183. package/dist/views/TimeDashboardView/index.cjs +0 -34
  184. package/dist/views/shared/AdminViewHeader.cjs +0 -68
  185. package/dist/views/shared/ErrorBoundary.cjs +0 -55
  186. package/dist/views/shared/Skeleton.cjs +0 -77
  187. package/dist/views/shared/StatusPill.cjs +0 -47
  188. package/dist/views/shared/adminTokens.cjs +0 -34
  189. package/dist/views/shared/config.cjs +0 -44
  190. package/dist/views/shared/dateLocale.cjs +0 -5
  191. package/dist/views/shared/index.cjs +0 -72
  192. package/dist/views/shared/sla.cjs +0 -37
  193. package/dist/views.cjs +0 -86
  194. package/dist/views.d.cts +0 -13
  195. package/src/__tests__/aiSummaryRendering.test.ts +0 -21
  196. package/src/__tests__/authResponses.test.ts +0 -69
  197. package/src/__tests__/capabilities.test.ts +0 -71
  198. package/src/__tests__/generateTrackingToken.test.ts +0 -67
  199. package/src/__tests__/integration/ai-agent.test.ts +0 -46
  200. package/src/__tests__/integration/auth.test.ts +0 -43
  201. package/src/__tests__/integration/automation-rules.test.ts +0 -68
  202. package/src/__tests__/integration/buildTestPayload.ts +0 -52
  203. package/src/__tests__/integration/channels.test.ts +0 -52
  204. package/src/__tests__/integration/core-behaviors.test.ts +0 -81
  205. package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
  206. package/src/__tests__/integration/digest.test.ts +0 -48
  207. package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
  208. package/src/__tests__/integration/isolation.test.ts +0 -81
  209. package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
  210. package/src/__tests__/integration/nps.test.ts +0 -37
  211. package/src/__tests__/integration/per-team-sla.test.ts +0 -44
  212. package/src/__tests__/integration/push.test.ts +0 -75
  213. package/src/__tests__/integration/sanitization.test.ts +0 -35
  214. package/src/__tests__/integration/sla-pause.test.ts +0 -39
  215. package/src/__tests__/integration/smoke.test.ts +0 -26
  216. package/src/__tests__/integration/snooze.test.ts +0 -68
  217. package/src/__tests__/integration/teams.test.ts +0 -38
  218. package/src/__tests__/rateLimiter.test.ts +0 -83
  219. package/src/__tests__/sanitizeHtml.test.ts +0 -165
  220. package/src/__tests__/sla.test.ts +0 -258
  221. package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
  222. package/src/__tests__/webhookSecurity.test.ts +0 -58
@@ -1,37 +0,0 @@
1
- 'use strict';
2
-
3
- const HOUR_MS = 36e5;
4
- const MINUTE_MS = 6e4;
5
- function computeSlaState(t, now = /* @__PURE__ */ new Date()) {
6
- const hasFirstResponse = !!t.firstResponseAt;
7
- const dueRaw = hasFirstResponse ? t.slaResolutionDue : t.slaFirstResponseDue;
8
- const breached = hasFirstResponse ? !!t.slaResolutionBreached : !!t.slaFirstResponseBreached;
9
- if (!dueRaw) return { state: "none", remainingMs: null, due: null };
10
- const dueDate = new Date(dueRaw);
11
- if (Number.isNaN(dueDate.getTime())) return { state: "none", remainingMs: null, due: null };
12
- const remaining = dueDate.getTime() - now.getTime();
13
- if (breached || remaining < 0) {
14
- return { state: "breach", remainingMs: remaining, due: dueRaw };
15
- }
16
- if (remaining < HOUR_MS) {
17
- return { state: "warn", remainingMs: remaining, due: dueRaw };
18
- }
19
- return { state: "ok", remainingMs: remaining, due: dueRaw };
20
- }
21
- function formatSlaRemaining(remainingMs) {
22
- if (remainingMs == null) return "\u2014";
23
- const abs = Math.abs(remainingMs);
24
- const sign = remainingMs < 0 ? "\u2212" : "";
25
- if (abs < HOUR_MS) {
26
- const m2 = Math.floor(abs / MINUTE_MS);
27
- return `${sign}${m2}m`;
28
- }
29
- const h = Math.floor(abs / HOUR_MS);
30
- const m = Math.floor(abs % HOUR_MS / MINUTE_MS);
31
- if (h < 24) return `${sign}${h}h${m > 0 ? ` ${String(m).padStart(2, "0")}` : ""}`;
32
- const d = Math.floor(h / 24);
33
- return `${sign}${d}j`;
34
- }
35
-
36
- exports.computeSlaState = computeSlaState;
37
- exports.formatSlaRemaining = formatSlaRemaining;
package/dist/views.cjs DELETED
@@ -1,86 +0,0 @@
1
- 'use strict';
2
-
3
- var index$a = require('./views/TicketInboxView/index');
4
- var index$9 = require('./views/TicketDetailView/index');
5
- var index$8 = require('./views/SupportDashboardView/index');
6
- var index$6 = require('./views/NewTicketView/index');
7
- var index$b = require('./views/TicketingSettingsView/index');
8
- var index$5 = require('./views/LogsView/index');
9
- var index$1 = require('./views/ChatView/index');
10
- var index$2 = require('./views/CrmView/index');
11
- var index$7 = require('./views/PendingEmailsView/index');
12
- var index$3 = require('./views/EmailTrackingView/index');
13
- var index = require('./views/BillingView/index');
14
- var index$c = require('./views/TimeDashboardView/index');
15
- var index$4 = require('./views/ImportConversationView/index');
16
-
17
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
-
19
- var index__default$a = /*#__PURE__*/_interopDefault(index$a);
20
- var index__default$9 = /*#__PURE__*/_interopDefault(index$9);
21
- var index__default$8 = /*#__PURE__*/_interopDefault(index$8);
22
- var index__default$6 = /*#__PURE__*/_interopDefault(index$6);
23
- var index__default$b = /*#__PURE__*/_interopDefault(index$b);
24
- var index__default$5 = /*#__PURE__*/_interopDefault(index$5);
25
- var index__default$1 = /*#__PURE__*/_interopDefault(index$1);
26
- var index__default$2 = /*#__PURE__*/_interopDefault(index$2);
27
- var index__default$7 = /*#__PURE__*/_interopDefault(index$7);
28
- var index__default$3 = /*#__PURE__*/_interopDefault(index$3);
29
- var index__default = /*#__PURE__*/_interopDefault(index);
30
- var index__default$c = /*#__PURE__*/_interopDefault(index$c);
31
- var index__default$4 = /*#__PURE__*/_interopDefault(index$4);
32
-
33
-
34
-
35
- Object.defineProperty(exports, "TicketInboxView", {
36
- enumerable: true,
37
- get: function () { return index__default$a.default; }
38
- });
39
- Object.defineProperty(exports, "TicketDetailView", {
40
- enumerable: true,
41
- get: function () { return index__default$9.default; }
42
- });
43
- Object.defineProperty(exports, "SupportDashboardView", {
44
- enumerable: true,
45
- get: function () { return index__default$8.default; }
46
- });
47
- Object.defineProperty(exports, "NewTicketView", {
48
- enumerable: true,
49
- get: function () { return index__default$6.default; }
50
- });
51
- Object.defineProperty(exports, "TicketingSettingsView", {
52
- enumerable: true,
53
- get: function () { return index__default$b.default; }
54
- });
55
- Object.defineProperty(exports, "LogsView", {
56
- enumerable: true,
57
- get: function () { return index__default$5.default; }
58
- });
59
- Object.defineProperty(exports, "ChatView", {
60
- enumerable: true,
61
- get: function () { return index__default$1.default; }
62
- });
63
- Object.defineProperty(exports, "CrmView", {
64
- enumerable: true,
65
- get: function () { return index__default$2.default; }
66
- });
67
- Object.defineProperty(exports, "PendingEmailsView", {
68
- enumerable: true,
69
- get: function () { return index__default$7.default; }
70
- });
71
- Object.defineProperty(exports, "EmailTrackingView", {
72
- enumerable: true,
73
- get: function () { return index__default$3.default; }
74
- });
75
- Object.defineProperty(exports, "BillingView", {
76
- enumerable: true,
77
- get: function () { return index__default.default; }
78
- });
79
- Object.defineProperty(exports, "TimeDashboardView", {
80
- enumerable: true,
81
- get: function () { return index__default$c.default; }
82
- });
83
- Object.defineProperty(exports, "ImportConversationView", {
84
- enumerable: true,
85
- get: function () { return index__default$4.default; }
86
- });
package/dist/views.d.cts DELETED
@@ -1,13 +0,0 @@
1
- export { default as TicketInboxView } from './views/TicketInboxView/index';
2
- export { default as TicketDetailView } from './views/TicketDetailView/index';
3
- export { default as SupportDashboardView } from './views/SupportDashboardView/index';
4
- export { default as NewTicketView } from './views/NewTicketView/index';
5
- export { default as TicketingSettingsView } from './views/TicketingSettingsView/index';
6
- export { default as LogsView } from './views/LogsView/index';
7
- export { default as ChatView } from './views/ChatView/index';
8
- export { default as CrmView } from './views/CrmView/index';
9
- export { default as PendingEmailsView } from './views/PendingEmailsView/index';
10
- export { default as EmailTrackingView } from './views/EmailTrackingView/index';
11
- export { default as BillingView } from './views/BillingView/index';
12
- export { default as TimeDashboardView } from './views/TimeDashboardView/index';
13
- export { default as ImportConversationView } from './views/ImportConversationView/index';
@@ -1,21 +0,0 @@
1
- import React from 'react'
2
- import { describe, expect, it } from 'vitest'
3
- import { renderAiSummary } from '../components/TicketConversation/components/AISummaryPanel'
4
-
5
- describe('AI summary rendering', () => {
6
- it('escapes model-provided HTML instead of interpreting it', () => {
7
- const nodes = renderAiSummary('<img src=x onerror=alert(1)>')
8
- expect(nodes).toEqual(['<img src=x onerror=alert(1)>'])
9
- expect(React.isValidElement(nodes[0])).toBe(false)
10
- })
11
-
12
- it('preserves bold markdown and line breaks without raw HTML', () => {
13
- const nodes = renderAiSummary('**Résumé**\nSuite')
14
- expect(React.isValidElement(nodes[0])).toBe(true)
15
- expect((nodes[0] as React.ReactElement).type).toBe('strong')
16
- expect((nodes[0] as React.ReactElement<{ children: string }>).props.children).toBe('Résumé')
17
- expect(React.isValidElement(nodes[1])).toBe(true)
18
- expect((nodes[1] as React.ReactElement).type).toBe('br')
19
- expect(nodes[2]).toBe('Suite')
20
- })
21
- })
@@ -1,69 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
- import { createLoginEndpoint } from '../endpoints/login'
3
- import { createOAuthGoogleEndpoint } from '../endpoints/oauth-google'
4
- import { resolveSlugs } from '../utils/slugs'
5
-
6
- const slugs = resolveSlugs()
7
-
8
- afterEach(() => {
9
- vi.restoreAllMocks()
10
- })
11
-
12
- describe('authentication responses', () => {
13
- it('keeps the login JWT exclusively in the HttpOnly cookie', async () => {
14
- const endpoint = createLoginEndpoint(slugs)
15
- const response = await endpoint.handler({
16
- headers: new Headers(),
17
- json: async () => ({ email: 'client@example.com', password: 'secret' }),
18
- payload: {
19
- login: vi.fn(async () => ({ token: 'jwt-secret', exp: 123, user: { id: 1 } })),
20
- create: vi.fn(async () => ({})),
21
- },
22
- } as never)
23
-
24
- expect(response.headers.get('set-cookie')).toContain('payload-token=jwt-secret')
25
- expect(response.headers.get('set-cookie')).toContain('HttpOnly')
26
- await expect(response.json()).resolves.toEqual({
27
- message: 'Login successful',
28
- user: { id: 1 },
29
- exp: 123,
30
- })
31
- })
32
-
33
- it('keeps the OAuth JWT exclusively in the HttpOnly cookie', async () => {
34
- process.env.GOOGLE_OAUTH_CLIENT_ID = 'google-client'
35
- process.env.GOOGLE_OAUTH_CLIENT_SECRET = 'google-secret'
36
- process.env.NEXT_PUBLIC_SERVER_URL = 'https://example.test'
37
- process.env.PAYLOAD_SECRET = 'payload-secret'
38
-
39
- vi.spyOn(globalThis, 'fetch')
40
- .mockResolvedValueOnce(new Response(JSON.stringify({ access_token: 'google-token' })))
41
- .mockResolvedValueOnce(new Response(JSON.stringify({
42
- id: 'google-id',
43
- email: 'client@example.com',
44
- verified_email: true,
45
- })))
46
-
47
- const endpoint = createOAuthGoogleEndpoint(slugs)
48
- const response = await endpoint.handler({
49
- json: async () => ({ code: 'code', state: 'state', cookieState: 'state' }),
50
- payload: {
51
- collections: {
52
- [slugs.supportClients]: {
53
- config: { auth: { tokenExpiration: 7200 }, fields: [] },
54
- },
55
- },
56
- find: vi.fn(async () => ({
57
- docs: [{ id: 1, email: 'client@example.com' }],
58
- totalDocs: 1,
59
- })),
60
- },
61
- } as never)
62
-
63
- expect(response.status).toBe(200)
64
- expect(response.headers.get('set-cookie')).toMatch(/^payload-token=/)
65
- const body = await response.json() as Record<string, unknown>
66
- expect(body).not.toHaveProperty('token')
67
- expect(body).toHaveProperty('user')
68
- })
69
- })
@@ -1,71 +0,0 @@
1
- import type { Config, Field } from 'payload'
2
- import { describe, expect, it, vi } from 'vitest'
3
- import { supportPlugin } from '../plugin'
4
-
5
- function fieldNames(fields: Field[]): string[] {
6
- return fields.flatMap((field) => {
7
- const own = 'name' in field && typeof field.name === 'string' ? [field.name] : []
8
- const nested = 'fields' in field && Array.isArray(field.fields) ? fieldNames(field.fields) : []
9
- return [...own, ...nested]
10
- })
11
- }
12
-
13
- function configuredPlugin() {
14
- return supportPlugin({
15
- capabilities: {
16
- aiSummaries: { generate: vi.fn(async () => undefined) },
17
- aiTitles: { generate: vi.fn(async () => undefined) },
18
- detailedBilling: true,
19
- inboundEmail: {
20
- secret: 'test-secret',
21
- handle: vi.fn(async () => Response.json({ success: true })),
22
- },
23
- projectSuggestions: {
24
- suggest: vi.fn(async () => Response.json({ suggestions: [] })),
25
- },
26
- sms: {
27
- adapter: { send: vi.fn(async () => ({ sent: true })) },
28
- },
29
- threadCleanup: { clean: vi.fn(async () => undefined) },
30
- volunteering: true,
31
- },
32
- rateLimitStore: 'payload',
33
- })({ collections: [], endpoints: [] } as unknown as Config) as Config
34
- }
35
-
36
- describe('Support capabilities', () => {
37
- it('owns the optional ticket, message and client fields', () => {
38
- const config = configuredPlugin()
39
- const collection = (slug: string) => config.collections?.find((item) => item.slug === slug)
40
-
41
- expect(fieldNames(collection('tickets')!.fields)).toEqual(expect.arrayContaining([
42
- 'billingLines',
43
- 'billedAt',
44
- 'displayTitle',
45
- 'displayTitleStatus',
46
- 'volunteer',
47
- 'volunteerValue',
48
- 'waitingSince',
49
- ]))
50
- expect(fieldNames(collection('ticket-messages')!.fields)).toEqual(expect.arrayContaining([
51
- 'notifyBySms',
52
- 'smsTo',
53
- 'smsMessage',
54
- 'isMarkedAsNoise',
55
- 'emailBodyOriginal',
56
- ]))
57
- expect(fieldNames(collection('support-clients')!.fields)).toEqual(expect.arrayContaining([
58
- 'notifyBySmsChannel',
59
- 'preferredFormality',
60
- 'preferredTone',
61
- ]))
62
- })
63
-
64
- it('registers protected deployment adapter endpoints in the module', () => {
65
- const paths = configuredPlugin().endpoints?.map((endpoint) => endpoint.path)
66
- expect(paths).toContain('/support-webhook/inbound-email')
67
- expect(paths).toContain('/support/suggest-projects')
68
- expect(paths).toContain('/support/ticket-title')
69
- expect(paths).toContain('/support/generate-missing-titles')
70
- })
71
- })
@@ -1,67 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { generateTrackingToken, verifyTrackingToken } from '../endpoints/track-open'
3
-
4
- describe('generateTrackingToken — determinism', () => {
5
- it('returns the same token for identical inputs (deterministic)', () => {
6
- const a = generateTrackingToken('42', '7', 'mysecret')
7
- const b = generateTrackingToken('42', '7', 'mysecret')
8
- expect(a).toBe(b)
9
- })
10
-
11
- it('returns the complete SHA-256 HMAC', () => {
12
- const token = generateTrackingToken('1', '2', 'secret')
13
- expect(token).toHaveLength(64)
14
- })
15
-
16
- it('returns only hex characters (0-9 a-f)', () => {
17
- const token = generateTrackingToken('100', '200', 'anothersecret')
18
- expect(token).toMatch(/^[0-9a-f]{64}$/)
19
- })
20
- })
21
-
22
- describe('verifyTrackingToken', () => {
23
- it('accepts the matching ticket and message signature', () => {
24
- const token = generateTrackingToken('42', '7', 'secret')
25
- expect(verifyTrackingToken('42', '7', token, 'secret')).toBe(true)
26
- })
27
-
28
- it('rejects malformed and mismatched signatures without throwing', () => {
29
- const token = generateTrackingToken('42', '7', 'secret')
30
- expect(verifyTrackingToken('42', '8', token, 'secret')).toBe(false)
31
- expect(verifyTrackingToken('42', '7', 'not-hex', 'secret')).toBe(false)
32
- expect(verifyTrackingToken('42', '7', token.slice(2), 'secret')).toBe(false)
33
- })
34
- })
35
-
36
- describe('generateTrackingToken — sensitivity to inputs', () => {
37
- it('produces different tokens for different ticketIds', () => {
38
- const t1 = generateTrackingToken('1', '5', 'secret')
39
- const t2 = generateTrackingToken('2', '5', 'secret')
40
- expect(t1).not.toBe(t2)
41
- })
42
-
43
- it('produces different tokens for different messageIds', () => {
44
- const t1 = generateTrackingToken('10', '1', 'secret')
45
- const t2 = generateTrackingToken('10', '2', 'secret')
46
- expect(t1).not.toBe(t2)
47
- })
48
-
49
- it('produces different tokens for different secrets', () => {
50
- const t1 = generateTrackingToken('42', '7', 'secret-A')
51
- const t2 = generateTrackingToken('42', '7', 'secret-B')
52
- expect(t1).not.toBe(t2)
53
- })
54
-
55
- it('is sensitive to ticketId/messageId order (no commutative collision)', () => {
56
- const t1 = generateTrackingToken('1', '23', 'secret')
57
- const t2 = generateTrackingToken('12', '3', 'secret')
58
- // '1:23' vs '12:3' — different strings → different HMACs
59
- expect(t1).not.toBe(t2)
60
- })
61
-
62
- it('treats ticket "0" and messageId "0" differently from "00"/"00"', () => {
63
- const t1 = generateTrackingToken('0', '0', 'secret')
64
- const t2 = generateTrackingToken('00', '00', 'secret')
65
- expect(t1).not.toBe(t2)
66
- })
67
- })
@@ -1,46 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { buildTestPayload } from './buildTestPayload'
3
- import { applyAgentDecision } from '../../utils/aiAgent'
4
- import { resolveSlugs } from '../../utils/slugs'
5
-
6
- const slugs = resolveSlugs({ users: 'users' })
7
- const PW = 'test-pw-12345'
8
-
9
- /**
10
- * Autonomous AI agent (P2 #8). The LLM decision is runtime-only; here we test the
11
- * DETERMINISTIC execution of a decision: a `reply` posts a public AI answer, an
12
- * `escalate` flags the ticket and records an internal note.
13
- */
14
- describe('AI agent — decision execution (P2 #8)', () => {
15
- it('reply decision posts a public answer attributed to the AI assistant', async () => {
16
- const payload = await buildTestPayload()
17
- const c = await payload.create({ collection: 'support-clients', data: { email: 'aiagent@example.com', password: PW, firstName: 'A', lastName: 'I', company: 'C' }, overrideAccess: true })
18
- const t = await payload.create({ collection: 'tickets', data: { subject: 'reset password', client: c.id, status: 'open', priority: 'normal' }, overrideAccess: true })
19
-
20
- const res = await applyAgentDecision(payload, slugs, t.id, { action: 'reply', reply: 'Cliquez sur « Mot de passe oublié » sur la page de connexion.', confidence: 0.95 })
21
- expect(res.acted).toBe('reply')
22
- expect(res.messageId).toBeTruthy()
23
-
24
- const msgs = await payload.find({ collection: 'ticket-messages', where: { ticket: { equals: t.id } }, overrideAccess: true })
25
- const aiMsg = msgs.docs.find((m) => (m as { fromAlias?: string }).fromAlias === 'Assistant IA') as { body?: string; isInternal?: boolean } | undefined
26
- expect(aiMsg).toBeTruthy()
27
- expect(aiMsg?.body).toContain('Mot de passe oublié')
28
- expect(aiMsg?.isInternal).toBe(false)
29
- }, 60_000)
30
-
31
- it('escalate decision flags the ticket and adds an internal note', async () => {
32
- const payload = await buildTestPayload()
33
- const c = await payload.create({ collection: 'support-clients', data: { email: 'aiagent2@example.com', password: PW, firstName: 'A', lastName: 'I', company: 'C' }, overrideAccess: true })
34
- const t = await payload.create({ collection: 'tickets', data: { subject: 'legal question', client: c.id, status: 'open', priority: 'normal' }, overrideAccess: true })
35
-
36
- const res = await applyAgentDecision(payload, slugs, t.id, { action: 'escalate', reason: 'Question juridique', confidence: 0.2 })
37
- expect(res.acted).toBe('escalate')
38
-
39
- const reread = await payload.findByID({ collection: 'tickets', id: t.id, overrideAccess: true })
40
- expect(reread.status).toBe('escalated')
41
-
42
- const msgs = await payload.find({ collection: 'ticket-messages', where: { and: [{ ticket: { equals: t.id } }, { isInternal: { equals: true } }] }, overrideAccess: true })
43
- const note = msgs.docs.find((m) => ((m as { body?: string }).body || '').includes('[Agent IA]'))
44
- expect(note).toBeTruthy()
45
- }, 60_000)
46
- })
@@ -1,43 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { buildTestPayload } from './buildTestPayload'
3
-
4
- const PW = 'test-pw-12345'
5
-
6
- /**
7
- * Validates the server-side 2FA enforcement (P0-2): the beforeLogin hook must
8
- * block login when 2FA is enabled without a fresh verification marker, allow it
9
- * with one, and consume the marker (single-use) so it can't be replayed.
10
- */
11
- describe('2FA enforcement (P0-2)', () => {
12
- it('blocks login without a fresh marker, allows with one, then consumes it', async () => {
13
- const payload = await buildTestPayload()
14
-
15
- const c = await payload.create({
16
- collection: 'support-clients',
17
- data: { email: '2fa@example.com', password: PW, firstName: 'T', lastName: 'F', company: 'C', twoFactorEnabled: true },
18
- overrideAccess: true,
19
- })
20
-
21
- // No marker → blocked even with the correct password.
22
- await expect(
23
- payload.login({ collection: 'support-clients', data: { email: '2fa@example.com', password: PW } }),
24
- ).rejects.toThrow(/2FA_REQUIRED/)
25
-
26
- // Fresh marker → login succeeds.
27
- await payload.update({ collection: 'support-clients', id: c.id, data: { twoFactorVerifiedAt: new Date().toISOString() }, overrideAccess: true })
28
- const ok = await payload.login({ collection: 'support-clients', data: { email: '2fa@example.com', password: PW } })
29
- expect(ok.token).toBeTruthy()
30
-
31
- // Marker is single-use → the next login is blocked again.
32
- await expect(
33
- payload.login({ collection: 'support-clients', data: { email: '2fa@example.com', password: PW } }),
34
- ).rejects.toThrow(/2FA_REQUIRED/)
35
- }, 60_000)
36
-
37
- it('lets a non-2FA client log in normally', async () => {
38
- const payload = await buildTestPayload()
39
- await payload.create({ collection: 'support-clients', data: { email: 'no2fa@example.com', password: PW, firstName: 'N', lastName: 'O', company: 'C' }, overrideAccess: true })
40
- const ok = await payload.login({ collection: 'support-clients', data: { email: 'no2fa@example.com', password: PW } })
41
- expect(ok.token).toBeTruthy()
42
- }, 60_000)
43
- })
@@ -1,68 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { buildTestPayload } from './buildTestPayload'
3
-
4
- const PW = 'test-pw-12345'
5
-
6
- /**
7
- * Automation rules engine (P2 #7): when a ticket event fires and the rule's
8
- * conditions match, its actions are applied — in a single guarded update (no loop).
9
- */
10
- describe('automation rules engine (P2 #7)', () => {
11
- it('applies actions when conditions match, leaves non-matching tickets untouched', async () => {
12
- const payload = await buildTestPayload()
13
- await payload.create({
14
- collection: 'automation-rules',
15
- data: {
16
- name: 'Bugs are urgent',
17
- enabled: true,
18
- event: 'ticket_created',
19
- matchType: 'all',
20
- order: 0,
21
- conditions: [{ field: 'category', operator: 'equals', value: 'bug' }],
22
- actions: [{ type: 'set_priority', value: 'urgent' }],
23
- },
24
- overrideAccess: true,
25
- })
26
- const c = await payload.create({ collection: 'support-clients', data: { email: 'auto@example.com', password: PW, firstName: 'A', lastName: 'U', company: 'C' }, overrideAccess: true })
27
-
28
- // Matching ticket → priority upgraded to urgent by the rule.
29
- const bug = await payload.create({ collection: 'tickets', data: { subject: 'crash', client: c.id, status: 'open', priority: 'normal', category: 'bug' }, overrideAccess: true })
30
- const bugRead = await payload.findByID({ collection: 'tickets', id: bug.id, overrideAccess: true })
31
- expect(bugRead.priority).toBe('urgent')
32
-
33
- // Non-matching ticket → unchanged.
34
- const q = await payload.create({ collection: 'tickets', data: { subject: 'how to', client: c.id, status: 'open', priority: 'normal', category: 'question' }, overrideAccess: true })
35
- const qRead = await payload.findByID({ collection: 'tickets', id: q.id, overrideAccess: true })
36
- expect(qRead.priority).toBe('normal')
37
- }, 60_000)
38
-
39
- it('supports multiple actions and the "any" match type', async () => {
40
- const payload = await buildTestPayload()
41
- await payload.create({
42
- collection: 'automation-rules',
43
- data: {
44
- name: 'Triage hosting/feature',
45
- enabled: true,
46
- event: 'ticket_created',
47
- matchType: 'any',
48
- order: 0,
49
- conditions: [
50
- { field: 'category', operator: 'equals', value: 'hosting' },
51
- { field: 'subject', operator: 'contains', value: 'urgent' },
52
- ],
53
- actions: [
54
- { type: 'set_status', value: 'escalated' },
55
- { type: 'set_priority', value: 'high' },
56
- ],
57
- },
58
- overrideAccess: true,
59
- })
60
- const c = await payload.create({ collection: 'support-clients', data: { email: 'auto2@example.com', password: PW, firstName: 'A', lastName: 'U', company: 'C' }, overrideAccess: true })
61
-
62
- // Matches via subject contains "urgent" (any) → both actions applied.
63
- const t = await payload.create({ collection: 'tickets', data: { subject: 'this is URGENT please', client: c.id, status: 'open', priority: 'low', category: 'question' }, overrideAccess: true })
64
- const r = await payload.findByID({ collection: 'tickets', id: t.id, overrideAccess: true })
65
- expect(r.status).toBe('escalated')
66
- expect(r.priority).toBe('high')
67
- }, 60_000)
68
- })
@@ -1,52 +0,0 @@
1
- import { buildConfig, getPayload, type Payload, type CollectionConfig, type SanitizedConfig } from 'payload'
2
- import { sqliteAdapter } from '@payloadcms/db-sqlite'
3
- import { lexicalEditor } from '@payloadcms/richtext-lexical'
4
- import sharp from 'sharp'
5
- import { supportPlugin } from '../../plugin'
6
-
7
- /**
8
- * Boots a real Payload instance backed by an in-memory SQLite database, with the
9
- * support plugin applied. Used by integration tests to exercise access control,
10
- * hooks and endpoints for real (the unit tests only cover pure functions).
11
- *
12
- * `skipViews: true` avoids loading the admin view components (which resolve to the
13
- * built dist subpath and need bundling) — integration tests use the Local API only.
14
- */
15
-
16
- // Minimal host collections the plugin relies on.
17
- const Users: CollectionConfig = {
18
- slug: 'users',
19
- auth: true,
20
- fields: [{ name: 'name', type: 'text' }],
21
- }
22
-
23
- const Media: CollectionConfig = {
24
- slug: 'media',
25
- upload: { staticDir: '/tmp/payload-support-test-media' },
26
- fields: [{ name: 'alt', type: 'text' }],
27
- }
28
-
29
- let cached: Promise<Payload> | null = null
30
-
31
- export function buildTestPayload(): Promise<Payload> {
32
- if (cached) return cached
33
- process.env.PAYLOAD_SECRET = process.env.PAYLOAD_SECRET || 'test-secret-payload-support-integration'
34
- cached = (async () => {
35
- const config: SanitizedConfig = await buildConfig({
36
- secret: process.env.PAYLOAD_SECRET as string,
37
- db: sqliteAdapter({ client: { url: ':memory:' } }),
38
- editor: lexicalEditor(),
39
- collections: [Users, Media],
40
- plugins: [
41
- supportPlugin({
42
- features: { chat: false, pendingEmails: false },
43
- skipViews: true,
44
- }),
45
- ],
46
- sharp,
47
- telemetry: false,
48
- })
49
- return getPayload({ config })
50
- })()
51
- return cached
52
- }
@@ -1,52 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { buildTestPayload } from './buildTestPayload'
3
- import { createChannelsWebhookEndpoint } from '../../endpoints/channels'
4
- import { resolveSlugs } from '../../utils/slugs'
5
-
6
- const slugs = resolveSlugs({ users: 'users' })
7
-
8
- /**
9
- * Social channels (roadmap): the inbound webhook turns a normalized WhatsApp /
10
- * Messenger message into a ticket (creating the channel client on first contact)
11
- * and appends follow-ups to the same open ticket.
12
- */
13
- describe('social channels inbound webhook (roadmap)', () => {
14
- it('creates a ticket from a WhatsApp message, then appends follow-ups', async () => {
15
- const payload = await buildTestPayload()
16
- process.env.CHANNELS_WEBHOOK_SECRET = 'test-channel-secret'
17
- const ep = createChannelsWebhookEndpoint(slugs)
18
- const post = (payloadBody: unknown) =>
19
- (ep.handler as (r: unknown) => Promise<Response>)({
20
- payload,
21
- headers: new Headers({ 'x-channel-secret': 'test-channel-secret' }),
22
- json: async () => payloadBody,
23
- })
24
-
25
- const res1 = await post({ provider: 'whatsapp', from: '33612345678', name: 'Jean', text: "Bonjour, j'ai besoin d'aide" })
26
- const j1 = await res1.json()
27
- expect(j1.ok).toBe(true)
28
- expect(j1.createdTicket).toBe(true)
29
-
30
- const ticket = await payload.findByID({ collection: 'tickets', id: j1.ticketId, overrideAccess: true })
31
- expect(ticket.source).toBe('whatsapp')
32
-
33
- const msgs1 = await payload.find({ collection: 'ticket-messages', where: { ticket: { equals: j1.ticketId } }, overrideAccess: true })
34
- expect(msgs1.docs.some((m) => ((m as { body?: string }).body || '').includes("besoin d'aide"))).toBe(true)
35
-
36
- // Follow-up from the same sender → same ticket, no new ticket.
37
- const res2 = await post({ provider: 'whatsapp', from: '33612345678', text: 'Toujours bloqué' })
38
- const j2 = await res2.json()
39
- expect(j2.createdTicket).toBe(false)
40
- expect(String(j2.ticketId)).toBe(String(j1.ticketId))
41
- }, 60_000)
42
-
43
- it('rejects without the channel secret', async () => {
44
- const payload = await buildTestPayload()
45
- process.env.CHANNELS_WEBHOOK_SECRET = 'test-channel-secret'
46
- const ep = createChannelsWebhookEndpoint(slugs)
47
- const res = await (ep.handler as (r: unknown) => Promise<Response>)({
48
- payload, headers: new Headers({}), json: async () => ({ provider: 'whatsapp', from: 'x', text: 'y' }),
49
- })
50
- expect(res.status).toBe(401)
51
- }, 60_000)
52
- })