@valentine-efagene/qshelter-common 2.0.66 → 2.0.68

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 (36) hide show
  1. package/dist/generated/client/browser.d.ts +29 -0
  2. package/dist/generated/client/client.d.ts +29 -0
  3. package/dist/generated/client/commonInputTypes.d.ts +120 -0
  4. package/dist/generated/client/enums.d.ts +33 -0
  5. package/dist/generated/client/enums.js +29 -0
  6. package/dist/generated/client/internal/class.d.ts +55 -0
  7. package/dist/generated/client/internal/class.js +2 -2
  8. package/dist/generated/client/internal/prismaNamespace.d.ts +475 -1
  9. package/dist/generated/client/internal/prismaNamespace.js +113 -0
  10. package/dist/generated/client/internal/prismaNamespaceBrowser.d.ts +123 -0
  11. package/dist/generated/client/internal/prismaNamespaceBrowser.js +113 -0
  12. package/dist/generated/client/models/EventChannel.d.ts +1305 -0
  13. package/dist/generated/client/models/EventChannel.js +1 -0
  14. package/dist/generated/client/models/EventHandler.d.ts +1749 -0
  15. package/dist/generated/client/models/EventHandler.js +1 -0
  16. package/dist/generated/client/models/EventHandlerExecution.d.ts +1525 -0
  17. package/dist/generated/client/models/EventHandlerExecution.js +1 -0
  18. package/dist/generated/client/models/EventType.d.ts +1653 -0
  19. package/dist/generated/client/models/EventType.js +1 -0
  20. package/dist/generated/client/models/Tenant.d.ts +796 -0
  21. package/dist/generated/client/models/WorkflowEvent.d.ts +1654 -0
  22. package/dist/generated/client/models/WorkflowEvent.js +1 -0
  23. package/dist/generated/client/models/index.d.ts +5 -0
  24. package/dist/generated/client/models/index.js +5 -0
  25. package/dist/generated/client/models.d.ts +5 -0
  26. package/dist/src/events/event-config.service.d.ts +123 -0
  27. package/dist/src/events/event-config.service.js +416 -0
  28. package/dist/src/events/index.d.ts +3 -0
  29. package/dist/src/events/index.js +5 -0
  30. package/dist/src/events/workflow-event.service.d.ts +205 -0
  31. package/dist/src/events/workflow-event.service.js +660 -0
  32. package/dist/src/events/workflow-types.d.ts +315 -0
  33. package/dist/src/events/workflow-types.js +14 -0
  34. package/package.json +1 -1
  35. package/prisma/migrations/20260105151535_add_event_workflow_system/migration.sql +132 -0
  36. package/prisma/schema.prisma +272 -0
