Public/Support/CallQueue/New-TeamsCallQueueTrigger.ps1

# Module: TeamsFunctions
# Function: CallQueue
# Author: David Eberhardt
# Updated: 07-AUG-2023
# Status: RC




function New-TeamsCallQueueTrigger {
  <#
  .SYNOPSIS
    Setting one of the Exception Handling Triggers for a Call Queue
  .DESCRIPTION
    Helper function to set one of the Exception Handling Triggers for a Call Queue
  .PARAMETER Name
    Required. Name or Id of the Call Queue. Used only to display proper output message.
  .PARAMETER Trigger
    Required to define a Trigger. Either Overflow, Timeout or NoAgent
  .PARAMETER Threshold
    Optional. Available for Triggers Overflow & Timeout
    Overflow: Number of people waiting in the queue before the Action to trigger
    Timeout: Time in Seconds for the Action to trigger
    NoAgent: Not available
  .PARAMETER NoAgentApplyTo
    Optional. Only available for Trigger NoAgent. Instructs that the setting applies to AllCalls or only NewCalls
  .PARAMETER Prompt
    Optional. Prompt to play when is reaching threshold but before Action is taken
    String as a Path for a Recording or a Text-to-Voice string. Function determines Type
    Aliases for backward compatibility: SharedVoicemailTextToSpeechPrompt, SharedVoicemailAudioFile
  .PARAMETER Action
    Required. Action to be taken if the Queue size limit (Threshold) is reached
    Forward requires specification of Target
    Default: DisconnectWithBusy, Values: DisconnectWithBusy, Forward, VoiceMail, SharedVoiceMail
  .PARAMETER Target
    Situational. Required only if Action is not the default. UserPrincipalName of the Target
    For Overflow, default is DisconnectWithBusy, for Timeout it is Default, for NoAgent it is Queue
  .PARAMETER SetPromptForAllTypes
    Situational. Boolean Switch. This switch will apply any provided prompt to all valid parameters
    If a Prompt is used setting a Text-to-Voice string value this switch will apply the same value to TextToSpeechPrompt-
    parameters of the queue. If not provided, it will only set the prompt for the Action provided.
    This affects Disconnect, RedirectPerson, RedirectVoiceApp, RedirectPhoneNumber, RedirectVoicemail & SharedVoicemail
    Prompts that are not provided will not be set (in the example above, any Timeout parameters would not be affected).
  .PARAMETER EnableTranscription
    Situational. Boolean Switch. Requires specification of LanguageId
    Enables a transcription of the Voicemail message to be sent to the Group mailbox
  .PARAMETER EnableSystemPromptSuppression
    Situational. Boolean Switch. Requires specification of LanguageId
    Enables a transcription of the Voicemail message to be sent to the Group mailbox
  .PARAMETER LanguageId
    Optional Language Identifier indicating the language that is used to play shared voicemail prompts.
    This parameter becomes a required parameter If the Action is set to SharedVoicemail and the language is not yet set.
  .EXAMPLE
    New-TeamsCallQueueTrigger -Trigger Overflow -Action Forward -Target 'User@domain.com'
    Creates a new Overflow trigger that is set to Redirect to a Person in the Organisation.
  .EXAMPLE
    New-TeamsCallQueueTrigger -Trigger Timeout -Action SharedVoicemail -Target "My Group" -EnableTranscription -EnableSystemPromptSuppression
    Creates a new Timeout trigger that is set to SharedVoicemail forwarding to "My Group" and enabling Transcription
    It also enables the suppression of the System Prompt before forwarding to the Target Group
  .EXAMPLE
    New-TeamsCallQueueTrigger -Trigger NoAgent -Action Disconnect -Prompt "Sorry, we are closed" -SetPromptForAllTypes -LanguageId en-gb
    Creates a new NoAgent trigger that is set to Disconnect with a Text-to-Speech Prompt and Language set to English (UK)
  .EXAMPLE
    New-TeamsCallQueueTrigger -Trigger Overflow -Action Disconnect -Prompt "Sorry, we are busy" -SetPromptForAllTypes
    Creates a new Overflow trigger that is set to Disconnect with a Text-to-Speech Prompt.
    The Switch SetPromptForAllTypes will set the Prompt for all types, not only for Disconnect, in case it is to be switched later.
    NOTE: As this is a Text-to-Voice prompt, it requires a Language to be set. Parameter LanguageId must be set in conjunction
    with the output of this Cmdlet, the creation of the Call Queue will fail
  .INPUTS
    System.String
  .OUTPUTS
    System.Object
  .NOTES
    Audio Files, if not found will result in the prompt not being configured.
  .COMPONENT
    TeamsCallQueue
  .FUNCTIONALITY
    Sets one of the Exception Handling triggers for a Call Queue
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/Set-TeamsCallQueueTrigger.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/about_TeamsFunctions.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/
  #>


  [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')]
  [Alias('New-TeamsCQT')]
  [OutputType([PSCustomObject])]
  param (
    [Parameter(Mandatory, Position = 0, HelpMessage = 'Name or ID of the Call Queue')]
    [string]$Name,

    [Parameter(Mandatory, HelpMessage = 'Name or ID of the Call Queue')]
    [Validateset('Overflow', 'Timeout', 'NoAgent')]
    [ArgumentCompleter({ param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters )
        $possibleValues = @{
          Overflow = @('DisconnectWithBusy', 'Forward', 'Voicemail', 'SharedVoicemail')
          Timeout  = @('Disconnect', 'Forward', 'Voicemail', 'SharedVoicemail')
          NoAgent  = @('Queue', 'Forward', 'Voicemail', 'SharedVoicemail')
        }
        if ($fakeBoundParameters.ContainsKey('Trigger')) {
          $possibleValues[$fakeBoundParameters.Trigger] | Where-Object { $_ -like "$wordToComplete*" }
        }
        else {
          $possibleValues.Values | ForEach-Object { $_ }
        }
      })]
    [string]$Trigger,

    [Parameter(HelpMessage = 'Overflow: # people in the Queue; Timeout: Time in seconds (0-2700s)')]
    [int16]$Threshold,

    [Parameter(HelpMessage = 'Prompt for trigger. Text-to-Voice will require the LanguageId Parameter')]
    [string]$Prompt,

    [Parameter(Mandatory, HelpMessage = 'Action to be taken')]
    [Validateset('Disconnect', 'Forward', 'Voicemail', 'SharedVoicemail', 'Queue', 'DisconnectWithBusy')]
    [string]$Action,

    # if Action is not DisconnectWithBusy, this is required
    [Parameter(HelpMessage = 'TEL URI, UPN or Group Display Name that is targeted upon trigger, only valid for forwarded calls')]
    [string]$Target,

    [Parameter(HelpMessage = 'Applies any provided prompt to all types')]
    [switch]$SetPromptForAllTypes,

    [Parameter(HelpMessage = 'Using this Parameter will make a Transcription of the Voicemail message available in the Mailbox')]
    [switch]$EnableTranscription,

    [Parameter(HelpMessage = 'Using this Parameter will make a Transcription of the Voicemail message available in the Mailbox')]
    [switch]$EnableSystemPromptSuppression,

    [Parameter(HelpMessage = 'Language Identifier from Get-CsAutoAttendantSupportedLanguage.')]
    [ValidateScript( {
        if ($_ -in $(Get-OrbitAcSbCsAutoAttendantSupportedLanguage @args) ) { $True } else {
          throw [System.Management.Automation.ValidationMetadataException] 'Value must be a supported Langauge Id. Use Intellisense for options'
        } })]
    # [ArgumentCompleter({ Get-OrbitAcSbCsAutoAttendantSupportedLanguage @args })]
    [string]$LanguageId

  )

  begin {
    Show-OrbitFunctionStatus -Level RC
    Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand)"

    # Asserting Graph Connection
    if ( -not (Test-GraphConnection) ) { throw 'Connection to Microsoft Graph not established. Please validate connection' }

    # Asserting MicrosoftTeams Connection
    if ( -not (Assert-MicrosoftTeamsConnection) ) { throw 'Connection to Microsoft Teams not established. Please validate connection' }

    # Setting Preference Variables according to Upstream settings
    if (-not $PSBoundParameters['Verbose']) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') }
    if (-not $PSBoundParameters['Confirm']) { $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference') }
    if (-not $PSBoundParameters['WhatIf']) { $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference') }
    $DebugPreference = if (-not $PSBoundParameters['Debug']) { $PSCmdlet.SessionState.PSVariable.GetValue('DebugPreference') } else { 'Continue' }
    $InformationPreference = if ( $PSBoundParameters['InformationAction']) { $PSCmdlet.SessionState.PSVariable.GetValue('InformationAction') } else { 'Continue' }

    $Stack = Get-PSCallStack
    $Called = ($stack.length -ge 3)

    #Initialising Counters
    $private:StepsID0, $private:StepsID1 = Get-WriteBetterProgressSteps -Code $($MyInvocation.MyCommand.Definition) -MaxId 1
    $private:ActivityID0 = $($MyInvocation.MyCommand.Name)
    [int] $private:CountID0 = [int] $private:CountID1 = 1

    $AudioFileMatch = '.(wav|wma|mp3)$'

    # Validation
    $StatusID0 = 'Validating Input'
    # Language has to be normalised as the Id is case sensitive
    $CurrentOperationID0 = 'LanguageId'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    if ($PSBoundParameters['LanguageId']) {
      $LanguageId = $($LanguageId.Split('-')[0]).ToLower() + '-' + $($LanguageId.Split('-')[1]).ToUpper()
      Write-Verbose "'$Name' LanguageId normalised to '$LanguageId'"
      $VRSupported = ((Get-CsAutoAttendantSupportedLanguage -Id $LanguageId).VoiceResponseSupported)
      Write-Verbose "LanguageId '$LanguageId' - Voice Responses are $(if ( -not $VRSupported ) { 'no t' })supported"
    }
    else {
      if ( ( $PSBoundParameters['EnableTranscription'] -and $EnableTranscription ) -or `
          $PSBoundParameters['Prompt'] -and $Prompt -notmatch $AudioFileMatch ) {
        Write-Error -Message "$Message`: Prompt or Transcription require a Language set selection. Please provide Parameter LanguageId" -ErrorAction Stop -RecommendedAction 'Add Parameter LanguageId'
      }
    }

    if ( $PSBoundParameters['Prompt'] -and $PSBoundParameters['SetPromptForAllTypes'] ) {
      Write-Warning -Message "The configuration parameters for all Prompts are currently only available in PowerShell `
      and do not appear in Teams admin center. Saving a call queue configuration through Teams admin center will remove `
      any of these configured items"

    }

    if ( $PSBoundParameters['Threshold'] ) {
      switch ( $Trigger ) {
        'Overflow' {
          If ($_ -ge 0 -and $_ -le 200) { $True } else {
            throw [System.Management.Automation.ValidationMetadataException] 'OverflowThreshold: Must be a value between 0 and 200.'
          }
        }
        'Timeout' {
          If ($_ -ge 0 -and $_ -le 2700) { $True } else {
            throw [System.Management.Automation.ValidationMetadataException] 'TimeoutThreshold: Must be a value between 0 and 2700.'
          }
        }
        'NoAgent' {
          throw [System.Management.Automation.ValidationMetadataException] 'NoAgentThreshold: Parameter not available.'
        }
      }
    }

    # ensuring non-accidental default application
    #$ChosenAction = $Action
    $Disconnect = switch ( $Trigger ) {
      'Overflow' { 'DisconnectWithBusy' }
      'Timeout' { 'Disconnect' }
      'NoAgent' { 'Disconnect' }
    }

    # SetPromptForAllTypes - Defining Actions for multi-application
    $AllNonDefaultActions = @('Disconnect', 'RedirectPerson', 'RedirectVoiceApp', 'RedirectPhoneNumber', 'RedirectVoicemail', 'SharedVoicemail')

  } #begin

  process {
    Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)"

    # re-Initialising counters for Progress bars (for Pipeline processing)
    [int] $private:CountID0 = 1

    $Parameters = @{}

    #region Target
    if ( $PSBoundParameters['Target'] ) {
      try {
        $CallTarget = $null
        $CallTarget = Get-TeamsCallableEntity -Identity "$Target" -ErrorAction Stop
        if ($PSBoundParameters['Debug'] -or $DebugPreference -eq 'Continue') {
          " Function: $($MyInvocation.MyCommand.Name) - CallTarget:", ($CallTarget | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
        }
      }
      catch {
        Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' not determined"
      }
      try {
        $ParameterName = $($Trigger + 'ActionTarget')
        Write-Verbose -Message "Call Queue '$Name' - Action '$Action' - $ParameterName - Parsing Target"
        switch ($Action) {
          'Queue' {
            # No target required for the default action
          }
          'Disconnect' {
            # No target required for the default action
          }
          'DisconnectWithBusy' {
            # No target required for the default action
          }
          'Forward' {
            # Forward requires a Target (Tel URI, ObjectId of UPN of a User or an Application Instance to be translated to GUID)
            Write-Verbose -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' - Querying Object"
            switch ( $CallTarget.ObjectType ) {
              'TelURI' {
                #Telephone Number (E.164)
                Write-Verbose -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$($CallTarget.Identity)' - OK"
                $Parameters.$ParameterName = $CallTarget.Identity
              }
              'User' {
                try {
                  $Assertion = $null
                  $Assertion = Assert-TeamsCallableEntity -UserPrincipalName "$($CallTarget.Entity)" -RequireEV -WarningAction SilentlyContinue -ErrorAction Stop
                  if ($Assertion) {
                    Write-Verbose -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$($CallTarget.Identity)' - OK"
                    $Parameters.$ParameterName = $CallTarget.Identity
                  }
                  else {
                    Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' not asserted"
                  }
                }
                catch {
                  Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' Error: $($_.Exception.Message)"
                }
              }
              'ResourceAccount' {
                try {
                  $Assertion = $null
                  $Assertion = Assert-TeamsCallableEntity -UserPrincipalName "$($CallTarget.Entity)" -WarningAction SilentlyContinue -ErrorAction Stop
                  if ($Assertion) {
                    Write-Verbose -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$($CallTarget.Identity)' - OK"
                    $Parameters.$ParameterName = $CallTarget.Identity
                  }
                  else {
                    Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' not asserted"
                  }
                }
                catch {
                  Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' Error: $($_.Exception.Message)"
                }
              }
              default {
                # Capturing any other specified Target that does not match for the Forward
                Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' is incompatible and is not processed!"
                Write-Verbose -Message "Call Queue '$Name' - Action '$Action': Target expected is: Tel URI or a UPN of a User or Resource Account" -Verbose
              }
            }
          }
          'VoiceMail' {
            # VoiceMail requires a Target (UPN of a User to be translated to GUID)
            if ($CallTarget.ObjectType -eq 'User') {
              try {
                $Assertion = $null
                $Assertion = Assert-TeamsCallableEntity -UserPrincipalName "$($CallTarget.Entity)" -RequireEV -WarningAction SilentlyContinue -ErrorAction Stop
                if ($Assertion) {
                  Write-Verbose -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$($CallTarget.Identity)' - OK"
                  $Parameters.$ParameterName = $CallTarget.Identity
                }
                else {
                  Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' not asserted"
                }
              }
              catch {
                Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' Error: $($_.Exception.Message)"
              }
            }
            else {
              Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' is incompatible and is not processed!"
              Write-Verbose -Message "Call Queue '$Name' - Action '$Action': Target expected is: UPN of a User" -Verbose
            }
          }
          'SharedVoiceMail' {
            # SharedVoiceMail requires a Target (UPN of a Group to be translated to GUID)
            switch ( $CallTarget.ObjectType ) {
              'Group' {
                Write-Verbose -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$($CallTarget.Identity)' - OK"
                $Parameters.$ParameterName = $CallTarget.Identity
              }
              'Unknown' {
                Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' not set! Error enumerating Target"
              }
              default {
                Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' not a Group!"
              }
            }
          }
        }
      }
      catch {
        Write-Warning -Message "Call Queue '$Name' - Action '$Action': $ParameterName '$Target' not set! Error enumerating Target: $($_.Exception.Message)"
      }
    }
    #endregion

    #region Prompt
    if ( $PSBoundParameters['Prompt'] ) {
        $ThisAction = switch ( $Action ) {
          'DisconnectWithBusy' { 'Disconnect' }
          'Disconnect' { 'Disconnect' }
          'Forward' {
            # Parameter depends on Type of Target provided. Determining correct (single) parameter to apply with CallTarget
            switch ( $CallTarget.ObjectType ) {
              'User' { 'RedirectPerson' }
              'ResourceAccount' { 'RedirectVoiceApp' }
              'TelUri' { 'RedirectPhoneNumber' }
              Default { 'Disconnect' } # Necessary as $CallTarget could be blank!
            }
          }
          'Voicemail' { 'RedirectVoicemail' }
          'SharedVoicemail' { 'SharedVoicemail' }
          default { $null }
        }

      if ( $ThisAction  ) {
        # Create Prompt
        $TeamsCallQueuePrompt = Checkpoint-TeamsCallQueuePrompt -Prompt "$Trigger`Prompt" -String "$Prompt" -ErrorAction Stop

        $(if ($PSBoundParameters['SetPromptForAllTypes']) { $AllNonDefaultActions } else { $ThisAction }) | ForEach-Object {
          $ParameterName = $TeamsCallQueuePrompt.Parameter -replace '\*', "$_"
          Write-Verbose -Message "Call Queue '$Name' - Parameter added: $ParameterName - Value '$($TeamsCallQueuePrompt.Value)'"
          $Parameters.$ParameterName = $TeamsCallQueuePrompt.Value
        }
      }
    }
    #endregion

    #region Action
    if ( $PSBoundParameters['Action'] ) {
      if ( $Action -EQ 'Queue' ) {
        Write-Verbose -Message "Call Queue '$Name' - Action: '$ParameterName' - Value: '$Action' - No Action needed - OK"
      }
      elseif ( $Action -NE $Disconnect -and -not $PSBoundParameters['Target'] ) {
        Write-Error -Message "$($MyInvocation.MyCommand) - Action '$Action' - Target missing. please provide a Target" -ErrorAction Stop
      }
      else {
        $ParameterName = $($Trigger + 'Action')
        if ( $Action -EQ $Disconnect ) {
          Write-Verbose -Message "Call Queue '$Name' - Action: '$ParameterName' - Value: '$Disconnect' - OK"
          $Parameters.$ParameterName = $Disconnect
        }
        elseif ($Parameters.ContainsKey("$ParameterName`Target")) {
          # Only adding a non-default action if a Target is present already
          Write-Verbose -Message "Call Queue '$Name' - Action: '$ParameterName' - Value: '$Action' - OK"
          $Parameters.$ParameterName = $Action
        }
        else {
          Write-Warning -Message "Call Queue '$Name' - Action: '$ParameterName' - Value: '$Action' - Target not enumerated. Action not set."
        }
      }
    }
    #endregion

    #region Language, Transcription & Suppression
    if ( $PSBoundParameters['LanguageId'] ) {
      Write-Verbose -Message "Call Queue '$Name' - LanguageId: $LanguageId - OK"
      $Parameters.LanguageId = $LanguageId
    }

    if ($PSBoundParameters['EnableTranscription']) {
      if ( $Action -NE 'SharedVoicemail' ) {
        Write-Verbose -Message "Call Queue '$Name' - Action '$Action' - EnableTranscription cannot be set (only for 'SharedVoicemail')"
      }
      else {
        $ParameterName = $('Enable' + $Trigger + 'SharedVoicemailTranscription')
        Write-Verbose -Message "Call Queue '$Name' - Action '$Action' - $ParameterName - OK"
        $Parameters.$ParameterName = $EnableTranscription
      }
    }

    if ($PSBoundParameters['EnableSystemPromptSuppression']) {
      if ( $Action -NE 'SharedVoicemail' ) {
        Write-Verbose -Message "Call Queue '$Name' - Action '$Action' - EnableSystemPromptSuppression cannot be set (only for 'SharedVoicemail')"
      }
      else {
        $ParameterName = $('Enable' + $Trigger + 'SharedVoicemailSystemPromptSuppression')
        Write-Verbose -Message "Call Queue '$Name' - Action '$Action' - $ParameterName - OK"
        $Parameters.$ParameterName = $EnableSystemPromptSuppression
      }
    }
    #endregion

    #region Settings dependent on ParameterSetName
    if ( $PSBoundParameters['Threshold'] ) {

      $ParameterName = $($Trigger + 'Threshold')
      Write-Verbose -Message "Call Queue '$Name' - $ParameterName - $Threshold - OK"
      $Parameters.$ParameterName = $Threshold
    }

    if ( $PSBoundParameters['NoAgentApplyTo'] ) {
      if ( $Trigger -NE 'NoAgent' ) {
        Write-Warning -Message "Call Queue '$Name' - NoAgentApplyTo is only available for 'NoAgent' Trigger. Omitting parameter"
      }
      else {
        Write-Verbose -Message "Call Queue '$Name' - NoAgentApplyTo: $NoAgentApplyTo - OK"
        $Parameters.NoAgentApplyTo = $NoAgentApplyTo
      }
    }
    #endregion

    # Output
    return $Parameters

  } #process

  end {
    Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)"

  } #end
} #New-TeamsCallQueueTrigger