/** * Workflow-specific implementation of IEventPublisher interface * This adapter provides event publishing for workflow contexts, publishing * events to both the workflow stream and notification channels. */ import type { IEventPublisher } from '@alga-psa/types'; import type { PublishOptions } from '@alga-psa/event-bus/publishers'; /** * Publish workflow-originated ticket events through the shared event bus. * * Inbound email ticket creation runs from shared workflow code, not the Next.js * ticket action path. Publishing through @alga-psa/event-bus keeps the stream * names and fanout channels aligned with the app subscribers (emailservice::v7 * and internal-notifications). Do not write raw Redis stream names here; they * can drift from the configured subscriber channels. */ async function publishNotificationEvent( eventType: string, payload: Record, options?: PublishOptions ): Promise { try { const { publishEvent } = await import('@alga-psa/event-bus/publishers'); await publishEvent({ eventType: eventType as any, payload } as any, options); console.log(`[WorkflowEventPublisher] Published ${eventType} through event bus`, { tenantId: payload.tenantId, ticketId: payload.ticketId, channel: options?.channel, }); } catch (error) { console.error(`[WorkflowEventPublisher] Failed to publish ${eventType} through event bus:`, error); // Don't throw - notification failure shouldn't break ticket operations } } export class WorkflowEventPublisher implements IEventPublisher { private readonly suppressCommentEmail: boolean; // suppressCommentEmail keeps comment events on the in-app channel only. Used for the // first comment on a new inbound-email ticket, which the TICKET_CREATED email already covers. constructor(options?: { suppressCommentEmail?: boolean }) { this.suppressCommentEmail = options?.suppressCommentEmail ?? false; } async publishTicketCreated(data: { tenantId: string; ticketId: string; userId?: string; metadata?: Record; }): Promise { const payload = { tenantId: data.tenantId, ticketId: data.ticketId, userId: data.userId || data.ticketId, // fallback for schema validation ...data.metadata }; await publishNotificationEvent('TICKET_CREATED', payload); } async publishTicketUpdated(data: { tenantId: string; ticketId: string; userId?: string; changes: Record; metadata?: Record; }): Promise { const payload = { tenantId: data.tenantId, ticketId: data.ticketId, userId: data.userId || data.ticketId, // fallback for schema validation changes: data.changes, ...data.metadata }; await publishNotificationEvent('TICKET_UPDATED', payload); } async publishTicketClosed(data: { tenantId: string; ticketId: string; userId?: string; metadata?: Record; }): Promise { const payload = { tenantId: data.tenantId, ticketId: data.ticketId, userId: data.userId || data.ticketId, // fallback for schema validation ...data.metadata }; await publishNotificationEvent('TICKET_CLOSED', payload); } async publishCommentCreated(data: { tenantId: string; ticketId: string; commentId: string; userId?: string; metadata?: Record; }): Promise { const payload = { tenantId: data.tenantId, ticketId: data.ticketId, userId: data.userId || data.ticketId, // fallback for schema validation comment: { id: data.commentId, content: data.metadata?.content || '', author: data.metadata?.author || 'System', isInternal: data.metadata?.isInternal || false } }; // Inbound replies fan out to internal + email channels (like the other publish* // methods) so the assigned tech/resources are emailed. The email subscriber excludes // the comment author and only emails external contacts for agent-authored comments, // so client replies never email the client back. The new-ticket first comment stays // in-app only (suppressCommentEmail) to avoid duplicating the TICKET_CREATED email. const options = this.suppressCommentEmail ? { channel: 'internal-notifications' } : undefined; await publishNotificationEvent('TICKET_COMMENT_ADDED', payload, options); } /** * Publish ticket assigned event - used when a ticket is assigned to an agent */ async publishTicketAssigned(data: { tenantId: string; ticketId: string; userId: string; assignedByUserId?: string; }): Promise { const payload = { tenantId: data.tenantId, ticketId: data.ticketId, userId: data.userId, assignedByUserId: data.assignedByUserId }; await publishNotificationEvent('TICKET_ASSIGNED', payload); } }