@valentine-efagene/qshelter-common 2.0.75 → 2.0.77

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