Edits history of script submission #24031 for ' Close change request (servicenow)'

  • bunnative
    One script reply has been approved by the moderators
    Ap­pro­ved
    //native
    
    function authHeader(auth: RT.Servicenow) {
      return auth.token
        ? `Bearer ${auth.token}`
        : `Basic ${btoa(`${auth.username}:${auth.password}`)}`
    }
    
    // Change Management API fields come back as { value, display_value }.
    function value(field: any) {
      return field != null && typeof field === "object" ? field.value : field
    }
    
    function label(field: any) {
      return field != null && typeof field === "object"
        ? field.display_value
        : field
    }
    
    // Default change_request state values, in lifecycle order:
    // New, Assess, Authorize, Scheduled, Implement, Review, Closed.
    // Canceled (4) is never chosen.
    const LIFECYCLE = ["-5", "-4", "-3", "-2", "-1", "0", "3"]
    const CLOSED = "3"
    const REVIEW = "0"
    const MAX_STEPS = 10
    
    /**
     * Close Change Request
     * Move a change request to Closed with a close code and close notes, stepping forward through the next states the Change Management API allows (GET .../nextstates, then PATCH /api/sn_chg_rest/change/{sys_id}; documented in the Yokohama and Brazil API references). Moving a change forward can request its approvals (as the Request approval button does); this action never approves anything. It stops and returns the current state and the reason (approval pending, a transition condition not met, or a state ServiceNow did not apply) instead of forcing the change. Assumes the default state values (Closed = 3, Review = 0); close_code defaults are successful, successful_issues and unsuccessful.
     */
    export async function main(
      auth: RT.Servicenow,
      sys_id: string,
      close_code: string,
      close_notes: string
    ) {
      const url = new URL(
        `${auth.instance_url}/api/sn_chg_rest/change/${encodeURIComponent(sys_id)}`
      )
      const headers = {
        Authorization: authHeader(auth),
        Accept: "application/json",
      }
    
      async function request(target: URL, init?: RequestInit) {
        const response = await fetch(target, init ?? { headers })
        if (!response.ok) {
          throw new Error(`${response.status} ${await response.text()}`)
        }
        return ((await response.json()) as { result: any }).result
      }
    
      function patch(body: { [key: string]: any }) {
        return request(url, {
          method: "PATCH",
          headers: { ...headers, "Content-Type": "application/json" },
          body: JSON.stringify(body),
        })
      }
    
      const steps: { from: string; to: string; result_state: string }[] = []
      let record = await request(url)
      let closeFieldsSet = false
    
      function stop(reason: string, extra: { [key: string]: any } = {}) {
        return {
          closed: false,
          sys_id: value(record.sys_id),
          number: value(record.number),
          state: String(value(record.state)),
          state_label: label(record.state),
          approval: value(record.approval),
          reason,
          steps,
          ...extra,
        }
      }
    
      for (let i = 0; i < MAX_STEPS; i++) {
        const current = String(value(record.state))
        if (current === CLOSED) {
          return {
            closed: true,
            sys_id: value(record.sys_id),
            number: value(record.number),
            state: current,
            state_label: label(record.state),
            steps,
          }
        }
        const currentIndex = LIFECYCLE.indexOf(current)
        if (currentIndex === -1) {
          return stop(
            `State ${label(record.state)} (${current}) is not on the New -> Closed path (canceled, or a custom state).`
          )
        }
    
        // In Review, record the closure details first: a model's Review -> Closed
        // condition can depend on them.
        if (current === REVIEW && !closeFieldsSet) {
          record = await patch({ close_code, close_notes })
          closeFieldsSet = true
        }
    
        const next = await request(new URL(`${url}/nextstates`))
        const available: string[] = (next.available_states ?? []).map(String)
        // Model-driven changes describe each transition; type-driven ones do not.
        const transitions: any[] = (next.state_transitions ?? []).flat()
        const allowed =
          transitions.length > 0
            ? available.filter((s) =>
                transitions.some(
                  (t) => String(t.to_state) === s && t.transition_available
                )
              )
            : available
        const forward = allowed
          .filter((s) => LIFECYCLE.indexOf(s) > currentIndex)
          .sort((a, b) => LIFECYCLE.indexOf(a) - LIFECYCLE.indexOf(b))
    
        if (forward.length === 0) {
          const blocked = transitions
            .filter(
              (t) =>
                !t.transition_available &&
                LIFECYCLE.indexOf(String(t.to_state)) > currentIndex
            )
            .map((t) => ({
              to_state: String(t.to_state),
              to_state_label: next.state_label?.[t.to_state],
              failed_conditions: (t.conditions ?? [])
                .filter((c: any) => !c.passed)
                .map((c: any) => c.condition?.name),
            }))
          const approval = value(record.approval)
          const reason =
            approval === "requested"
              ? `Approval pending: the change is in ${label(record.state)} and waits for its approvers.`
              : approval === "rejected"
                ? `Approval rejected: the change is in ${label(record.state)}.`
                : `No forward state is available from ${label(record.state)} (${current}).`
          return stop(reason, {
            available_states: available,
            blocked_transitions: blocked,
          })
        }
    
        const target = forward.includes(CLOSED) ? CLOSED : forward[0]
        const updated = await patch(
          target === CLOSED
            ? { state: target, close_code, close_notes }
            : { state: target }
        )
        const ignored: string[] = updated.__meta?.ignoredFields ?? []
        steps.push({
          from: current,
          to: target,
          result_state: String(value(updated.state)),
        })
        if (ignored.includes("state") || String(value(updated.state)) === current) {
          record = updated
          return stop(
            `ServiceNow did not apply the move from ${current} to ${target}; the change is still in ${label(updated.state)}.`,
            { ignored_fields: ignored }
          )
        }
        record = updated
      }
    
      return stop(`Stopped after ${MAX_STEPS} transitions without reaching Closed.`)
    }
    

    Submitted by hugo989 7 hours ago