servchan/src/services/webexService.js
jmcqueen c2e98d105e Initial commit: ServChan Webex bot for ServiceChannel work orders.
ServiceChannel webhook processor, proposal approval cards, attachment auto-post,
CollabSupport commands, and Docker deployment configuration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 15:12:08 -04:00

101 lines
No EOL
4.3 KiB
JavaScript

/**
* 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;