// Convenience wrappers around closeTicket() aimed at Contact Center agent // intents. Each one calls closeTicket with a specific resolution + a // standardized audit-trail comment, plus any intent-specific side effects // (like creating a formal "Duplicate" issueLink). // // These are thin — the real work lives in issues.js closeTicket + // setSSValidatorFields. If a caller wants full control, they can still hit // /ticket/:key/close directly. import logger from '../../utilities/logger.js'; import { jiraClient } from './client.js'; import { closeTicket } from './issues.js'; const KEY_RE = /^[A-Z]+-\d+$/; /** * "The caller confirms it's fixed." Resolves the ticket as Done. * * @param {string} key * @param {Object} [opts] * @param {string} [opts.comment] Extra context to append; a default * is used if omitted. * @param {string} [opts.subType] Feeds the validator-field default * lookup in ssCloseDefaults.js. * @param {string} [opts.component] Override the default component. * @param {string} [opts.businessService] * @param {string} [opts.system] * @param {string} [opts.cause] Defaults to "Unknown" for CC-driven * closes, which is the tenant's designed * catch-all Cause Code. * @param {boolean} [opts.internal=true] Post comment as internal (visible only * to Service Desk Team). Default true * because CC-agent notes should not * show to the customer as public replies. */ export async function confirmFixed(key, opts = {}) { if (!key || !KEY_RE.test(key)) throw new Error(`Invalid ticket key: "${key}"`); const { comment, subType, component, businessService, system, cause, internal = true, } = opts; const finalComment = comment || 'Closed via WxCC AI agent: caller confirmed the issue is resolved.'; return closeTicket(key, { resolution: 'Done', comment: finalComment, internal, component, businessService, system, cause, subType, }); } /** * "The caller wants to cancel their ticket." Resolves as Won't Do. * * @param {string} key * @param {Object} [opts] * @param {string} [opts.reason] Freeform reason to include in the * comment (e.g. "customer says the * issue self-resolved"). * @param {string} [opts.subType] Feeds default lookup. * @param {string} [opts.component] * @param {string} [opts.businessService] * @param {string} [opts.system] * @param {string} [opts.cause] Defaults to whatever the subType map * says (usually "Unknown"). * @param {boolean} [opts.internal=true] */ export async function customerCancelled(key, opts = {}) { if (!key || !KEY_RE.test(key)) throw new Error(`Invalid ticket key: "${key}"`); const { reason, subType, component, businessService, system, cause, internal = true, } = opts; const comment = reason ? `Closed via WxCC AI agent: caller requested cancellation. Reason: ${reason}` : 'Closed via WxCC AI agent: caller requested cancellation.'; return closeTicket(key, { resolution: "Won't Do", comment, internal, component, businessService, system, cause, subType, }); } /** * "This ticket is a duplicate of SS-XXXX." * * Closes the given ticket with resolution=Duplicate AND creates a formal * `Duplicate` issue link pointing to the primary ticket. Both operations * are attempted; if the link creation fails, the close still completes * (the link error is logged and surfaced in the response). * * @param {string} key * @param {Object} opts * @param {string} opts.primaryKey The ticket this is a duplicate OF (required). * @param {string} [opts.comment] Extra context; a default is used if omitted. * @param {string} [opts.subType] * @param {string} [opts.component] * @param {string} [opts.businessService] * @param {string} [opts.system] * @param {string} [opts.cause] * @param {boolean} [opts.internal=true] */ export async function markDuplicate(key, opts = {}) { if (!key || !KEY_RE.test(key)) throw new Error(`Invalid ticket key: "${key}"`); const { primaryKey, comment, subType, component, businessService, system, cause, internal = true, } = opts; if (!primaryKey || !KEY_RE.test(primaryKey)) { throw new Error(`markDuplicate requires primaryKey; got "${primaryKey}"`); } if (primaryKey === key) { throw new Error(`markDuplicate: primaryKey (${primaryKey}) cannot equal the ticket being closed (${key})`); } const finalComment = comment || `Closed via WxCC AI agent: duplicate of ${primaryKey}.`; // Try to create the formal Duplicate link first. If Jira rejects it (e.g. // primary doesn't exist), abort — closing without the link would lose // the connection. let linkResult = null; try { await jiraClient.post('/rest/api/3/issueLink', { type: { name: 'Duplicate' }, inwardIssue: { key: key }, outwardIssue: { key: primaryKey }, }, { headers: { 'Content-Type': 'application/json' } }); linkResult = { ok: true, primaryKey, linkType: 'Duplicate' }; logger.info('markDuplicate: created formal issueLink', { key, primaryKey }); } catch (err) { const detail = err.response?.data?.errorMessages?.join('; ') || err.message; logger.warn('markDuplicate: issueLink create failed; will still close', { key, primaryKey, error: detail, status: err.response?.status, }); linkResult = { ok: false, primaryKey, error: detail }; // We continue: the close-with-comment still records the intent. } const closeResult = await closeTicket(key, { resolution: 'Duplicate', comment: finalComment, internal, component, businessService, system, cause, subType, }); return { ...closeResult, issueLink: linkResult }; }