@@ -0,0 +1,660 @@
1
+ /**
2
+ * Workflow Event Service
3
+ *
4
+ * Handles emission and processing of workflow events.
5
+ * Events are stored in the database and processed by registered handlers.
6
+ *
7
+ * Design principles:
8
+ * 1. Events are immutable once emitted
9
+ * 2. Handlers are configured by admins, not hardcoded
10
+ * 3. Each handler execution is logged for audit
11
+ * 4. Failed handlers can be retried
12
+ * 5. Events can be correlated for tracing
13
+ *
14
+ * Usage:
15
+ * ```typescript
16
+ * const eventService = new WorkflowEventService(prisma);
17
+ *
18
+ * // Emit an event
19
+ * const event = await eventService.emit(tenantId, {
20
+ * eventType: 'DOCUMENT_UPLOADED',
21
+ * payload: {
22
+ * contractId: 'ctr_123',
23
+ * stepId: 'step_456',
24
+ * documentUrl: 'https://...',
25
+ * },
26
+ * source: 'contract-service',
27
+ * actor: { id: 'user_789', type: 'USER' },
28
+ * });
29
+ *
30
+ * // Process the event (run all handlers)
31
+ * const result = await eventService.processEvent(event.id);
32
+ * ```
33
+ */
34
+ import { EventPublisher } from './event-publisher';
35
+ import { NotificationType, NotificationChannel } from './notification-enums';
36
+ /**
37
+ * Simple in-memory service registry
38
+ */
39
+ class InMemoryServiceRegistry {
40
+ services = new Map();
41
+ get(serviceName) {
42
+ return this.services.get(serviceName);
43
+ }
44
+ register(serviceName, service) {
45
+ this.services.set(serviceName, service);
46
+ }
47
+ }
48
+ export class WorkflowEventService {
49
+ prisma;
50
+ serviceRegistry;
51
+ eventPublisher;
52
+ constructor(prisma, serviceRegistry, eventPublisher) {
53
+ this.prisma = prisma;
54
+ this.serviceRegistry = serviceRegistry || new InMemoryServiceRegistry();
55
+ this.eventPublisher = eventPublisher || new EventPublisher('workflow-event-service');
56
+ }
57
+ /**
58
+ * Register a service for internal event handlers
59
+ *
60
+ * Services can be called by INTERNAL handler type configurations.
61
+ * The service should expose methods that match the handler config.
62
+ */
63
+ registerService(name, service) {
64
+ this.serviceRegistry.register(name, service);
65
+ }
66
+ /**
67
+ * Emit an event
68
+ *
69
+ * This creates an event record and optionally processes it immediately
70
+ * or leaves it for async processing by a worker.
71
+ *
72
+ * @param tenantId - Tenant context
73
+ * @param input - Event details
74
+ * @param processImmediately - Whether to process handlers now (default: false)
75
+ */
76
+ async emit(tenantId, input, processImmediately = false) {
77
+ // Look up the event type by code
78
+ const eventType = await this.prisma.eventType.findFirst({
79
+ where: {
80
+ tenantId,
81
+ code: input.eventType.toUpperCase(),
82
+ enabled: true,
83
+ channel: { enabled: true },
84
+ },
85
+ include: {
86
+ channel: true,
87
+ },
88
+ });
89
+ if (!eventType) {
90
+ throw new Error(`Event type '${input.eventType}' not found or not enabled for tenant`);
91
+ }
92
+ // TODO: Validate payload against schema if defined
93
+ // if (eventType.payloadSchema) {
94
+ // validateJsonSchema(input.payload, eventType.payloadSchema);
95
+ // }
96
+ // Create the event record
97
+ const event = await this.prisma.workflowEvent.create({
98
+ data: {
99
+ tenantId,
100
+ eventTypeId: eventType.id,
101
+ payload: input.payload,
102
+ source: input.source,
103
+ actorId: input.actor?.id,
104
+ actorType: input.actor?.type || 'SYSTEM',
105
+ correlationId: input.correlationId,
106
+ causationId: input.causationId,
107
+ status: 'PENDING',
108
+ },
109
+ include: {
110
+ eventType: {
111
+ include: { channel: true },
112
+ },
113
+ },
114
+ });
115
+ const result = {
116
+ id: event.id,
117
+ tenantId: event.tenantId,
118
+ eventTypeId: event.eventTypeId,
119
+ eventTypeCode: event.eventType.code,
120
+ channelCode: event.eventType.channel.code,
121
+ payload: event.payload,
122
+ source: event.source,
123
+ actorId: event.actorId,
124
+ actorType: event.actorType,
125
+ status: event.status,
126
+ correlationId: event.correlationId,
127
+ causationId: event.causationId,
128
+ error: event.error,
129
+ processedAt: event.processedAt,
130
+ createdAt: event.createdAt,
131
+ };
132
+ // Process immediately if requested
133
+ if (processImmediately) {
134
+ await this.processEvent(event.id);
135
+ }
136
+ return result;
137
+ }
138
+ /**
139
+ * Process an event by executing all registered handlers
140
+ *
141
+ * This is typically called by a worker/queue processor,
142
+ * but can also be called synchronously for simple cases.
143
+ */
144
+ async processEvent(eventId) {
145
+ // Fetch the event with its type and handlers
146
+ const event = await this.prisma.workflowEvent.findUnique({
147
+ where: { id: eventId },
148
+ include: {
149
+ eventType: {
150
+ include: {
151
+ handlers: {
152
+ where: { enabled: true },
153
+ orderBy: { priority: 'asc' },
154
+ },
155
+ },
156
+ },
157
+ },
158
+ });
159
+ if (!event) {
160
+ throw new Error(`Event ${eventId} not found`);
161
+ }
162
+ // Mark as processing
163
+ await this.prisma.workflowEvent.update({
164
+ where: { id: eventId },
165
+ data: { status: 'PROCESSING' },
166
+ });
167
+ const handlers = event.eventType.handlers;
168
+ const errors = [];
169
+ let handlersSucceeded = 0;
170
+ let handlersFailed = 0;
171
+ // Execute each handler in priority order
172
+ for (const handler of handlers) {
173
+ // Check filter condition
174
+ if (handler.filterCondition) {
175
+ const shouldRun = this.evaluateFilterCondition(handler.filterCondition, event.payload);
176
+ if (!shouldRun) {
177
+ // Log as skipped
178
+ await this.prisma.eventHandlerExecution.create({
179
+ data: {
180
+ eventId: event.id,
181
+ handlerId: handler.id,
182
+ status: 'SKIPPED',
183
+ input: event.payload,
184
+ },
185
+ });
186
+ continue;
187
+ }
188
+ }
189
+ // Create execution record
190
+ const execution = await this.prisma.eventHandlerExecution.create({
191
+ data: {
192
+ eventId: event.id,
193
+ handlerId: handler.id,
194
+ status: 'RUNNING',
195
+ input: event.payload,
196
+ startedAt: new Date(),
197
+ },
198
+ });
199
+ try {
200
+ // Execute the handler based on type
201
+ const output = await this.executeHandler(handler.handlerType, handler.config, event.payload, event.tenantId);
202
+ // Mark as completed
203
+ const completedAt = new Date();
204
+ await this.prisma.eventHandlerExecution.update({
205
+ where: { id: execution.id },
206
+ data: {
207
+ status: 'COMPLETED',
208
+ output: output,
209
+ completedAt,
210
+ durationMs: completedAt.getTime() - execution.startedAt.getTime(),
211
+ },
212
+ });
213
+ handlersSucceeded++;
214
+ }
215
+ catch (error) {
216
+ // Mark as failed
217
+ const completedAt = new Date();
218
+ await this.prisma.eventHandlerExecution.update({
219
+ where: { id: execution.id },
220
+ data: {
221
+ status: 'FAILED',
222
+ error: error.message,
223
+ errorCode: error.code,
224
+ completedAt,
225
+ durationMs: completedAt.getTime() - execution.startedAt.getTime(),
226
+ },
227
+ });
228
+ errors.push({
229
+ handlerId: handler.id,
230
+ handlerName: handler.name,
231
+ error: error.message,
232
+ });
233
+ handlersFailed++;
234
+ // TODO: Implement retry logic based on handler.maxRetries and handler.retryDelayMs
235
+ }
236
+ }
237
+ // Update event status
238
+ const finalStatus = handlersFailed > 0
239
+ ? handlersSucceeded > 0
240
+ ? 'COMPLETED' // Partial success still counts as completed
241
+ : 'FAILED'
242
+ : 'COMPLETED';
243
+ await this.prisma.workflowEvent.update({
244
+ where: { id: eventId },
245
+ data: {
246
+ status: finalStatus,
247
+ processedAt: new Date(),
248
+ error: errors.length > 0 ? JSON.stringify(errors) : null,
249
+ },
250
+ });
251
+ return {
252
+ eventId,
253
+ status: finalStatus,
254
+ handlersExecuted: handlers.length,
255
+ handlersSucceeded,
256
+ handlersFailed,
257
+ errors,
258
+ };
259
+ }
260
+ /**
261
+ * Get pending events for processing (for worker/queue)
262
+ */
263
+ async getPendingEvents(tenantId, limit = 100) {
264
+ const events = await this.prisma.workflowEvent.findMany({
265
+ where: {
266
+ status: 'PENDING',
267
+ ...(tenantId && { tenantId }),
268
+ },
269
+ include: {
270
+ eventType: {
271
+ include: { channel: true },
272
+ },
273
+ },
274
+ orderBy: { createdAt: 'asc' },
275
+ take: limit,
276
+ });
277
+ return events.map((event) => ({
278
+ id: event.id,
279
+ tenantId: event.tenantId,
280
+ eventTypeId: event.eventTypeId,
281
+ eventTypeCode: event.eventType.code,
282
+ channelCode: event.eventType.channel.code,
283
+ payload: event.payload,
284
+ source: event.source,
285
+ actorId: event.actorId,
286
+ actorType: event.actorType,
287
+ status: event.status,
288
+ correlationId: event.correlationId,
289
+ causationId: event.causationId,
290
+ error: event.error,
291
+ processedAt: event.processedAt,
292
+ createdAt: event.createdAt,
293
+ }));
294
+ }
295
+ /**
296
+ * Get events by correlation ID (for tracing related events)
297
+ */
298
+ async getEventsByCorrelation(tenantId, correlationId) {
299
+ const events = await this.prisma.workflowEvent.findMany({
300
+ where: { tenantId, correlationId },
301
+ include: {
302
+ eventType: {
303
+ include: { channel: true },
304
+ },
305
+ },
306
+ orderBy: { createdAt: 'asc' },
307
+ });
308
+ return events.map((event) => ({
309
+ id: event.id,
310
+ tenantId: event.tenantId,
311
+ eventTypeId: event.eventTypeId,
312
+ eventTypeCode: event.eventType.code,
313
+ channelCode: event.eventType.channel.code,
314
+ payload: event.payload,
315
+ source: event.source,
316
+ actorId: event.actorId,
317
+ actorType: event.actorType,
318
+ status: event.status,
319
+ correlationId: event.correlationId,
320
+ causationId: event.causationId,
321
+ error: event.error,
322
+ processedAt: event.processedAt,
323
+ createdAt: event.createdAt,
324
+ }));
325
+ }
326
+ /**
327
+ * Get event with executions (for debugging/auditing)
328
+ */
329
+ async getEventWithExecutions(tenantId, eventId) {
330
+ const event = await this.prisma.workflowEvent.findFirst({
331
+ where: { id: eventId, tenantId },
332
+ include: {
333
+ eventType: {
334
+ include: { channel: true },
335
+ },
336
+ executions: {
337
+ include: {
338
+ handler: {
339
+ select: { id: true, name: true, handlerType: true },
340
+ },
341
+ },
342
+ orderBy: { createdAt: 'asc' },
343
+ },
344
+ },
345
+ });
346
+ return event;
347
+ }
348
+ // ==========================================
349
+ // HANDLER EXECUTION
350
+ // ==========================================
351
+ /**
352
+ * Execute a handler based on its type
353
+ */
354
+ async executeHandler(handlerType, config, payload, tenantId) {
355
+ switch (handlerType) {
356
+ case 'INTERNAL':
357
+ return this.executeInternalHandler(config, payload, tenantId);
358
+ case 'WEBHOOK':
359
+ return this.executeWebhookHandler(config, payload);
360
+ case 'WORKFLOW':
361
+ return this.executeWorkflowHandler(config, payload, tenantId);
362
+ case 'NOTIFICATION':
363
+ return this.executeNotificationHandler(config, payload, tenantId);
364
+ case 'SNS':
365
+ return this.executeSnsHandler(config, payload, tenantId);
366
+ case 'SCRIPT':
367
+ // TODO: Implement script execution (sandboxed)
368
+ throw new Error('Script handlers not yet implemented');
369
+ default:
370
+ throw new Error(`Unknown handler type: ${handlerType}`);
371
+ }
372
+ }
373
+ /**
374
+ * Execute an internal service method
375
+ */
376
+ async executeInternalHandler(config, payload, tenantId) {
377
+ // Get the service from the registry
378
+ const service = this.serviceRegistry.get(config.service);
379
+ if (!service) {
380
+ throw new Error(`Service '${config.service}' not found in registry`);
381
+ }
382
+ // Get the method
383
+ const method = service[config.method];
384
+ if (typeof method !== 'function') {
385
+ throw new Error(`Method '${config.method}' not found on service '${config.service}'`);
386
+ }
387
+ // Transform payload if mapping is defined
388
+ const transformedPayload = config.payloadMapping
389
+ ? this.transformPayload(payload, config.payloadMapping)
390
+ : payload;
391
+ // Call the method with tenantId and payload
392
+ return method.call(service, tenantId, transformedPayload);
393
+ }
394
+ /**
395
+ * Execute a webhook handler
396
+ */
397
+ async executeWebhookHandler(config, payload) {
398
+ const transformedPayload = config.payloadMapping
399
+ ? this.transformPayload(payload, config.payloadMapping)
400
+ : payload;
401
+ const controller = new AbortController();
402
+ const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs || 30000);
403
+ try {
404
+ const response = await fetch(config.url, {
405
+ method: config.method,
406
+ headers: {
407
+ 'Content-Type': 'application/json',
408
+ ...config.headers,
409
+ },
410
+ body: config.method !== 'GET' ? JSON.stringify(transformedPayload) : undefined,
411
+ signal: controller.signal,
412
+ });
413
+ clearTimeout(timeoutId);
414
+ if (!response.ok) {
415
+ const errorText = await response.text();
416
+ throw new Error(`Webhook returned ${response.status}: ${errorText}`);
417
+ }
418
+ // Try to parse JSON response, fall back to empty object
419
+ try {
420
+ return await response.json();
421
+ }
422
+ catch {
423
+ return {};
424
+ }
425
+ }
426
+ finally {
427
+ clearTimeout(timeoutId);
428
+ }
429
+ }
430
+ /**
431
+ * Execute a workflow handler
432
+ *
433
+ * This emits a new event that the workflow service can pick up,
434
+ * creating loose coupling between event system and workflow engine.
435
+ */
436
+ async executeWorkflowHandler(config, payload, tenantId) {
437
+ // Return the workflow action data
438
+ // The workflow service should listen for WORKFLOW handler results
439
+ return {
440
+ action: config.action,
441
+ workflowId: config.workflowId,
442
+ phaseId: config.phaseId,
443
+ stepId: config.stepId,
444
+ data: { ...config.data, ...payload },
445
+ tenantId,
446
+ };
447
+ }
448
+ /**
449
+ * Execute a notification handler
450
+ *
451
+ * This would integrate with a notification service.
452
+ * Returns what would be sent for logging purposes.
453
+ */
454
+ async executeNotificationHandler(config, payload, tenantId) {
455
+ // TODO: Integrate with actual notification service
456
+ // For now, return the notification data for logging
457
+ return {
458
+ template: config.template,
459
+ channels: config.channels,
460
+ recipients: this.resolveRecipients(config.recipients, payload),
461
+ priority: config.priority || 'normal',
462
+ data: payload,
463
+ tenantId,
464
+ };
465
+ }
466
+ /**
467
+ * Execute an SNS handler
468
+ *
469
+ * Publishes to SNS topic using the EventPublisher, which triggers
470
+ * the notification-service via SNS->SQS subscription.
471
+ *
472
+ * The config specifies the notification type, channel, and how to
473
+ * map the event payload to the notification payload.
474
+ */
475
+ async executeSnsHandler(config, payload, tenantId) {
476
+ // Build the notification payload from config
477
+ const notificationPayload = this.buildSnsPayload(config, payload);
478
+ // Map channel string to NotificationChannel enum
479
+ const channelMap = {
480
+ email: NotificationChannel.EMAIL,
481
+ sms: NotificationChannel.SMS,
482
+ push: NotificationChannel.PUSH,
483
+ };
484
+ const channel = channelMap[config.channel] || NotificationChannel.EMAIL;
485
+ // Validate notification type is a valid enum value
486
+ const notificationType = config.notificationType;
487
+ if (!Object.values(NotificationType).includes(notificationType)) {
488
+ throw new Error(`Invalid notification type: ${config.notificationType}`);
489
+ }
490
+ // Publish to SNS via EventPublisher
491
+ const messageId = await this.eventPublisher.publish(notificationType, channel, notificationPayload, {
492
+ tenantId,
493
+ correlationId: payload.correlationId || undefined,
494
+ userId: payload.userId || payload.actorId || undefined,
495
+ });
496
+ return {
497
+ success: true,
498
+ messageId,
499
+ notificationType: config.notificationType,
500
+ channel: config.channel,
501
+ payload: notificationPayload,
502
+ tenantId,
503
+ };
504
+ }
505
+ /**
506
+ * Build the notification payload for SNS from config and event payload
507
+ */
508
+ buildSnsPayload(config, payload) {
509
+ const result = {};
510
+ // Apply static payload first
511
+ if (config.staticPayload) {
512
+ Object.assign(result, config.staticPayload);
513
+ }
514
+ // Apply payload mapping (map from event payload to notification payload)
515
+ if (config.payloadMapping) {
516
+ for (const [targetField, sourcePath] of Object.entries(config.payloadMapping)) {
517
+ const value = this.resolvePath(payload, sourcePath.replace(/^\$\./, ''));
518
+ if (value !== undefined) {
519
+ result[targetField] = value;
520
+ }
521
+ }
522
+ }
523
+ // If recipientPath is specified, extract the recipient email
524
+ if (config.recipientPath) {
525
+ const email = this.resolvePath(payload, config.recipientPath.replace(/^\$\./, ''));
526
+ if (email && typeof email === 'string') {
527
+ result.to_email = email;
528
+ }
529
+ }
530
+ return result;
531
+ }
532
+ // ==========================================
533
+ // UTILITY METHODS
534
+ // ==========================================
535
+ /**
536
+ * Evaluate a filter condition against the payload
537
+ */
538
+ evaluateFilterCondition(condition, payload) {
539
+ try {
540
+ // Simple JSONPath-like evaluation
541
+ // Supports: $.field == 'value', $.field != 'value', $.field > 10, etc.
542
+ // Equality: $.field == 'value'
543
+ const eqMatch = condition.match(/^\$\.(\w+(?:\.\w+)*)\s*==\s*['"](.+)['"]$/);
544
+ if (eqMatch) {
545
+ const [, path, value] = eqMatch;
546
+ return this.resolvePath(payload, path) === value;
547
+ }
548
+ // Inequality: $.field != 'value'
549
+ const neqMatch = condition.match(/^\$\.(\w+(?:\.\w+)*)\s*!=\s*['"](.+)['"]$/);
550
+ if (neqMatch) {
551
+ const [, path, value] = neqMatch;
552
+ return this.resolvePath(payload, path) !== value;
553
+ }
554
+ // Numeric comparison: $.field > 10
555
+ const numMatch = condition.match(/^\$\.(\w+(?:\.\w+)*)\s*([<>=!]+)\s*(\d+(?:\.\d+)?)$/);
556
+ if (numMatch) {
557
+ const [, path, op, numStr] = numMatch;
558
+ const fieldValue = this.resolvePath(payload, path);
559
+ const num = parseFloat(numStr);
560
+ if (typeof fieldValue !== 'number')
561
+ return false;
562
+ switch (op) {
563
+ case '>':
564
+ return fieldValue > num;
565
+ case '>=':
566
+ return fieldValue >= num;
567
+ case '<':
568
+ return fieldValue < num;
569
+ case '<=':
570
+ return fieldValue <= num;
571
+ case '==':
572
+ return fieldValue === num;
573
+ case '!=':
574
+ return fieldValue !== num;
575
+ default:
576
+ return true;
577
+ }
578
+ }
579
+ // Existence check: $.field (truthy check)
580
+ const existsMatch = condition.match(/^\$\.(\w+(?:\.\w+)*)$/);
581
+ if (existsMatch) {
582
+ const [, path] = existsMatch;
583
+ return !!this.resolvePath(payload, path);
584
+ }
585
+ // If we can't parse, run the handler (fail open)
586
+ return true;
587
+ }
588
+ catch {
589
+ return true;
590
+ }
591
+ }
592
+ /**
593
+ * Transform payload using a mapping
594
+ */
595
+ transformPayload(payload, mapping) {
596
+ const result = {};
597
+ for (const [targetKey, sourcePath] of Object.entries(mapping)) {
598
+ result[targetKey] = this.resolvePath(payload, sourcePath);
599
+ }
600
+ return result;
601
+ }
602
+ /**
603
+ * Resolve a dot-notation path in an object
604
+ */
605
+ resolvePath(obj, path) {
606
+ const parts = path.replace(/^\$\./, '').split('.');
607
+ let current = obj;
608
+ for (const part of parts) {
609
+ if (current == null)
610
+ return undefined;
611
+ current = current[part];
612
+ }
613
+ return current;
614
+ }
615
+ /**
616
+ * Resolve recipients from config, potentially using payload variables
617
+ */
618
+ resolveRecipients(recipients, payload) {
619
+ if (!recipients)
620
+ return undefined;
621
+ const resolved = {};
622
+ // Resolve email recipients
623
+ if (recipients.email) {
624
+ resolved.email = recipients.email.map((addr) => {
625
+ if (addr.startsWith('$.')) {
626
+ const value = this.resolvePath(payload, addr);
627
+ return typeof value === 'string' ? value : addr;
628
+ }
629
+ return addr;
630
+ });
631
+ }
632
+ // Resolve phone recipients
633
+ if (recipients.phone) {
634
+ resolved.phone = recipients.phone.map((phone) => {
635
+ if (phone.startsWith('$.')) {
636
+ const value = this.resolvePath(payload, phone);
637
+ return typeof value === 'string' ? value : phone;
638
+ }
639
+ return phone;
640
+ });
641
+ }
642
+ // Resolve userId recipients
643
+ if (recipients.userId) {
644
+ resolved.userId = recipients.userId.map((id) => {
645
+ if (id.startsWith('$.')) {
646
+ const value = this.resolvePath(payload, id);
647
+ return typeof value === 'string' ? value : id;
648
+ }
649
+ return id;
650
+ });
651
+ }
652
+ return resolved;
653
+ }
654
+ }
655
+ /**
656
+ * Create a workflow event service instance
657
+ */
658
+ export function createWorkflowEventService(prisma, serviceRegistry) {
659
+ return new WorkflowEventService(prisma, serviceRegistry);
660
+ }