/** * SLA Plugin Validation Functions * * This module contains validation functions that enforce conditional field requirements * for SLA definitions. Fields may be mandatory, optional, or restricted depending on * the values of control fields. * * @see ./SLA-VALIDATION-BEHAVIORS.md for detailed documentation of all 11 validation behaviors */ import type { Diagnostics, ObjectShape } from '@servicenow/sdk-build-core' import { toReference } from '../utils' export const DEFAULT_WHEN_TO_RESUME = 'on_condition' export const DEFAULT_WHEN_TO_CANCEL = 'on_condition' export const DEFAULT_TIMEZONE_SOURCE = 'task.caller_id.time_zone' export const DEFAULT_RETROACTIVE_PAUSE = true export const DEFAULT_NO_SCHEDULE_ID = '38fa64edc0a8016400f4a5724b0434b8' export const ERROR_MESSAGES = { MANDATORY_FIELD: (field: string, reason: string) => `Field '${field}' is required ${reason}`, RESTRICTED_FIELD: (field: string, reason: string) => `Field '${field}' cannot be specified ${reason}. Remove this field from your SLA definition.`, RESTRICTED_WITH_DEFAULT: (field: string, reason: string, defaultValue: unknown) => `Field '${field}' cannot be specified ${reason}. The default value '${defaultValue}' will be used automatically.`, WARNING_FIELD_PERSISTENCE: (field: string, reason: string) => `Field '${field}' is restricted ${reason}. If this value came from an existing SLA definition, it will be preserved but cannot be modified. If you're creating a new SLA, remove this field.`, } /** * Validates user-specified duration fields (Behavior 1) */ export function validateUserSpecifiedDuration(config: ObjectShape, diagnostics: Diagnostics) { const duration = config.get('duration') // Duration is mandatory when durationType is empty/"User specified" if (!duration || duration.isUndefined()) { diagnostics.error( config, ERROR_MESSAGES.MANDATORY_FIELD('duration', 'when durationType is empty (user specified duration)') ) } // Note: Schedule validation is handled in validateSlaDefinitionSchedule based on scheduleSource } /** * Validates relative duration restrictions (Behavior 2) */ export function validateRelativeDuration( config: ObjectShape, conditions: ObjectShape | undefined, diagnostics: Diagnostics ) { // When durationType is a relative duration, duration, conditions.pause, conditions.resume, and whenTo.resume // are restricted fields. Existing values from XML are preserved (pass-through). // We issue a warning to inform the user these fields are managed by the relative duration. const duration = config.get('duration') const pauseCondition = conditions?.get('pause') const resumeCondition = conditions?.get('resume') const whenTo = config.get('whenTo')?.ifDefined()?.asObject() const whenToResume = whenTo?.get('resume') // duration field is ignored when using relative duration if (duration && !duration.isUndefined()) { diagnostics.hint( duration, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('duration', 'when durationType is a relative duration') ) } if (pauseCondition && !pauseCondition.isUndefined()) { diagnostics.hint( pauseCondition, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('conditions.pause', 'when durationType is a relative duration') ) } if (resumeCondition && !resumeCondition.isUndefined()) { diagnostics.hint( resumeCondition, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('conditions.resume', 'when durationType is a relative duration') ) } if (whenToResume && !whenToResume.isUndefined()) { diagnostics.hint( whenToResume, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('whenTo.resume', 'when durationType is a relative duration') ) } } /** * Validates retroactive start requirements (Behaviors 6 & 8) */ export function validateRetroactiveStart( retroactive: ObjectShape, isUserSpecifiedDuration: boolean, diagnostics: Diagnostics ) { const setStartTo = retroactive.get('setStartTo') const retroactivePause = retroactive.get('pause') // setStartTo is mandatory when retroactive.start is true const setStartToValue = setStartTo?.ifString()?.getValue() if (!setStartToValue) { diagnostics.error( retroactive, ERROR_MESSAGES.MANDATORY_FIELD('retroactive.setStartTo', 'when retroactive.start is true') ) } // Behavior 8: retroactive.start + relative duration restricts retroactive.pause // Existing values from XML are preserved (pass-through) with a warning. if (!isUserSpecifiedDuration && retroactivePause && !retroactivePause.isUndefined()) { diagnostics.hint( retroactivePause, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE( 'retroactive.pause', 'when retroactive.start is true and durationType is a relative duration' ) ) } } /** * Validates that retroactive.pause and retroactive.setStartTo are not set when retroactive.start is false (Behavior 7) */ export function validateRetroactiveFieldsWithoutStart(retroactive: ObjectShape | undefined, diagnostics: Diagnostics) { if (!retroactive) { return } const setStartTo = retroactive.get('setStartTo') const retroactivePause = retroactive.get('pause') // setStartTo requires retroactive.start to be true (warning for existing values) if (setStartTo && !setStartTo.isUndefined()) { const value = setStartTo.ifString()?.getValue() if (value) { diagnostics.hint( setStartTo, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('retroactive.setStartTo', 'unless retroactive.start is true') ) } } // retroactive.pause requires retroactive.start to be true (warning for existing values) if (retroactivePause && !retroactivePause.isUndefined()) { const value = retroactivePause.ifBoolean()?.getValue() if (value !== undefined) { diagnostics.hint( retroactivePause, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('retroactive.pause', 'unless retroactive.start is true') ) } } } /** * Validates no_schedule restrictions (Behavior 4) */ export function validateNoScheduleRestrictions(config: ObjectShape, diagnostics: Diagnostics) { const schedule = config.get('schedule') const timezoneSource = config.get('timezoneSource') const timezone = config.get('timezone') const scheduleSourceField = config.get('scheduleSourceField') // Schedule field: warn if provided, since default is forced if (schedule && !schedule.isUndefined()) { const value = toReference(schedule) if (value) { diagnostics.hint( schedule, ERROR_MESSAGES.RESTRICTED_WITH_DEFAULT( 'schedule', 'when scheduleSource is "no_schedule"', DEFAULT_NO_SCHEDULE_ID ) ) } } // timezoneSource: existing values are preserved with a warning if (timezoneSource && !timezoneSource.isUndefined()) { const value = timezoneSource.ifString()?.getValue() if (value && value !== DEFAULT_TIMEZONE_SOURCE) { diagnostics.hint( timezoneSource, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('timezoneSource', 'when scheduleSource is "no_schedule"') ) } } // timezone: existing values are preserved with a warning if (timezone && !timezone.isUndefined()) { const value = timezone.ifString()?.getValue() if (value) { diagnostics.hint( timezone, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('timezone', 'when scheduleSource is "no_schedule"') ) } } // scheduleSourceField: warn since it doesn't apply to no_schedule if (scheduleSourceField && !scheduleSourceField.isUndefined()) { const value = scheduleSourceField.ifString()?.getValue() if (value) { diagnostics.hint( scheduleSourceField, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('scheduleSourceField', 'when scheduleSource is "no_schedule"') ) } } } /** * Validates task_field schedule requirements (Behavior 5) */ export function validateTaskFieldSchedule(config: ObjectShape, diagnostics: Diagnostics) { const schedule = config.get('schedule') const scheduleSourceField = config.get('scheduleSourceField') const duration = config.get('duration') // schedule: warn since it doesn't apply when using task_field if (schedule && !schedule.isUndefined()) { const value = toReference(schedule) if (value) { diagnostics.hint( schedule, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('schedule', 'when scheduleSource is "task_field"') ) } } // duration: warn since it doesn't apply when using task_field if (duration && !duration.isUndefined()) { diagnostics.hint( duration, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('duration', 'when scheduleSource is "task_field"') ) } if (!scheduleSourceField || scheduleSourceField.isUndefined()) { diagnostics.error( config, ERROR_MESSAGES.MANDATORY_FIELD('scheduleSourceField', 'when scheduleSource is "task_field"') ) } } /** * Validates sla_definition schedule requirements (Behavior 3) */ export function validateSlaDefinitionSchedule(config: ObjectShape, diagnostics: Diagnostics) { const schedule = config.get('schedule') const scheduleValue = schedule && !schedule.isUndefined() ? toReference(schedule) : '' // Check for undefined, null, or empty string if (!schedule || schedule.isUndefined() || !scheduleValue) { diagnostics.error(config, ERROR_MESSAGES.MANDATORY_FIELD('schedule', 'when scheduleSource is "sla_definition"')) } } /** * Validates timezone source restrictions (Behavior 9) */ export function validateTimezoneSource( config: ObjectShape, timezoneSource: string | undefined, diagnostics: Diagnostics ) { const timezone = config.get('timezone') // Only allow timezone field when timezoneSource is 'sla.timezone' // Existing values from XML are preserved (pass-through) with a warning. if (timezoneSource !== 'sla.timezone' && timezone && !timezone.isUndefined()) { const currentValue = timezone.ifString()?.getValue() if (currentValue) { diagnostics.hint( timezone, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('timezone', 'unless timezoneSource is "sla.timezone"') ) } } } /** * Validates no_match resume restrictions (Behavior 10) */ export function validateNoMatchResume(conditions: ObjectShape | undefined, diagnostics: Diagnostics) { const resumeCondition = conditions?.get('resume') // When whenToResume is "no_match", conditions.resume is restricted. // Existing values from XML are preserved (pass-through) with a warning. if (resumeCondition && !resumeCondition.isUndefined()) { const currentValue = resumeCondition.ifString()?.getValue() if (currentValue) { diagnostics.hint( resumeCondition, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('conditions.resume', 'when whenToResume is "no_match"') ) } } } /** * Validates cancel condition restrictions (Behavior 11) */ export function validateCancelCondition( whenToCancel: string | undefined, conditions: ObjectShape | undefined, diagnostics: Diagnostics ) { const cancelCondition = conditions?.get('cancel') // Only allow conditions.cancel when whenToCancel is 'on_condition' // Existing values from XML are preserved (pass-through) with a warning. if (whenToCancel !== 'on_condition' && cancelCondition && !cancelCondition.isUndefined()) { const currentValue = cancelCondition.ifString()?.getValue() if (currentValue) { diagnostics.hint( cancelCondition, ERROR_MESSAGES.WARNING_FIELD_PERSISTENCE('conditions.cancel', 'unless whenToCancel is "on_condition"') ) } } }