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>
159 lines
5.5 KiB
JavaScript
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.'
|
|
);
|
|
}
|
|
}
|