services-as-software 2.1.1 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/package.json +14 -14
- package/tests/communication.test.ts +517 -0
- package/tests/metrics.test.ts +478 -0
- package/tests/pricing.test.ts +297 -0
- package/tests/service-definition.test.ts +208 -0
- package/tests/sla.test.ts +414 -0
- package/tests/workflow.test.ts +569 -0
- package/vitest.config.ts +12 -0
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 .org.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "services-as-software",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "Primitives for building AI-powered services that operate as software",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,18 +11,10 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsc",
|
|
16
|
-
"dev": "tsc --watch",
|
|
17
|
-
"test": "vitest",
|
|
18
|
-
"typecheck": "tsc --noEmit",
|
|
19
|
-
"lint": "eslint .",
|
|
20
|
-
"clean": "rm -rf dist"
|
|
21
|
-
},
|
|
22
14
|
"dependencies": {
|
|
23
|
-
"ai-database": "2.1.
|
|
24
|
-
"ai-functions": "2.1.
|
|
25
|
-
"digital-workers": "2.1.
|
|
15
|
+
"ai-database": "2.1.3",
|
|
16
|
+
"ai-functions": "2.1.3",
|
|
17
|
+
"digital-workers": "2.1.3"
|
|
26
18
|
},
|
|
27
19
|
"keywords": [
|
|
28
20
|
"services",
|
|
@@ -30,5 +22,13 @@
|
|
|
30
22
|
"ai",
|
|
31
23
|
"primitives"
|
|
32
24
|
],
|
|
33
|
-
"license": "MIT"
|
|
34
|
-
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"dev": "tsc --watch",
|
|
29
|
+
"test": "vitest",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint .",
|
|
32
|
+
"clean": "rm -rf dist"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Communication Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for automated client communication and notifications.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
8
|
+
import {
|
|
9
|
+
createCommunicationChannel,
|
|
10
|
+
sendNotification,
|
|
11
|
+
scheduleNotification,
|
|
12
|
+
createNotificationTemplate,
|
|
13
|
+
buildCommunicationWorkflow,
|
|
14
|
+
trackCommunication,
|
|
15
|
+
CommunicationType,
|
|
16
|
+
NotificationPriority,
|
|
17
|
+
} from '../src/communication/index.js'
|
|
18
|
+
|
|
19
|
+
describe('Client Communication Automation', () => {
|
|
20
|
+
describe('Communication Channels', () => {
|
|
21
|
+
it('should create an email channel', () => {
|
|
22
|
+
const channel = createCommunicationChannel({
|
|
23
|
+
type: 'email',
|
|
24
|
+
name: 'Customer Email',
|
|
25
|
+
config: {
|
|
26
|
+
from: 'support@service.com',
|
|
27
|
+
replyTo: 'help@service.com',
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
expect(channel.type).toBe('email')
|
|
32
|
+
expect(channel.config.from).toBe('support@service.com')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should create a Slack channel', () => {
|
|
36
|
+
const channel = createCommunicationChannel({
|
|
37
|
+
type: 'slack',
|
|
38
|
+
name: 'Customer Slack',
|
|
39
|
+
config: {
|
|
40
|
+
webhookUrl: 'https://hooks.slack.com/...',
|
|
41
|
+
defaultChannel: '#client-updates',
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(channel.type).toBe('slack')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should create an SMS channel', () => {
|
|
49
|
+
const channel = createCommunicationChannel({
|
|
50
|
+
type: 'sms',
|
|
51
|
+
name: 'Customer SMS',
|
|
52
|
+
config: {
|
|
53
|
+
provider: 'twilio',
|
|
54
|
+
fromNumber: '+15551234567',
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(channel.type).toBe('sms')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should create a webhook channel', () => {
|
|
62
|
+
const channel = createCommunicationChannel({
|
|
63
|
+
type: 'webhook',
|
|
64
|
+
name: 'Customer Webhook',
|
|
65
|
+
config: {
|
|
66
|
+
url: 'https://customer.com/webhooks/updates',
|
|
67
|
+
headers: { 'X-API-Key': '{{apiKey}}' },
|
|
68
|
+
method: 'POST',
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(channel.type).toBe('webhook')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('Notifications', () => {
|
|
77
|
+
it('should send a notification', async () => {
|
|
78
|
+
const sendMock = vi.fn().mockResolvedValue({ messageId: 'msg-123' })
|
|
79
|
+
|
|
80
|
+
const result = await sendNotification({
|
|
81
|
+
to: 'client@example.com',
|
|
82
|
+
channel: 'email',
|
|
83
|
+
subject: 'Project Update',
|
|
84
|
+
body: 'Your project has been completed.',
|
|
85
|
+
priority: 'normal',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(result.sent).toBe(true)
|
|
89
|
+
expect(result.messageId).toBeDefined()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should send notification with template', async () => {
|
|
93
|
+
const template = createNotificationTemplate({
|
|
94
|
+
id: 'project-complete',
|
|
95
|
+
name: 'Project Completion',
|
|
96
|
+
subject: 'Your {{projectName}} project is complete!',
|
|
97
|
+
body: `
|
|
98
|
+
Hi {{clientName}},
|
|
99
|
+
|
|
100
|
+
Great news! Your project "{{projectName}}" has been completed.
|
|
101
|
+
|
|
102
|
+
Deliverables:
|
|
103
|
+
{{#each deliverables}}
|
|
104
|
+
- {{this}}
|
|
105
|
+
{{/each}}
|
|
106
|
+
|
|
107
|
+
Please review and let us know if you have any questions.
|
|
108
|
+
|
|
109
|
+
Best regards,
|
|
110
|
+
{{teamName}}
|
|
111
|
+
`,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const result = await sendNotification({
|
|
115
|
+
to: 'client@example.com',
|
|
116
|
+
channel: 'email',
|
|
117
|
+
template: template.id,
|
|
118
|
+
variables: {
|
|
119
|
+
clientName: 'John',
|
|
120
|
+
projectName: 'Website Redesign',
|
|
121
|
+
deliverables: ['Homepage', 'About Page', 'Contact Form'],
|
|
122
|
+
teamName: 'Design Team',
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(result.sent).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should handle multiple recipients', async () => {
|
|
130
|
+
const result = await sendNotification({
|
|
131
|
+
to: ['client1@example.com', 'client2@example.com'],
|
|
132
|
+
channel: 'email',
|
|
133
|
+
subject: 'Team Update',
|
|
134
|
+
body: 'Project update for all stakeholders.',
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(result.recipients).toHaveLength(2)
|
|
138
|
+
expect(result.successCount).toBe(2)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should support priority levels', async () => {
|
|
142
|
+
const result = await sendNotification({
|
|
143
|
+
to: 'client@example.com',
|
|
144
|
+
channel: 'email',
|
|
145
|
+
subject: 'Urgent: Action Required',
|
|
146
|
+
body: 'Please review immediately.',
|
|
147
|
+
priority: 'urgent',
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
expect(result.priority).toBe('urgent')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should handle notification failures', async () => {
|
|
154
|
+
const result = await sendNotification({
|
|
155
|
+
to: 'invalid-email',
|
|
156
|
+
channel: 'email',
|
|
157
|
+
subject: 'Test',
|
|
158
|
+
body: 'Test',
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
expect(result.sent).toBe(false)
|
|
162
|
+
expect(result.error).toBeDefined()
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('Scheduled Notifications', () => {
|
|
167
|
+
it('should schedule a notification', async () => {
|
|
168
|
+
const scheduled = await scheduleNotification({
|
|
169
|
+
to: 'client@example.com',
|
|
170
|
+
channel: 'email',
|
|
171
|
+
subject: 'Weekly Summary',
|
|
172
|
+
body: 'Here is your weekly summary...',
|
|
173
|
+
scheduledFor: new Date('2024-01-15T09:00:00Z'),
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
expect(scheduled.id).toBeDefined()
|
|
177
|
+
expect(scheduled.status).toBe('scheduled')
|
|
178
|
+
expect(scheduled.scheduledFor).toEqual(new Date('2024-01-15T09:00:00Z'))
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should schedule recurring notifications', async () => {
|
|
182
|
+
const scheduled = await scheduleNotification({
|
|
183
|
+
to: 'client@example.com',
|
|
184
|
+
channel: 'email',
|
|
185
|
+
subject: 'Weekly Progress Report',
|
|
186
|
+
body: 'Here is this week\'s progress...',
|
|
187
|
+
schedule: {
|
|
188
|
+
type: 'recurring',
|
|
189
|
+
cron: '0 9 * * 1', // Every Monday at 9am
|
|
190
|
+
timezone: 'America/New_York',
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
expect(scheduled.recurring).toBe(true)
|
|
195
|
+
expect(scheduled.nextRun).toBeDefined()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should cancel scheduled notification', async () => {
|
|
199
|
+
const scheduled = await scheduleNotification({
|
|
200
|
+
to: 'client@example.com',
|
|
201
|
+
channel: 'email',
|
|
202
|
+
subject: 'Test',
|
|
203
|
+
body: 'Test',
|
|
204
|
+
scheduledFor: new Date('2024-12-31'),
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const result = await scheduled.cancel()
|
|
208
|
+
|
|
209
|
+
expect(result.cancelled).toBe(true)
|
|
210
|
+
expect(result.status).toBe('cancelled')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('Notification Templates', () => {
|
|
215
|
+
it('should create a notification template', () => {
|
|
216
|
+
const template = createNotificationTemplate({
|
|
217
|
+
id: 'welcome',
|
|
218
|
+
name: 'Welcome Email',
|
|
219
|
+
subject: 'Welcome to {{serviceName}}!',
|
|
220
|
+
body: 'Hi {{name}}, welcome aboard!',
|
|
221
|
+
variables: ['serviceName', 'name'],
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
expect(template.id).toBe('welcome')
|
|
225
|
+
expect(template.variables).toContain('name')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should support multiple channels', () => {
|
|
229
|
+
const template = createNotificationTemplate({
|
|
230
|
+
id: 'status-update',
|
|
231
|
+
name: 'Status Update',
|
|
232
|
+
channels: {
|
|
233
|
+
email: {
|
|
234
|
+
subject: 'Project Status: {{status}}',
|
|
235
|
+
body: 'Your project status has changed to {{status}}.',
|
|
236
|
+
},
|
|
237
|
+
slack: {
|
|
238
|
+
message: ':bell: Project status changed to *{{status}}*',
|
|
239
|
+
},
|
|
240
|
+
sms: {
|
|
241
|
+
message: 'Project status: {{status}}',
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
expect(template.channels.email).toBeDefined()
|
|
247
|
+
expect(template.channels.slack).toBeDefined()
|
|
248
|
+
expect(template.channels.sms).toBeDefined()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should validate template variables', () => {
|
|
252
|
+
const template = createNotificationTemplate({
|
|
253
|
+
id: 'test',
|
|
254
|
+
name: 'Test',
|
|
255
|
+
subject: 'Hello {{name}}',
|
|
256
|
+
body: 'Welcome {{name}}, your ID is {{userId}}',
|
|
257
|
+
validation: {
|
|
258
|
+
name: { required: true, type: 'string' },
|
|
259
|
+
userId: { required: true, type: 'string' },
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
expect(() => template.render({ name: 'John' })).toThrow() // missing userId
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should support conditional content', () => {
|
|
267
|
+
const template = createNotificationTemplate({
|
|
268
|
+
id: 'invoice',
|
|
269
|
+
name: 'Invoice',
|
|
270
|
+
body: `
|
|
271
|
+
Amount due: {{amount}}
|
|
272
|
+
{{#if overdue}}
|
|
273
|
+
This invoice is overdue. Please pay immediately.
|
|
274
|
+
{{/if}}
|
|
275
|
+
`,
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const rendered = template.render({
|
|
279
|
+
amount: '$100',
|
|
280
|
+
overdue: true,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
expect(rendered).toContain('overdue')
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe('Communication Workflows', () => {
|
|
288
|
+
it('should build a communication workflow', () => {
|
|
289
|
+
const workflow = buildCommunicationWorkflow({
|
|
290
|
+
name: 'Project Lifecycle',
|
|
291
|
+
triggers: [
|
|
292
|
+
{ event: 'project.started', template: 'project-started' },
|
|
293
|
+
{ event: 'milestone.completed', template: 'milestone-update' },
|
|
294
|
+
{ event: 'review.requested', template: 'review-request' },
|
|
295
|
+
{ event: 'project.completed', template: 'project-complete' },
|
|
296
|
+
],
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
expect(workflow.name).toBe('Project Lifecycle')
|
|
300
|
+
expect(workflow.triggers).toHaveLength(4)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('should handle conditional notifications', async () => {
|
|
304
|
+
const workflow = buildCommunicationWorkflow({
|
|
305
|
+
name: 'Conditional Workflow',
|
|
306
|
+
triggers: [
|
|
307
|
+
{
|
|
308
|
+
event: 'payment.received',
|
|
309
|
+
template: 'payment-receipt',
|
|
310
|
+
condition: (ctx) => ctx.amount > 0,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
event: 'payment.failed',
|
|
314
|
+
template: 'payment-failed',
|
|
315
|
+
condition: (ctx) => ctx.retryCount < 3,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const result = await workflow.process({
|
|
321
|
+
event: 'payment.received',
|
|
322
|
+
amount: 100,
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
expect(result.notificationSent).toBe(true)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('should support notification delays', async () => {
|
|
329
|
+
const workflow = buildCommunicationWorkflow({
|
|
330
|
+
name: 'Delayed Workflow',
|
|
331
|
+
triggers: [
|
|
332
|
+
{
|
|
333
|
+
event: 'trial.started',
|
|
334
|
+
template: 'trial-welcome',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
event: 'trial.started',
|
|
338
|
+
template: 'trial-day-3',
|
|
339
|
+
delay: '3d',
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
event: 'trial.started',
|
|
343
|
+
template: 'trial-ending',
|
|
344
|
+
delay: '12d',
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
const scheduled = await workflow.process({
|
|
350
|
+
event: 'trial.started',
|
|
351
|
+
userId: 'user-123',
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
expect(scheduled).toHaveLength(3)
|
|
355
|
+
expect(scheduled[1].delay).toBe('3d')
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('should cancel pending notifications on event', async () => {
|
|
359
|
+
const workflow = buildCommunicationWorkflow({
|
|
360
|
+
name: 'Cancellable Workflow',
|
|
361
|
+
triggers: [
|
|
362
|
+
{
|
|
363
|
+
event: 'reminder.scheduled',
|
|
364
|
+
template: 'reminder',
|
|
365
|
+
cancelOnEvents: ['task.completed', 'task.cancelled'],
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
// Schedule reminder
|
|
371
|
+
await workflow.process({
|
|
372
|
+
event: 'reminder.scheduled',
|
|
373
|
+
taskId: 'task-123',
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
// Complete task (should cancel reminder)
|
|
377
|
+
const result = await workflow.process({
|
|
378
|
+
event: 'task.completed',
|
|
379
|
+
taskId: 'task-123',
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
expect(result.cancelledNotifications).toHaveLength(1)
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
describe('Communication Tracking', () => {
|
|
387
|
+
it('should track sent communications', async () => {
|
|
388
|
+
await sendNotification({
|
|
389
|
+
to: 'client@example.com',
|
|
390
|
+
channel: 'email',
|
|
391
|
+
subject: 'Test',
|
|
392
|
+
body: 'Test',
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
const tracking = await trackCommunication.get({
|
|
396
|
+
recipient: 'client@example.com',
|
|
397
|
+
timeRange: '24h',
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
expect(tracking.messages).toHaveLength(1)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('should track email opens', async () => {
|
|
404
|
+
const result = await sendNotification({
|
|
405
|
+
to: 'client@example.com',
|
|
406
|
+
channel: 'email',
|
|
407
|
+
subject: 'Test',
|
|
408
|
+
body: 'Test',
|
|
409
|
+
tracking: {
|
|
410
|
+
opens: true,
|
|
411
|
+
clicks: true,
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// Simulate open
|
|
416
|
+
await trackCommunication.recordOpen(result.messageId)
|
|
417
|
+
|
|
418
|
+
const tracking = await trackCommunication.get({ messageId: result.messageId })
|
|
419
|
+
expect(tracking.opened).toBe(true)
|
|
420
|
+
expect(tracking.openedAt).toBeDefined()
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('should track link clicks', async () => {
|
|
424
|
+
const result = await sendNotification({
|
|
425
|
+
to: 'client@example.com',
|
|
426
|
+
channel: 'email',
|
|
427
|
+
subject: 'Test',
|
|
428
|
+
body: '<a href="https://example.com">Click here</a>',
|
|
429
|
+
tracking: { clicks: true },
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
// Simulate click
|
|
433
|
+
await trackCommunication.recordClick(result.messageId, 'https://example.com')
|
|
434
|
+
|
|
435
|
+
const tracking = await trackCommunication.get({ messageId: result.messageId })
|
|
436
|
+
expect(tracking.clicks).toHaveLength(1)
|
|
437
|
+
expect(tracking.clicks[0].url).toBe('https://example.com')
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('should generate communication analytics', async () => {
|
|
441
|
+
const analytics = await trackCommunication.getAnalytics({
|
|
442
|
+
channel: 'email',
|
|
443
|
+
timeRange: '30d',
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
expect(analytics.sent).toBeDefined()
|
|
447
|
+
expect(analytics.delivered).toBeDefined()
|
|
448
|
+
expect(analytics.opened).toBeDefined()
|
|
449
|
+
expect(analytics.clicked).toBeDefined()
|
|
450
|
+
expect(analytics.bounced).toBeDefined()
|
|
451
|
+
expect(analytics.openRate).toBeDefined()
|
|
452
|
+
expect(analytics.clickRate).toBeDefined()
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
describe('Client Preferences', () => {
|
|
457
|
+
it('should respect communication preferences', async () => {
|
|
458
|
+
const preferences = {
|
|
459
|
+
email: true,
|
|
460
|
+
sms: false,
|
|
461
|
+
slack: true,
|
|
462
|
+
preferredTime: '09:00-17:00',
|
|
463
|
+
timezone: 'America/New_York',
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const result = await sendNotification({
|
|
467
|
+
to: 'client@example.com',
|
|
468
|
+
channels: ['email', 'sms', 'slack'],
|
|
469
|
+
subject: 'Update',
|
|
470
|
+
body: 'Your update',
|
|
471
|
+
respectPreferences: true,
|
|
472
|
+
clientPreferences: preferences,
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
expect(result.channelsUsed).toContain('email')
|
|
476
|
+
expect(result.channelsUsed).toContain('slack')
|
|
477
|
+
expect(result.channelsUsed).not.toContain('sms')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('should handle do-not-disturb', async () => {
|
|
481
|
+
const result = await sendNotification({
|
|
482
|
+
to: 'client@example.com',
|
|
483
|
+
channel: 'email',
|
|
484
|
+
subject: 'Test',
|
|
485
|
+
body: 'Test',
|
|
486
|
+
priority: 'normal',
|
|
487
|
+
clientPreferences: {
|
|
488
|
+
doNotDisturb: {
|
|
489
|
+
enabled: true,
|
|
490
|
+
hours: '22:00-08:00',
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
currentTime: new Date('2024-01-15T23:00:00'),
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
expect(result.queued).toBe(true)
|
|
497
|
+
expect(result.queuedUntil).toBeDefined()
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('should allow urgent notifications to bypass preferences', async () => {
|
|
501
|
+
const result = await sendNotification({
|
|
502
|
+
to: 'client@example.com',
|
|
503
|
+
channel: 'sms',
|
|
504
|
+
subject: 'Urgent',
|
|
505
|
+
body: 'Urgent message',
|
|
506
|
+
priority: 'urgent',
|
|
507
|
+
clientPreferences: {
|
|
508
|
+
sms: false,
|
|
509
|
+
},
|
|
510
|
+
bypassPreferencesForUrgent: true,
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
expect(result.sent).toBe(true)
|
|
514
|
+
expect(result.bypassedPreferences).toBe(true)
|
|
515
|
+
})
|
|
516
|
+
})
|
|
517
|
+
})
|