@bernierllc/email-webhook-events 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 (97) hide show
  1. package/.eslintrc.cjs +29 -0
  2. package/README.md +349 -0
  3. package/__tests__/EmailWebhookEventsService.test.ts +247 -0
  4. package/__tests__/analytics/AnalyticsEngine.test.ts +244 -0
  5. package/__tests__/normalizers/MailgunNormalizer.test.ts +149 -0
  6. package/__tests__/normalizers/PostmarkNormalizer.test.ts +112 -0
  7. package/__tests__/normalizers/SESNormalizer.test.ts +168 -0
  8. package/__tests__/normalizers/SMTP2GONormalizer.test.ts +83 -0
  9. package/__tests__/normalizers/SendGridNormalizer.test.ts +181 -0
  10. package/__tests__/persistence/PersistenceAdapter.test.ts +103 -0
  11. package/coverage/clover.xml +328 -0
  12. package/coverage/coverage-final.json +10 -0
  13. package/coverage/lcov-report/base.css +224 -0
  14. package/coverage/lcov-report/block-navigation.js +87 -0
  15. package/coverage/lcov-report/favicon.png +0 -0
  16. package/coverage/lcov-report/index.html +161 -0
  17. package/coverage/lcov-report/prettify.css +1 -0
  18. package/coverage/lcov-report/prettify.js +2 -0
  19. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  20. package/coverage/lcov-report/sorter.js +210 -0
  21. package/coverage/lcov-report/src/EmailWebhookEventsService.ts.html +826 -0
  22. package/coverage/lcov-report/src/analytics/AnalyticsEngine.ts.html +775 -0
  23. package/coverage/lcov-report/src/analytics/index.html +116 -0
  24. package/coverage/lcov-report/src/index.html +131 -0
  25. package/coverage/lcov-report/src/normalizers/MailgunNormalizer.ts.html +301 -0
  26. package/coverage/lcov-report/src/normalizers/PostmarkNormalizer.ts.html +301 -0
  27. package/coverage/lcov-report/src/normalizers/SESNormalizer.ts.html +436 -0
  28. package/coverage/lcov-report/src/normalizers/SMTP2GONormalizer.ts.html +247 -0
  29. package/coverage/lcov-report/src/normalizers/SendGridNormalizer.ts.html +274 -0
  30. package/coverage/lcov-report/src/normalizers/index.html +176 -0
  31. package/coverage/lcov-report/src/persistence/InMemoryPersistenceAdapter.ts.html +289 -0
  32. package/coverage/lcov-report/src/persistence/index.html +116 -0
  33. package/coverage/lcov-report/src/types.ts.html +823 -0
  34. package/coverage/lcov.info +710 -0
  35. package/dist/EmailWebhookEventsService.d.ts +53 -0
  36. package/dist/EmailWebhookEventsService.d.ts.map +1 -0
  37. package/dist/EmailWebhookEventsService.js +198 -0
  38. package/dist/EmailWebhookEventsService.js.map +1 -0
  39. package/dist/analytics/AnalyticsEngine.d.ts +20 -0
  40. package/dist/analytics/AnalyticsEngine.d.ts.map +1 -0
  41. package/dist/analytics/AnalyticsEngine.js +160 -0
  42. package/dist/analytics/AnalyticsEngine.js.map +1 -0
  43. package/dist/index.d.ts +12 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +31 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/normalizers/EventNormalizer.d.ts +5 -0
  48. package/dist/normalizers/EventNormalizer.d.ts.map +1 -0
  49. package/dist/normalizers/EventNormalizer.js +10 -0
  50. package/dist/normalizers/EventNormalizer.js.map +1 -0
  51. package/dist/normalizers/MailgunNormalizer.d.ts +9 -0
  52. package/dist/normalizers/MailgunNormalizer.d.ts.map +1 -0
  53. package/dist/normalizers/MailgunNormalizer.js +73 -0
  54. package/dist/normalizers/MailgunNormalizer.js.map +1 -0
  55. package/dist/normalizers/PostmarkNormalizer.d.ts +10 -0
  56. package/dist/normalizers/PostmarkNormalizer.d.ts.map +1 -0
  57. package/dist/normalizers/PostmarkNormalizer.js +75 -0
  58. package/dist/normalizers/PostmarkNormalizer.js.map +1 -0
  59. package/dist/normalizers/SESNormalizer.d.ts +16 -0
  60. package/dist/normalizers/SESNormalizer.d.ts.map +1 -0
  61. package/dist/normalizers/SESNormalizer.js +107 -0
  62. package/dist/normalizers/SESNormalizer.js.map +1 -0
  63. package/dist/normalizers/SMTP2GONormalizer.d.ts +9 -0
  64. package/dist/normalizers/SMTP2GONormalizer.d.ts.map +1 -0
  65. package/dist/normalizers/SMTP2GONormalizer.js +55 -0
  66. package/dist/normalizers/SMTP2GONormalizer.js.map +1 -0
  67. package/dist/normalizers/SendGridNormalizer.d.ts +9 -0
  68. package/dist/normalizers/SendGridNormalizer.d.ts.map +1 -0
  69. package/dist/normalizers/SendGridNormalizer.js +64 -0
  70. package/dist/normalizers/SendGridNormalizer.js.map +1 -0
  71. package/dist/persistence/InMemoryPersistenceAdapter.d.ts +12 -0
  72. package/dist/persistence/InMemoryPersistenceAdapter.d.ts.map +1 -0
  73. package/dist/persistence/InMemoryPersistenceAdapter.js +58 -0
  74. package/dist/persistence/InMemoryPersistenceAdapter.js.map +1 -0
  75. package/dist/persistence/PersistenceAdapter.d.ts +7 -0
  76. package/dist/persistence/PersistenceAdapter.d.ts.map +1 -0
  77. package/dist/persistence/PersistenceAdapter.js +10 -0
  78. package/dist/persistence/PersistenceAdapter.js.map +1 -0
  79. package/dist/types.d.ts +178 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +41 -0
  82. package/dist/types.js.map +1 -0
  83. package/jest.config.cjs +29 -0
  84. package/package.json +51 -0
  85. package/src/EmailWebhookEventsService.ts +247 -0
  86. package/src/analytics/AnalyticsEngine.ts +230 -0
  87. package/src/index.ts +39 -0
  88. package/src/normalizers/EventNormalizer.ts +13 -0
  89. package/src/normalizers/MailgunNormalizer.ts +72 -0
  90. package/src/normalizers/PostmarkNormalizer.ts +72 -0
  91. package/src/normalizers/SESNormalizer.ts +117 -0
  92. package/src/normalizers/SMTP2GONormalizer.ts +54 -0
  93. package/src/normalizers/SendGridNormalizer.ts +63 -0
  94. package/src/persistence/InMemoryPersistenceAdapter.ts +68 -0
  95. package/src/persistence/PersistenceAdapter.ts +15 -0
  96. package/src/types.ts +246 -0
  97. package/tsconfig.json +31 -0
