@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,343 @@
1
+ /**
2
+ * Event Type Seeder
3
+ *
4
+ * Utilities for seeding EventChannel, EventType, and EventHandler records.
5
+ * This allows admins to configure event-driven workflows programmatically.
6
+ *
7
+ * Usage:
8
+ * ```typescript
9
+ * import { seedContractEventTypes } from '@valentine-efagene/qshelter-common';
10
+ *
11
+ * await seedContractEventTypes(prisma, tenantId);
12
+ * ```
13
+ */
14
+ /**
15
+ * Seed event channels
16
+ */
17
+ export async function seedEventChannels(prisma, tenantId, channels) {
18
+ for (const channel of channels) {
19
+ await prisma.eventChannel.upsert({
20
+ where: {
21
+ tenantId_code: {
22
+ tenantId,
23
+ code: channel.code,
24
+ },
25
+ },
26
+ create: {
27
+ tenantId,
28
+ code: channel.code,
29
+ name: channel.name,
30
+ description: channel.description,
31
+ enabled: channel.enabled ?? true,
32
+ },
33
+ update: {
34
+ name: channel.name,
35
+ description: channel.description,
36
+ enabled: channel.enabled ?? true,
37
+ },
38
+ });
39
+ }
40
+ }
41
+ /**
42
+ * Seed event types
43
+ */
44
+ export async function seedEventTypes(prisma, tenantId, eventTypes) {
45
+ for (const eventType of eventTypes) {
46
+ // Get the channel
47
+ const channel = await prisma.eventChannel.findFirst({
48
+ where: {
49
+ tenantId,
50
+ code: eventType.channelCode,
51
+ },
52
+ });
53
+ if (!channel) {
54
+ throw new Error(`Channel ${eventType.channelCode} not found for tenant ${tenantId}`);
55
+ }
56
+ await prisma.eventType.upsert({
57
+ where: {
58
+ tenantId_code: {
59
+ tenantId,
60
+ code: eventType.code,
61
+ },
62
+ },
63
+ create: {
64
+ tenantId,
65
+ channelId: channel.id,
66
+ code: eventType.code,
67
+ name: eventType.name,
68
+ description: eventType.description,
69
+ payloadSchema: eventType.payloadSchema,
70
+ enabled: eventType.enabled ?? true,
71
+ },
72
+ update: {
73
+ channelId: channel.id,
74
+ name: eventType.name,
75
+ description: eventType.description,
76
+ payloadSchema: eventType.payloadSchema,
77
+ enabled: eventType.enabled ?? true,
78
+ },
79
+ });
80
+ }
81
+ }
82
+ /**
83
+ * Seed event handlers
84
+ */
85
+ export async function seedEventHandlers(prisma, tenantId, handlers) {
86
+ for (const handler of handlers) {
87
+ // Get the event type
88
+ const eventType = await prisma.eventType.findFirst({
89
+ where: {
90
+ tenantId,
91
+ code: handler.eventTypeCode,
92
+ },
93
+ });
94
+ if (!eventType) {
95
+ throw new Error(`Event type ${handler.eventTypeCode} not found for tenant ${tenantId}`);
96
+ }
97
+ // Check if handler already exists
98
+ const existing = await prisma.eventHandler.findFirst({
99
+ where: {
100
+ tenantId,
101
+ eventTypeId: eventType.id,
102
+ name: handler.name,
103
+ },
104
+ });
105
+ if (existing) {
106
+ await prisma.eventHandler.update({
107
+ where: { id: existing.id },
108
+ data: {
109
+ description: handler.description,
110
+ handlerType: handler.handlerType,
111
+ config: handler.config,
112
+ priority: handler.priority ?? 100,
113
+ enabled: handler.enabled ?? true,
114
+ maxRetries: handler.maxRetries ?? 3,
115
+ retryDelayMs: handler.retryDelayMs ?? 1000,
116
+ filterCondition: handler.filterCondition,
117
+ },
118
+ });
119
+ }
120
+ else {
121
+ await prisma.eventHandler.create({
122
+ data: {
123
+ tenantId,
124
+ eventTypeId: eventType.id,
125
+ name: handler.name,
126
+ description: handler.description,
127
+ handlerType: handler.handlerType,
128
+ config: handler.config,
129
+ priority: handler.priority ?? 100,
130
+ enabled: handler.enabled ?? true,
131
+ maxRetries: handler.maxRetries ?? 3,
132
+ retryDelayMs: handler.retryDelayMs ?? 1000,
133
+ filterCondition: handler.filterCondition,
134
+ },
135
+ });
136
+ }
137
+ }
138
+ }
139
+ // ============================================================================
140
+ // Contract Event Types (Mortgage Service)
141
+ // ============================================================================
142
+ /**
143
+ * Contract event channels
144
+ */
145
+ export const CONTRACT_CHANNELS = [
146
+ {
147
+ code: 'CONTRACTS',
148
+ name: 'Contract Events',
149
+ description: 'Events related to contract lifecycle (creation, activation, termination)',
150
+ },
151
+ {
152
+ code: 'PAYMENTS',
153
+ name: 'Payment Events',
154
+ description: 'Events related to payments and installments',
155
+ },
156
+ {
157
+ code: 'DOCUMENTS',
158
+ name: 'Document Events',
159
+ description: 'Events related to document upload, approval, and rejection',
160
+ },
161
+ {
162
+ code: 'WORKFLOW',
163
+ name: 'Workflow Events',
164
+ description: 'Events related to workflow steps and phases',
165
+ },
166
+ ];
167
+ /**
168
+ * Contract event types
169
+ */
170
+ export const CONTRACT_EVENT_TYPES = [
171
+ // Contract lifecycle events
172
+ {
173
+ code: 'CONTRACT_CREATED',
174
+ name: 'Contract Created',
175
+ description: 'Fired when a new contract is created',
176
+ channelCode: 'CONTRACTS',
177
+ },
178
+ {
179
+ code: 'CONTRACT_ACTIVATED',
180
+ name: 'Contract Activated',
181
+ description: 'Fired when a contract is activated',
182
+ channelCode: 'CONTRACTS',
183
+ },
184
+ {
185
+ code: 'CONTRACT_TERMINATED',
186
+ name: 'Contract Terminated',
187
+ description: 'Fired when a contract is terminated',
188
+ channelCode: 'CONTRACTS',
189
+ },
190
+ {
191
+ code: 'CONTRACT_TERMINATION_REQUESTED',
192
+ name: 'Contract Termination Requested',
193
+ description: 'Fired when a contract termination is requested',
194
+ channelCode: 'CONTRACTS',
195
+ },
196
+ {
197
+ code: 'CONTRACT_TERMINATION_APPROVED',
198
+ name: 'Contract Termination Approved',
199
+ description: 'Fired when a contract termination is approved',
200
+ channelCode: 'CONTRACTS',
201
+ },
202
+ // Payment events
203
+ {
204
+ code: 'PAYMENT_RECEIVED',
205
+ name: 'Payment Received',
206
+ description: 'Fired when a payment is successfully received',
207
+ channelCode: 'PAYMENTS',
208
+ },
209
+ {
210
+ code: 'PAYMENT_FAILED',
211
+ name: 'Payment Failed',
212
+ description: 'Fired when a payment fails',
213
+ channelCode: 'PAYMENTS',
214
+ },
215
+ {
216
+ code: 'PAYMENT_DUE_REMINDER',
217
+ name: 'Payment Due Reminder',
218
+ description: 'Fired to remind buyer of upcoming payment',
219
+ channelCode: 'PAYMENTS',
220
+ },
221
+ {
222
+ code: 'PAYMENT_OVERDUE',
223
+ name: 'Payment Overdue',
224
+ description: 'Fired when a payment is overdue',
225
+ channelCode: 'PAYMENTS',
226
+ },
227
+ // Document events
228
+ {
229
+ code: 'DOCUMENT_UPLOADED',
230
+ name: 'Document Uploaded',
231
+ description: 'Fired when a document is uploaded',
232
+ channelCode: 'DOCUMENTS',
233
+ },
234
+ {
235
+ code: 'DOCUMENT_APPROVED',
236
+ name: 'Document Approved',
237
+ description: 'Fired when a document is approved',
238
+ channelCode: 'DOCUMENTS',
239
+ },
240
+ {
241
+ code: 'DOCUMENT_REJECTED',
242
+ name: 'Document Rejected',
243
+ description: 'Fired when a document is rejected',
244
+ channelCode: 'DOCUMENTS',
245
+ },
246
+ // Workflow events
247
+ {
248
+ code: 'PHASE_ACTIVATED',
249
+ name: 'Phase Activated',
250
+ description: 'Fired when a contract phase is activated',
251
+ channelCode: 'WORKFLOW',
252
+ },
253
+ {
254
+ code: 'PHASE_COMPLETED',
255
+ name: 'Phase Completed',
256
+ description: 'Fired when a contract phase is completed',
257
+ channelCode: 'WORKFLOW',
258
+ },
259
+ {
260
+ code: 'STEP_COMPLETED',
261
+ name: 'Step Completed',
262
+ description: 'Fired when a workflow step is completed',
263
+ channelCode: 'WORKFLOW',
264
+ },
265
+ {
266
+ code: 'STEP_REJECTED',
267
+ name: 'Step Rejected',
268
+ description: 'Fired when a workflow step is rejected',
269
+ channelCode: 'WORKFLOW',
270
+ },
271
+ ];
272
+ /**
273
+ * Seed all contract event types
274
+ */
275
+ export async function seedContractEventTypes(prisma, tenantId) {
276
+ console.log(`Seeding contract event types for tenant ${tenantId}...`);
277
+ // Seed channels
278
+ await seedEventChannels(prisma, tenantId, CONTRACT_CHANNELS);
279
+ console.log(`✓ Seeded ${CONTRACT_CHANNELS.length} event channels`);
280
+ // Seed event types
281
+ await seedEventTypes(prisma, tenantId, CONTRACT_EVENT_TYPES);
282
+ console.log(`✓ Seeded ${CONTRACT_EVENT_TYPES.length} event types`);
283
+ console.log('✓ Contract event types seeded successfully');
284
+ }
285
+ /**
286
+ * Seed example email notification handlers
287
+ *
288
+ * Note: In production, admins would configure these via the UI.
289
+ * This is just an example for testing.
290
+ */
291
+ export async function seedExampleEmailHandlers(prisma, tenantId) {
292
+ const handlers = [
293
+ {
294
+ eventTypeCode: 'CONTRACT_CREATED',
295
+ name: 'Send Contract Created Email',
296
+ description: 'Send email notification when contract is created',
297
+ handlerType: 'SEND_EMAIL',
298
+ config: {
299
+ notificationType: 'CONTRACT_CREATED',
300
+ recipientPath: '$.buyerEmail',
301
+ templateData: {
302
+ contractNumber: '$.contractNumber',
303
+ propertyName: '$.propertyName',
304
+ totalAmount: '$.totalAmount',
305
+ },
306
+ },
307
+ priority: 100,
308
+ },
309
+ {
310
+ eventTypeCode: 'PAYMENT_RECEIVED',
311
+ name: 'Send Payment Received Email',
312
+ description: 'Send email notification when payment is received',
313
+ handlerType: 'SEND_EMAIL',
314
+ config: {
315
+ notificationType: 'PAYMENT_RECEIVED',
316
+ recipientPath: '$.buyerEmail',
317
+ templateData: {
318
+ contractNumber: '$.contractNumber',
319
+ paymentAmount: '$.amount',
320
+ paymentReference: '$.reference',
321
+ },
322
+ },
323
+ priority: 100,
324
+ },
325
+ {
326
+ eventTypeCode: 'DOCUMENT_APPROVED',
327
+ name: 'Send Document Approved Email',
328
+ description: 'Send email notification when document is approved',
329
+ handlerType: 'SEND_EMAIL',
330
+ config: {
331
+ notificationType: 'DOCUMENT_APPROVED',
332
+ recipientPath: '$.buyerEmail',
333
+ templateData: {
334
+ documentType: '$.documentType',
335
+ stepName: '$.stepName',
336
+ },
337
+ },
338
+ priority: 100,
339
+ },
340
+ ];
341
+ await seedEventHandlers(prisma, tenantId, handlers);
342
+ console.log(`✓ Seeded ${handlers.length} example email handlers`);
343
+ }
@@ -0,0 +1,230 @@
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 { PrismaClient } from '../../../generated/client/client';
35
+ import type { EmitEventInput, WorkflowEventData, ProcessEventResult } from './workflow-types';
36
+ import { EventPublisher } from '../notifications/event-publisher';
37
+ /**
38
+ * Automation registry interface for RUN_AUTOMATION handlers
39
+ */
40
+ export interface AutomationRegistry {
41
+ get(automationName: string): ((inputs: Record<string, unknown>, tenantId: string) => Promise<unknown>) | undefined;
42
+ register(automationName: string, handler: (inputs: Record<string, unknown>, tenantId: string) => Promise<unknown>): void;
43
+ }
44
+ /**
45
+ * Service registry interface for internal handlers (legacy support)
46
+ */
47
+ export interface ServiceRegistry {
48
+ get(serviceName: string): any | undefined;
49
+ register(serviceName: string, service: any): void;
50
+ }
51
+ export declare class WorkflowEventService {
52
+ private prisma;
53
+ private automationRegistry;
54
+ private eventPublisher;
55
+ constructor(prisma: PrismaClient, automationRegistry?: AutomationRegistry, eventPublisher?: EventPublisher);
56
+ /**
57
+ * Register an automation for RUN_AUTOMATION handlers
58
+ *
59
+ * Automations are business logic functions that can be triggered by events.
60
+ * Example: "calculateLateFee", "sendWelcomePackage", "archiveContract"
61
+ */
62
+ registerAutomation(name: string, handler: (inputs: Record<string, unknown>, tenantId: string) => Promise<unknown>): void;
63
+ /**
64
+ * Emit an event
65
+ *
66
+ * This creates an event record and optionally processes it immediately
67
+ * or leaves it for async processing by a worker.
68
+ *
69
+ * @param tenantId - Tenant context
70
+ * @param input - Event details
71
+ * @param processImmediately - Whether to process handlers now (default: false)
72
+ */
73
+ emit(tenantId: string, input: EmitEventInput, processImmediately?: boolean): Promise<WorkflowEventData>;
74
+ /**
75
+ * Process an event by executing all registered handlers
76
+ *
77
+ * This is typically called by a worker/queue processor,
78
+ * but can also be called synchronously for simple cases.
79
+ */
80
+ processEvent(eventId: string): Promise<ProcessEventResult>;
81
+ /**
82
+ * Get pending events for processing (for worker/queue)
83
+ */
84
+ getPendingEvents(tenantId?: string, limit?: number): Promise<WorkflowEventData[]>;
85
+ /**
86
+ * Get events by correlation ID (for tracing related events)
87
+ */
88
+ getEventsByCorrelation(tenantId: string, correlationId: string): Promise<WorkflowEventData[]>;
89
+ /**
90
+ * Get event with executions (for debugging/auditing)
91
+ */
92
+ getEventWithExecutions(tenantId: string, eventId: string): Promise<({
93
+ eventType: {
94
+ channel: {
95
+ name: string;
96
+ id: string;
97
+ tenantId: string;
98
+ createdAt: Date;
99
+ updatedAt: Date;
100
+ description: string | null;
101
+ enabled: boolean;
102
+ code: string;
103
+ };
104
+ } & {
105
+ name: string;
106
+ id: string;
107
+ tenantId: string;
108
+ createdAt: Date;
109
+ updatedAt: Date;
110
+ description: string | null;
111
+ enabled: boolean;
112
+ code: string;
113
+ channelId: string;
114
+ payloadSchema: import("@prisma/client/runtime/client").JsonValue | null;
115
+ };
116
+ executions: ({
117
+ handler: {
118
+ name: string;
119
+ id: string;
120
+ handlerType: import("./workflow-types").EventHandlerType;
121
+ };
122
+ } & {
123
+ id: string;
124
+ createdAt: Date;
125
+ status: import("./workflow-types").ExecutionStatus;
126
+ handlerId: string;
127
+ completedAt: Date | null;
128
+ error: string | null;
129
+ eventId: string;
130
+ attempt: number;
131
+ input: import("@prisma/client/runtime/client").JsonValue | null;
132
+ output: import("@prisma/client/runtime/client").JsonValue | null;
133
+ errorCode: string | null;
134
+ startedAt: Date | null;
135
+ durationMs: number | null;
136
+ })[];
137
+ } & {
138
+ id: string;
139
+ tenantId: string;
140
+ createdAt: Date;
141
+ status: import("./workflow-types").WorkflowEventStatus;
142
+ processedAt: Date | null;
143
+ actorId: string | null;
144
+ actorType: import("./workflow-types").ActorType;
145
+ eventTypeId: string;
146
+ payload: import("@prisma/client/runtime/client").JsonValue;
147
+ correlationId: string | null;
148
+ causationId: string | null;
149
+ source: string;
150
+ error: string | null;
151
+ }) | null>;
152
+ /**
153
+ * Execute a handler based on its type
154
+ *
155
+ * Handler types are business-friendly names that abstract the underlying implementation:
156
+ * - SEND_EMAIL: Send email via notification service (SNS → SQS → SES)
157
+ * - SEND_SMS: Send SMS via notification service
158
+ * - SEND_PUSH: Send push notification via notification service
159
+ * - CALL_WEBHOOK: Make HTTP request to external URL
160
+ * - ADVANCE_WORKFLOW: Move workflow steps forward/backward
161
+ * - RUN_AUTOMATION: Execute registered business logic automation
162
+ */
163
+ private executeHandler;
164
+ /**
165
+ * Execute SEND_EMAIL handler
166
+ *
167
+ * Sends an email via the notification service using SNS → SQS → SES.
168
+ * Business users configure: template, recipient, and template data.
169
+ */
170
+ private executeSendEmailHandler;
171
+ /**
172
+ * Execute SEND_SMS handler
173
+ *
174
+ * Sends an SMS via the notification service.
175
+ * Business users configure: template, recipient phone, and template data.
176
+ */
177
+ private executeSendSmsHandler;
178
+ /**
179
+ * Execute SEND_PUSH handler
180
+ *
181
+ * Sends a push notification via the notification service.
182
+ * Business users configure: template, recipient user, and template data.
183
+ */
184
+ private executeSendPushHandler;
185
+ /**
186
+ * Build notification payload from config and event payload
187
+ */
188
+ private buildNotificationPayload;
189
+ /**
190
+ * Execute CALL_WEBHOOK handler
191
+ *
192
+ * Makes an HTTP request to an external URL.
193
+ * Business users configure: URL, method, headers, and body mapping.
194
+ */
195
+ private executeCallWebhookHandler;
196
+ /**
197
+ * Execute ADVANCE_WORKFLOW handler
198
+ *
199
+ * Advances or modifies workflow state.
200
+ * Business users configure: action (approve/reject/skip), step path, and data.
201
+ */
202
+ private executeAdvanceWorkflowHandler;
203
+ /**
204
+ * Execute RUN_AUTOMATION handler
205
+ *
206
+ * Runs a registered business logic automation.
207
+ * Business users select from pre-defined automations like
208
+ * "Calculate Mortgage Payment", "Generate Contract", etc.
209
+ */
210
+ private executeRunAutomationHandler;
211
+ /**
212
+ * Evaluate a filter condition against the payload
213
+ */
214
+ private evaluateFilterCondition;
215
+ /**
216
+ * Transform payload using a mapping
217
+ */
218
+ private transformPayload;
219
+ /**
220
+ * Resolve a dot-notation path in an object
221
+ */
222
+ private resolvePath;
223
+ }
224
+ /**
225
+ * Create a workflow event service instance
226
+ *
227
+ * @param prisma - Prisma client for database access
228
+ * @param automationRegistry - Optional registry of business automations
229
+ */
230
+ export declare function createWorkflowEventService(prisma: PrismaClient, automationRegistry?: AutomationRegistry): WorkflowEventService;