/** * src/services/webexService.js * * Thin service layer over the Webex bot client. * * Purpose: * - Provides a clean, ServChan-specific interface for Webex operations * - Improves testability (easy to mock the entire service) * - Central place for any future Webex business logic related to work orders * - Better isolation between webhookProcessor and raw botClient calls * * Created as part of the 2026-05-28 refactoring (step 4). */ import botClient from '../integrations/webex/botClient.js'; // default singleton for convenience import { logger } from '../utils/logger.js'; export class WebexService { /** * @param {object} [client] - Optional bot client instance. * Defaults to the shared botClient singleton. */ constructor(client = botClient) { this.client = client; } // ──────────────────────────────────────────────────────────────────────────── // Low-level passthroughs (kept for compatibility with current processor) // ──────────────────────────────────────────────────────────────────────────── async createRoom(title, teamId = null) { return this.client.createRoom(title, teamId); } async addMember(roomId, personEmail) { return this.client.addMember(roomId, personEmail); } async sendMarkdown(roomId, markdown, textFallback = null) { return this.client.sendMarkdown(roomId, markdown, textFallback); } async sendWithAttachment(roomId, buffer, fileName, contentType, text, fileUrl = null) { return this.client.sendWithAttachment(roomId, buffer, fileName, contentType, text, fileUrl); } // ──────────────────────────────────────────────────────────────────────────── // Higher-level ServChan-specific helpers (recommended for new code) // ──────────────────────────────────────────────────────────────────────────── /** * Creates a work order room with the standard ServChan title format. */ async createWorkOrderRoom(workOrder, teamId) { const title = `ServChan WO-${workOrder.Number} | Store ${workOrder.LocationStoreId} | ${workOrder.LocationName}`; const room = await this.createRoom(title, teamId); logger('webexService', `Created room for WO-${workOrder.Number}: ${room.id}`); return room; } /** * Adds the standard default members to a work order room. */ async addDefaultMembers(roomId, members = []) { if (!members.length) return; const results = await Promise.allSettled( members.map(email => this.addMember(roomId, email)) ); results.forEach((result, i) => { if (result.status === 'rejected') { logger('webexService:addDefaultMembers', `Failed to add ${members[i]}: ${result.reason?.message || result.reason}`, 'warn'); } }); } /** * Posts a message to a work order room. */ async postWorkOrderMessage(roomId, markdown) { return this.sendMarkdown(roomId, markdown); } // ──────────────────────────────────────────────────────────────────────────── // Adaptive Card support (for proposal approval cards) // ──────────────────────────────────────────────────────────────────────────── async sendAdaptiveCard(roomId, card, fallbackText) { return this.client.sendAdaptiveCard(roomId, card, fallbackText); } async deleteMessage(messageId) { return this.client.deleteMessage(messageId); } } // Convenience singleton (matches the pattern used elsewhere) export const webexService = new WebexService(); export default webexService;