package/.eslintrc.cjs ADDED
@@ -0,0 +1,29 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ module.exports = {
10
+ parser: '@typescript-eslint/parser',
11
+ parserOptions: {
12
+ ecmaVersion: 2020,
13
+ sourceType: 'module',
14
+ project: './tsconfig.json',
15
+ },
16
+ plugins: ['@typescript-eslint'],
17
+ extends: [
18
+ 'eslint:recommended',
19
+ 'plugin:@typescript-eslint/recommended',
20
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking',
21
+ ],
22
+ rules: {
23
+ '@typescript-eslint/explicit-function-return-type': 'warn',
24
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
25
+ '@typescript-eslint/no-explicit-any': 'error',
26
+ '@typescript-eslint/strict-boolean-expressions': 'off',
27
+ },
28
+ ignorePatterns: ['dist/', 'node_modules/', '*.cjs'],
29
+ };
package/README.md ADDED
@@ -0,0 +1,349 @@
1
+ # @bernierllc/email-webhook-events
2
+
3
+ Email event tracking, normalization, and analytics service for processing webhooks from all email providers (SendGrid, Mailgun, AWS SES, Postmark, SMTP2GO). Provides unified event format, real-time notifications, and comprehensive analytics aggregation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bernierllc/email-webhook-events
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Setup
14
+
15
+ ```typescript
16
+ import { EmailWebhookEventsService, EmailProvider } from '@bernierllc/email-webhook-events';
17
+
18
+ const service = new EmailWebhookEventsService({
19
+ providers: [
20
+ {
21
+ provider: EmailProvider.SENDGRID,
22
+ enabled: true,
23
+ webhookUrl: '/webhooks/sendgrid',
24
+ secretKey: process.env.SENDGRID_WEBHOOK_SECRET!,
25
+ signatureHeader: 'X-Twilio-Email-Event-Webhook-Signature',
26
+ signatureAlgorithm: 'sha256'
27
+ },
28
+ {
29
+ provider: EmailProvider.MAILGUN,
30
+ enabled: true,
31
+ webhookUrl: '/webhooks/mailgun',
32
+ secretKey: process.env.MAILGUN_WEBHOOK_SECRET!,
33
+ signatureHeader: 'X-Mailgun-Signature',
34
+ signatureAlgorithm: 'sha256'
35
+ }
36
+ ],
37
+ persistence: {
38
+ enabled: true,
39
+ adapter: 'memory' // or 'supabase', 'postgresql', 'mongodb'
40
+ },
41
+ analytics: {
42
+ realTimeEnabled: true,
43
+ aggregationInterval: 5000,
44
+ retentionDays: 90
45
+ },
46
+ notifications: {
47
+ enabled: true,
48
+ criticalEvents: ['bounced', 'complained', 'failed'],
49
+ channels: [
50
+ { type: 'neverhub', config: {} }
51
+ ]
52
+ }
53
+ });
54
+
55
+ await service.initialize();
56
+ ```
57
+
58
+ ### Processing Webhooks
59
+
60
+ ```typescript
61
+ // Express.js example
62
+ app.post('/webhooks/sendgrid', async (req, res) => {
63
+ const webhook = {
64
+ provider: EmailProvider.SENDGRID,
65
+ signature: req.headers['x-twilio-email-event-webhook-signature'],
66
+ payload: req.body,
67
+ headers: req.headers
68
+ };
69
+
70
+ const result = await service.processWebhook(webhook);
71
+
72
+ if (result.success) {
73
+ res.status(200).json({ message: 'Event processed' });
74
+ } else {
75
+ res.status(400).json({ error: result.error });
76
+ }
77
+ });
78
+ ```
79
+
80
+ ### Querying Analytics
81
+
82
+ ```typescript
83
+ // Get analytics for the last 30 days
84
+ const startDate = new Date();
85
+ startDate.setDate(startDate.getDate() - 30);
86
+ const endDate = new Date();
87
+
88
+ const analytics = await service.getAnalytics(startDate, endDate);
89
+
90
+ console.log(`Delivery rate: ${(analytics.deliveryRate * 100).toFixed(2)}%`);
91
+ console.log(`Open rate: ${(analytics.openRate * 100).toFixed(2)}%`);
92
+ console.log(`Click rate: ${(analytics.clickRate * 100).toFixed(2)}%`);
93
+ console.log(`Bounce rate: ${(analytics.bounceRate * 100).toFixed(2)}%`);
94
+
95
+ // Filter by provider
96
+ const sendgridAnalytics = await service.getAnalytics(
97
+ startDate,
98
+ endDate,
99
+ { provider: EmailProvider.SENDGRID }
100
+ );
101
+
102
+ // Filter by event type
103
+ const bounceAnalytics = await service.getAnalytics(
104
+ startDate,
105
+ endDate,
106
+ { eventTypes: ['bounced'] }
107
+ );
108
+ ```
109
+
110
+ ### Querying Events
111
+
112
+ ```typescript
113
+ // Get all events for a specific email
114
+ const emailEvents = await service.getEventsForEmail('email-123');
115
+
116
+ // Query events with filters
117
+ const events = await service.queryEvents({
118
+ recipient: 'user@example.com',
119
+ eventTypes: ['opened', 'clicked'],
120
+ startDate: new Date('2025-01-01'),
121
+ endDate: new Date('2025-12-31'),
122
+ limit: 100
123
+ });
124
+ ```
125
+
126
+ ## API Reference
127
+
128
+ ### EmailWebhookEventsService
129
+
130
+ #### `constructor(config: EmailWebhookEventsConfig)`
131
+
132
+ Creates a new EmailWebhookEventsService instance.
133
+
134
+ **Parameters:**
135
+ - `config.providers` - Array of provider configurations
136
+ - `config.persistence` - Persistence settings (optional)
137
+ - `config.analytics` - Analytics settings (optional)
138
+ - `config.notifications` - Notification settings (optional)
139
+ - `config.rateLimiting` - Rate limiting settings (optional)
140
+
141
+ #### `async initialize(): Promise<void>`
142
+
143
+ Initializes the service. Must be called before processing webhooks.
144
+
145
+ #### `async processWebhook(webhook: WebhookPayload): Promise<EmailWebhookResult>`
146
+
147
+ Processes an incoming webhook from an email provider.
148
+
149
+ **Parameters:**
150
+ - `webhook.provider` - Email provider (sendgrid, mailgun, aws_ses, etc.)
151
+ - `webhook.signature` - Webhook signature for validation
152
+ - `webhook.payload` - Raw webhook payload
153
+ - `webhook.headers` - Request headers
154
+
155
+ **Returns:** `EmailWebhookResult` with success status and normalized event
156
+
157
+ #### `async getAnalytics(startDate: Date, endDate: Date, filters?: AnalyticsFilters): Promise<EmailAnalytics>`
158
+
159
+ Retrieves analytics for the specified date range.
160
+
161
+ **Parameters:**
162
+ - `startDate` - Start of date range
163
+ - `endDate` - End of date range
164
+ - `filters` - Optional filters (provider, emailId, recipient, eventTypes)
165
+
166
+ **Returns:** `EmailAnalytics` with counts, rates, and breakdowns
167
+
168
+ #### `async queryEvents(query: EventQuery): Promise<EmailEvent[]>`
169
+
170
+ Queries events with various filters.
171
+
172
+ **Parameters:**
173
+ - `query.emailId` - Filter by email ID
174
+ - `query.recipient` - Filter by recipient
175
+ - `query.eventTypes` - Filter by event types
176
+ - `query.startDate` - Filter by start date
177
+ - `query.endDate` - Filter by end date
178
+ - `query.limit` - Maximum number of results
179
+ - `query.offset` - Offset for pagination
180
+
181
+ **Returns:** Array of `EmailEvent`
182
+
183
+ #### `async getEventsForEmail(emailId: string): Promise<EmailEvent[]>`
184
+
185
+ Gets all events for a specific email.
186
+
187
+ **Parameters:**
188
+ - `emailId` - Email identifier
189
+
190
+ **Returns:** Array of `EmailEvent`
191
+
192
+ ## Supported Providers
193
+
194
+ ### SendGrid
195
+
196
+ ```typescript
197
+ {
198
+ provider: EmailProvider.SENDGRID,
199
+ signatureHeader: 'X-Twilio-Email-Event-Webhook-Signature',
200
+ signatureAlgorithm: 'sha256'
201
+ }
202
+ ```
203
+
204
+ **Supported Events:** delivered, open, click, bounce, spamreport, unsubscribe, deferred, dropped
205
+
206
+ ### Mailgun
207
+
208
+ ```typescript
209
+ {
210
+ provider: EmailProvider.MAILGUN,
211
+ signatureHeader: 'X-Mailgun-Signature',
212
+ signatureAlgorithm: 'sha256'
213
+ }
214
+ ```
215
+
216
+ **Supported Events:** delivered, opened, clicked, bounced, complained, unsubscribed, failed
217
+
218
+ ### AWS SES
219
+
220
+ ```typescript
221
+ {
222
+ provider: EmailProvider.AWS_SES,
223
+ signatureHeader: 'X-Amz-Sns-Message-Type',
224
+ signatureAlgorithm: 'sha256'
225
+ }
226
+ ```
227
+
228
+ **Supported Events:** Delivery, Open, Click, Bounce, Complaint, Reject
229
+
230
+ ### Postmark
231
+
232
+ ```typescript
233
+ {
234
+ provider: EmailProvider.POSTMARK,
235
+ signatureHeader: 'X-Postmark-Signature',
236
+ signatureAlgorithm: 'sha256'
237
+ }
238
+ ```
239
+
240
+ **Supported Events:** Delivery, Open, Click, Bounce, SpamComplaint, SubscriptionChange
241
+
242
+ ### SMTP2GO
243
+
244
+ ```typescript
245
+ {
246
+ provider: EmailProvider.SMTP2GO,
247
+ signatureHeader: 'X-Smtp2go-Signature',
248
+ signatureAlgorithm: 'sha256'
249
+ }
250
+ ```
251
+
252
+ **Supported Events:** delivered, open, click, bounce, failed
253
+
254
+ ## Event Types
255
+
256
+ All provider events are normalized to these unified types:
257
+
258
+ - `DELIVERED` - Email successfully delivered
259
+ - `OPENED` - Email opened by recipient
260
+ - `CLICKED` - Link clicked in email
261
+ - `BOUNCED` - Email bounced (hard or soft)
262
+ - `COMPLAINED` - Spam complaint received
263
+ - `UNSUBSCRIBED` - Recipient unsubscribed
264
+ - `FAILED` - Email delivery failed
265
+ - `DEFERRED` - Email delivery deferred
266
+ - `DROPPED` - Email dropped by provider
267
+
268
+ ## Analytics Metrics
269
+
270
+ ### Counts
271
+ - `totalSent` - Total emails sent
272
+ - `totalDelivered` - Total emails delivered
273
+ - `totalOpened` - Total emails opened
274
+ - `totalClicked` - Total links clicked
275
+ - `totalBounced` - Total emails bounced
276
+ - `totalComplained` - Total spam complaints
277
+ - `totalUnsubscribed` - Total unsubscribes
278
+ - `totalFailed` - Total delivery failures
279
+
280
+ ### Rates
281
+ - `deliveryRate` - delivered / sent
282
+ - `openRate` - opened / delivered
283
+ - `clickRate` - clicked / delivered
284
+ - `clickToOpenRate` - clicked / opened
285
+ - `bounceRate` - bounced / sent
286
+ - `complaintRate` - complained / sent
287
+ - `unsubscribeRate` - unsubscribed / delivered
288
+
289
+ ### Breakdowns
290
+ - `byProvider` - Analytics per provider
291
+ - `byEventType` - Counts per event type
292
+ - `byBounceType` - Hard vs soft bounces
293
+ - `topLinks` - Most clicked links
294
+ - `byDevice` - Desktop, mobile, tablet breakdown
295
+
296
+ ## Configuration
297
+
298
+ ### Environment Variables
299
+
300
+ ```bash
301
+ # Provider webhook secrets
302
+ EMAIL_WEBHOOK_SENDGRID_SECRET=your-sendgrid-webhook-secret
303
+ EMAIL_WEBHOOK_MAILGUN_SECRET=your-mailgun-webhook-secret
304
+ EMAIL_WEBHOOK_SES_SECRET=your-ses-sns-topic-subscription-secret
305
+ EMAIL_WEBHOOK_POSTMARK_SECRET=your-postmark-webhook-secret
306
+
307
+ # Persistence
308
+ EMAIL_WEBHOOK_PERSISTENCE_ENABLED=true
309
+ EMAIL_WEBHOOK_PERSISTENCE_ADAPTER=memory
310
+ EMAIL_WEBHOOK_PERSISTENCE_RETENTION_DAYS=90
311
+
312
+ # Analytics
313
+ EMAIL_WEBHOOK_ANALYTICS_REALTIME=true
314
+ EMAIL_WEBHOOK_ANALYTICS_INTERVAL=5000
315
+
316
+ # Notifications
317
+ EMAIL_WEBHOOK_NOTIFICATIONS_ENABLED=true
318
+
319
+ # Rate limiting
320
+ EMAIL_WEBHOOK_RATE_LIMIT_ENABLED=false
321
+ EMAIL_WEBHOOK_RATE_LIMIT_MAX_PER_SECOND=100
322
+ ```
323
+
324
+ ## Integration Status
325
+
326
+ - **Logger**: integrated - Uses MockLogger (pending @bernierllc/logger publication)
327
+ - **Docs-Suite**: ready - TypeScript interfaces with JSDoc comments
328
+ - **NeverHub**: required - Event publishing and service discovery (pending implementation)
329
+
330
+ ## Examples
331
+
332
+ See the `examples/` directory for complete usage examples:
333
+
334
+ - `setup-sendgrid.ts` - SendGrid webhook setup
335
+ - `setup-mailgun.ts` - Mailgun webhook setup
336
+ - `query-analytics.ts` - Analytics query examples
337
+ - `critical-events.ts` - Critical event handling
338
+
339
+ ## License
340
+
341
+ Copyright (c) 2025 Bernier LLC. All rights reserved.
342
+
343
+ This package is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
344
+
345
+ ## See Also
346
+
347
+ - [@bernierllc/webhook-processor](../webhook-processor) - Generic webhook processing (pending)
348
+ - [@bernierllc/email-sender](../../core/email-sender) - Email sending service
349
+ - [@bernierllc/email-campaign-management](./email-campaign-management) - Campaign management (pending)
@@ -0,0 +1,247 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { EmailWebhookEventsService } from '../src/EmailWebhookEventsService';
10
+ import {
11
+ EmailEventType,
12
+ EmailProvider,
13
+ WebhookPayload,
14
+ ProviderWebhookConfig
15
+ } from '../src/types';
16
+
17
+ describe('EmailWebhookEventsService', () => {
18
+ let service: EmailWebhookEventsService;
19
+
20
+ const createProviderConfig = (): ProviderWebhookConfig => ({
21
+ provider: EmailProvider.SENDGRID,
22
+ enabled: true,
23
+ webhookUrl: '/webhooks/sendgrid',
24
+ secretKey: 'test-secret',
25
+ signatureHeader: 'X-Twilio-Email-Event-Webhook-Signature',
26
+ signatureAlgorithm: 'sha256'
27
+ });
28
+
29
+ beforeEach(async () => {
30
+ service = new EmailWebhookEventsService({
31
+ providers: [createProviderConfig()],
32
+ persistence: {
33
+ enabled: true,
34
+ adapter: 'memory'
35
+ },
36
+ analytics: {
37
+ realTimeEnabled: true,
38
+ aggregationInterval: 5000,
39
+ retentionDays: 90
40
+ },
41
+ notifications: {
42
+ enabled: true,
43
+ criticalEvents: [EmailEventType.BOUNCED, EmailEventType.COMPLAINED],
44
+ channels: []
45
+ }
46
+ });
47
+
48
+ await service.initialize();
49
+ });
50
+
51
+ const createWebhook = (eventType: string, overrides: Partial<WebhookPayload> = {}): WebhookPayload => ({
52
+ provider: EmailProvider.SENDGRID,
53
+ signature: 'test-signature',
54
+ payload: {
55
+ event: eventType,
56
+ email: 'user@example.com',
57
+ timestamp: Math.floor(Date.now() / 1000),
58
+ sg_message_id: 'msg-123',
59
+ email_id: 'email-456'
60
+ },
61
+ headers: {},
62
+ ...overrides
63
+ });
64
+
65
+ describe('processWebhook', () => {
66
+ it('should process delivered event successfully', async () => {
67
+ const webhook = createWebhook('delivered');
68
+ const result = await service.processWebhook(webhook);
69
+
70
+ expect(result.success).toBe(true);
71
+ expect(result.event).toBeDefined();
72
+ expect(result.event!.type).toBe(EmailEventType.DELIVERED);
73
+ expect(result.event!.recipient).toBe('user@example.com');
74
+ });
75
+
76
+ it('should process opened event successfully', async () => {
77
+ const webhook = createWebhook('open');
78
+ const result = await service.processWebhook(webhook);
79
+
80
+ expect(result.success).toBe(true);
81
+ expect(result.event!.type).toBe(EmailEventType.OPENED);
82
+ });
83
+
84
+ it('should process clicked event successfully', async () => {
85
+ const webhook = createWebhook('click', {
86
+ payload: {
87
+ event: 'click',
88
+ email: 'user@example.com',
89
+ timestamp: Math.floor(Date.now() / 1000),
90
+ sg_message_id: 'msg-123',
91
+ url: 'https://example.com/link'
92
+ }
93
+ });
94
+
95
+ const result = await service.processWebhook(webhook);
96
+
97
+ expect(result.success).toBe(true);
98
+ expect(result.event!.type).toBe(EmailEventType.CLICKED);
99
+ expect(result.event!.metadata.url).toBe('https://example.com/link');
100
+ });
101
+
102
+ it('should process bounce event successfully', async () => {
103
+ const webhook = createWebhook('bounce', {
104
+ payload: {
105
+ event: 'bounce',
106
+ email: 'user@example.com',
107
+ timestamp: Math.floor(Date.now() / 1000),
108
+ sg_message_id: 'msg-123',
109
+ status: '5.1.1',
110
+ reason: 'User unknown'
111
+ }
112
+ });
113
+
114
+ const result = await service.processWebhook(webhook);
115
+
116
+ expect(result.success).toBe(true);
117
+ expect(result.event!.type).toBe(EmailEventType.BOUNCED);
118
+ expect(result.event!.metadata.bounceType).toBe('hard');
119
+ expect(result.event!.metadata.bounceReason).toBe('User unknown');
120
+ });
121
+
122
+ it('should handle unsupported provider gracefully', async () => {
123
+ const webhook = createWebhook('delivered', {
124
+ provider: EmailProvider.GENERIC_SMTP
125
+ });
126
+
127
+ const result = await service.processWebhook(webhook);
128
+
129
+ expect(result.success).toBe(false);
130
+ expect(result.error).toBe('Event normalization failed');
131
+ });
132
+ });
133
+
134
+ describe('getAnalytics', () => {
135
+ it('should return analytics for processed events', async () => {
136
+ // Process multiple events
137
+ await service.processWebhook(createWebhook('delivered'));
138
+ await service.processWebhook(createWebhook('delivered'));
139
+ await service.processWebhook(createWebhook('open'));
140
+ await service.processWebhook(createWebhook('click', {
141
+ payload: {
142
+ event: 'click',
143
+ email: 'user@example.com',
144
+ timestamp: Math.floor(Date.now() / 1000),
145
+ sg_message_id: 'msg-123',
146
+ url: 'https://example.com/link'
147
+ }
148
+ }));
149
+
150
+ const analytics = await service.getAnalytics(
151
+ new Date('2025-01-01'),
152
+ new Date('2025-12-31')
153
+ );
154
+
155
+ expect(analytics.totalDelivered).toBe(2);
156
+ expect(analytics.totalOpened).toBe(1);
157
+ expect(analytics.totalClicked).toBe(1);
158
+ expect(analytics.openRate).toBe(0.5);
159
+ expect(analytics.clickRate).toBe(0.5);
160
+ });
161
+
162
+ it('should filter analytics by provider', async () => {
163
+ await service.processWebhook(createWebhook('delivered', {
164
+ provider: EmailProvider.SENDGRID
165
+ }));
166
+
167
+ const analytics = await service.getAnalytics(
168
+ new Date('2025-01-01'),
169
+ new Date('2025-12-31'),
170
+ { provider: EmailProvider.SENDGRID }
171
+ );
172
+
173
+ expect(analytics.totalDelivered).toBe(1);
174
+ });
175
+ });
176
+
177
+ describe('queryEvents', () => {
178
+ it('should query events by email ID', async () => {
179
+ await service.processWebhook(createWebhook('delivered', {
180
+ payload: {
181
+ event: 'delivered',
182
+ email: 'user@example.com',
183
+ timestamp: Math.floor(Date.now() / 1000),
184
+ sg_message_id: 'msg-123',
185
+ email_id: 'email-123'
186
+ }
187
+ }));
188
+
189
+ await service.processWebhook(createWebhook('delivered', {
190
+ payload: {
191
+ event: 'delivered',
192
+ email: 'user@example.com',
193
+ timestamp: Math.floor(Date.now() / 1000),
194
+ sg_message_id: 'msg-456',
195
+ email_id: 'email-456'
196
+ }
197
+ }));
198
+
199
+ const events = await service.queryEvents({ emailId: 'email-123' });
200
+
201
+ expect(events).toHaveLength(1);
202
+ expect(events[0].emailId).toBe('email-123');
203
+ });
204
+
205
+ it('should query events by event type', async () => {
206
+ await service.processWebhook(createWebhook('delivered'));
207
+ await service.processWebhook(createWebhook('open'));
208
+
209
+ const events = await service.queryEvents({
210
+ eventTypes: [EmailEventType.DELIVERED]
211
+ });
212
+
213
+ expect(events).toHaveLength(1);
214
+ expect(events[0].type).toBe(EmailEventType.DELIVERED);
215
+ });
216
+ });
217
+
218
+ describe('getEventsForEmail', () => {
219
+ it('should return all events for a specific email', async () => {
220
+ await service.processWebhook(createWebhook('delivered', {
221
+ payload: {
222
+ event: 'delivered',
223
+ email: 'user@example.com',
224
+ timestamp: Math.floor(Date.now() / 1000),
225
+ sg_message_id: 'msg-123',
226
+ email_id: 'email-123'
227
+ }
228
+ }));
229
+
230
+ await service.processWebhook(createWebhook('open', {
231
+ payload: {
232
+ event: 'open',
233
+ email: 'user@example.com',
234
+ timestamp: Math.floor(Date.now() / 1000),
235
+ sg_message_id: 'msg-123',
236
+ email_id: 'email-123'
237
+ }
238
+ }));
239
+
240
+ const events = await service.getEventsForEmail('email-123');
241
+
242
+ expect(events).toHaveLength(2);
243
+ expect(events[0].emailId).toBe('email-123');
244
+ expect(events[1].emailId).toBe('email-123');
245
+ });
246
+ });
247
+ });