@valentine-efagene/qshelter-common 2.0.69 → 2.0.71

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.
@@ -32,7 +32,19 @@
32
32
  * ```
33
33
  */
34
34
  import { EventPublisher } from './event-publisher';
35
- import { NotificationType, NotificationChannel } from './notification-enums';
35
+ import { NotificationChannel } from './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
+ }
36
48
  /**
37
49
  * Simple in-memory service registry
38
50
  */
@@ -47,21 +59,21 @@ class InMemoryServiceRegistry {
47
59
  }
48
60
  export class WorkflowEventService {
49
61
  prisma;
50
- serviceRegistry;
62
+ automationRegistry;
51
63
  eventPublisher;
52
- constructor(prisma, serviceRegistry, eventPublisher) {
64
+ constructor(prisma, automationRegistry, eventPublisher) {
53
65
  this.prisma = prisma;
54
- this.serviceRegistry = serviceRegistry || new InMemoryServiceRegistry();
66
+ this.automationRegistry = automationRegistry || new InMemoryAutomationRegistry();
55
67
  this.eventPublisher = eventPublisher || new EventPublisher('workflow-event-service');
56
68
  }
57
69
  /**
58
- * Register a service for internal event handlers
70
+ * Register an automation for RUN_AUTOMATION handlers
59
71
  *
60
- * Services can be called by INTERNAL handler type configurations.
61
- * The service should expose methods that match the handler config.
72
+ * Automations are business logic functions that can be triggered by events.
73
+ * Example: "calculateLateFee", "sendWelcomePackage", "archiveContract"
62
74
  */
63
- registerService(name, service) {
64
- this.serviceRegistry.register(name, service);
75
+ registerAutomation(name, handler) {
76
+ this.automationRegistry.register(name, handler);
65
77
  }
66
78
  /**
67
79
  * Emit an event
@@ -350,53 +362,155 @@ export class WorkflowEventService {
350
362
  // ==========================================
351
363
  /**
352
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
353
373
  */
354
374
  async executeHandler(handlerType, config, payload, tenantId) {
355
375
  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');
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);
369
388
  default:
370
389
  throw new Error(`Unknown handler type: ${handlerType}`);
371
390
  }
372
391
  }
373
392
  /**
374
- * Execute an internal service method
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.
375
397
  */
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`);
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
+ }
381
407
  }
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}'`);
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
+ }
386
438
  }
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);
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;
393
504
  }
394
505
  /**
395
- * Execute a webhook handler
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.
396
510
  */
397
- async executeWebhookHandler(config, payload) {
398
- const transformedPayload = config.payloadMapping
399
- ? this.transformPayload(payload, config.payloadMapping)
511
+ async executeCallWebhookHandler(config, payload) {
512
+ const transformedPayload = config.bodyMapping
513
+ ? this.transformPayload(payload, config.bodyMapping)
400
514
  : payload;
401
515
  const controller = new AbortController();
402
516
  const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs || 30000);
@@ -428,106 +542,50 @@ export class WorkflowEventService {
428
542
  }
429
543
  }
430
544
  /**
431
- * Execute a workflow handler
545
+ * Execute ADVANCE_WORKFLOW handler
432
546
  *
433
- * This emits a new event that the workflow service can pick up,
434
- * creating loose coupling between event system and workflow engine.
547
+ * Advances or modifies workflow state.
548
+ * Business users configure: action (approve/reject/skip), step path, and data.
435
549
  */
436
- async executeWorkflowHandler(config, payload, tenantId) {
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
+ }
437
559
  // Return the workflow action data
438
- // The workflow service should listen for WORKFLOW handler results
560
+ // The workflow service should listen for ADVANCE_WORKFLOW handler results
439
561
  return {
440
562
  action: config.action,
441
563
  workflowId: config.workflowId,
442
564
  phaseId: config.phaseId,
443
- stepId: config.stepId,
565
+ stepId,
444
566
  data: { ...config.data, ...payload },
445
567
  tenantId,
446
568
  };
447
569
  }
448
570
  /**
449
- * Execute a notification handler
571
+ * Execute RUN_AUTOMATION handler
450
572
  *
451
- * This would integrate with a notification service.
452
- * Returns what would be sent for logging purposes.
573
+ * Runs a registered business logic automation.
574
+ * Business users select from pre-defined automations like
575
+ * "Calculate Mortgage Payment", "Generate Contract", etc.
453
576
  */
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}`);
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`);
489
582
  }
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;
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);
531
589
  }
532
590
  // ==========================================
533
591
  // UTILITY METHODS
@@ -612,49 +670,13 @@ export class WorkflowEventService {
612
670
  }
613
671
  return current;
614
672
  }
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
673
  }
655
674
  /**
656
675
  * Create a workflow event service instance
676
+ *
677
+ * @param prisma - Prisma client for database access
678
+ * @param automationRegistry - Optional registry of business automations
657
679
  */
658
- export function createWorkflowEventService(prisma, serviceRegistry) {
659
- return new WorkflowEventService(prisma, serviceRegistry);
680
+ export function createWorkflowEventService(prisma, automationRegistry) {
681
+ return new WorkflowEventService(prisma, automationRegistry);
660
682
  }