servchan/src/commands/woConfirmed.js
jmcqueen cba047cb4e Add invoice approval cards, /confirmed close-out fallback, and /addNote.
Replace /completed with /confirmed that tries SC CONFIRMED then falls back to SC notes and ServChan close-out records when status is locked. Post invoice approval cards on PDF attach, track close-outs for cleanup, and add WO-space /addNote.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 12:59:54 -04:00

159 lines
5.5 KiB
JavaScript

// src/commands/woConfirmed.js
import botClient from '../integrations/webex/botClient.js';
import {
addWorkOrderNote,
getWorkOrderStatus,
updateWorkOrderStatus,
} from '../integrations/serviceChannel/client.js';
import db from '../db/mappings.js';
import { logger } from '../utils/logger.js';
import { getCompletedOperationsRoomId } from '../utils/servchanRoomTitle.js';
import { resolveDisplayName, resolveWoRoomContext } from '../utils/woRoomContext.js';
import { getWoCloseout, saveWoCloseout } from '../services/woCloseoutService.js';
function normalizeExtended(value) {
return (value || '').trim().toUpperCase();
}
function isTerminalExtended(extendedRaw) {
const extended = normalizeExtended(extendedRaw);
if (extended === 'CONFIRMED' || extended === 'CANCELLED') return true;
return extended === 'NO CHARGE' || extended.includes('NO CHARGE');
}
function buildConfirmationText(displayName) {
return `${displayName} has confirmed this WO resolution verified and approves closing it out.`;
}
function isScStatusLockedError(err) {
const msg = String(err?.message || err).toLowerCase();
return msg.includes('921') || msg.includes('invoiced') || msg.includes('status');
}
export async function handleWoConfirmed(bot, trigger) {
logger('wo:confirmed', 'HANDLER ENTERED');
const roomId = trigger.message?.roomId;
const isGroup = trigger.message?.roomType === 'group';
const ctx = await resolveWoRoomContext({ db, botClient, roomId, isGroup });
if (!ctx.ok) {
await bot.say(ctx.error);
return;
}
const { workOrderId: mappedWorkOrderId, parsed } = ctx;
const opsRoomId = getCompletedOperationsRoomId();
if (!opsRoomId) {
logger('wo:confirmed', 'COMPLETED_OPERATIONS_ROOM_ID not configured', 'warn');
await bot.say('The close-out ops space is not configured on this bot (`COMPLETED_OPERATIONS_ROOM_ID`).');
return;
}
const existingCloseout = await getWoCloseout(db, mappedWorkOrderId);
if (existingCloseout) {
await bot.say(
`Close-out was already recorded for this work order on ${new Date(existingCloseout.confirmedAt).toLocaleDateString('en-US')} by **${existingCloseout.confirmedBy}**.`
);
return;
}
const woStatus = await getWorkOrderStatus(mappedWorkOrderId);
if (!woStatus) {
await bot.say('Could not load this work order from ServiceChannel. Please try again.');
return;
}
const primary = (woStatus.primaryStatus || '').trim().toUpperCase();
if (primary !== 'COMPLETED') {
await bot.say(
`This command applies only to **COMPLETED** work orders. Current status: **${woStatus.primaryStatus || 'unknown'}**${woStatus.extendedStatus ? ` / ${woStatus.extendedStatus}` : ''}.`
);
return;
}
if (isTerminalExtended(woStatus.extendedStatus)) {
await bot.say(
`This work order is already in a terminal status (**${woStatus.extendedStatus}**). No action needed.`
);
return;
}
const displayName = await resolveDisplayName(botClient, trigger.personId);
const confirmationText = buildConfirmationText(displayName);
const scNote = `${displayName} confirmed WO resolution verified and approves closing it out.`;
let scStatusUpdated = false;
try {
await updateWorkOrderStatus(mappedWorkOrderId, {
primary: 'COMPLETED',
extended: 'CONFIRMED',
note: scNote,
});
scStatusUpdated = true;
logger(
'wo:confirmed',
`Set WO ${mappedWorkOrderId} to COMPLETED/CONFIRMED by ${displayName}`
);
} catch (err) {
logger(
'wo:confirmed',
`SC status update failed for WO ${mappedWorkOrderId}: ${err.message} — falling back to note-only close-out`,
'warn'
);
try {
await addWorkOrderNote(mappedWorkOrderId, scNote);
logger(
'wo:confirmed',
`Posted close-out note to SC WO ${mappedWorkOrderId} by ${displayName} (status API blocked)`
);
} catch (noteErr) {
logger('wo:confirmed', `Failed to post SC note for WO ${mappedWorkOrderId}: ${noteErr.message}`, 'error');
const hint = isScStatusLockedError(err)
? ' ServiceChannel does not allow CONFIRMED after invoice on this work order.'
: '';
await bot.say(
`Could not set ServiceChannel status to CONFIRMED and adding a work order note also failed:${hint} ${noteErr.message}`
);
return;
}
}
try {
await saveWoCloseout(db, {
workOrderId: mappedWorkOrderId,
confirmedBy: displayName,
scStatusUpdated,
noteText: scNote,
});
} catch (err) {
logger('wo:confirmed', `Failed to save close-out record for WO ${mappedWorkOrderId}: ${err.message}`, 'error');
}
const opsMessage = `${parsed.headerLine}\n${confirmationText}`;
try {
await botClient.sendMarkdown(opsRoomId, opsMessage);
logger(
'wo:confirmed',
`Posted close-out confirmation for WO-${parsed.woNumber} to ops room ${opsRoomId} by ${displayName}`
);
} catch (err) {
logger('wo:confirmed', `Failed to post to ops room: ${err.message}`, 'error');
await bot.say(
scStatusUpdated
? `ServiceChannel status was updated to CONFIRMED, but notifying the ops space failed: ${err.message}`
: `Close-out was noted on the work order in ServiceChannel, but notifying the ops space failed: ${err.message}`
);
return;
}
if (!scStatusUpdated) {
await bot.say(
'ServiceChannel would not allow CONFIRMED on this work order — close-out noted on the WO, recorded in ServChan, and ops notified. The SC webhook should post the note here shortly.'
);
}
}