Public/AutoAttendant/Update-TeamsAutoAttendantBusinessHours.ps1

# Module: TeamsFunctions
# Function: AutoAttendant
# Author: David Eberhardt
# Updated: 03-SEP 2022
# Status: RC




function Update-TeamsAutoAttendantBusinessHours {
  <#
  .SYNOPSIS
    Changing, amending or replacing the Business Hours Call Flow on existing Auto Attendants; Calling Set-CsAutoAttendant
  .DESCRIPTION
    Editing exiting Auto Attendants with Set-CsAutoAttendant requires manual creation of objects and surgical replacement.
    This script tries to simplify editing Auto Attendants by providing four functions:
    Update-TeamsAutoAttendant covers general Settings of the Auto Attendnat
    Update-TeamsAutoAttendantBusinessHours covers the Default Call Flow
    Update-TeamsAutoAttendantAfterHours covers the After Hours Call Flow
    Update-TeamsAutoAttendantHoliday covers the Holiday Call Flow
    Each individual Script can replace the full call flow with a previously created object,
    or amend some specific parts of the respective flow
  .PARAMETER Name
    Name of the Auto Attendant. Required to locate the Auto Attendant.
    Alternatively ID of the Auto Attendant for more precise location
  .PARAMETER BusinessHoursGreeting
    Optional. Replaces the Greeting for the Default Call Flow (during business hours) utilising New-TeamsAutoAttendantPrompt
    A supported Audio File or a text string that is parsed by the text-to-voice engine in the Language specified
    The last 4 digits will determine the type. For an AudioFile they are expected to be the file extension: '.wav', '.wma' or 'mp3'
    If DefaultCallFlow is provided, this parameter will be ignored.
  .PARAMETER BusinessHoursCallFlowOption
    Optional. Disconnect, TransferCallToTarget, Menu.
    TransferCallToTarget requires BusinessHoursCallTarget. Menu requires BusinessHoursMenu
    If DefaultCallFlow is provided, this parameter will be ignored.
  .PARAMETER BusinessHoursCallTarget
    Optional. Requires BusinessHoursCallFlowOption to be TransferCallToTarget. Creates a Callable entity for this Call Target.
    Expected are UserPrincipalName (User, ResourceAccount), a TelURI (ExternalPstn), an Office 365 Group Name (SharedVoicemail)
    If DefaultCallFlow is provided, this parameter will be ignored.
  .PARAMETER BusinessHoursMenu
    Optional. Requires BusinessHoursCallFlowOption to be Menu and expects an AutoAttendantMenuObject
    If DefaultCallFlow is provided, this parameter will be ignored.
  .PARAMETER DefaultCallFlow
    Optional. Call Flow Object to pass to Set-CsAutoAttendant (used as the Default Call Flow)
    Using this parameter to define the default Call Flow overrides all -BusinessHours Parameters
  .PARAMETER EnableTranscription
    Optional. Where possible, tries to enable Voicemail Transcription.
    Effective only for SharedVoicemail Targets as an Operator or MenuOption. Otherwise has no effect.
  .PARAMETER PassThru
    Optional. Displays Auto Attendant Object after action.
  .PARAMETER Force
    Suppresses confirmation prompt to enable Users for Enterprise Voice, if Users are specified
    Currently no other impact
  .EXAMPLE
    Update-TeamsAutoAttendantBusinessHours -Name "My Auto Attendant"
 
    Creates a new Auto Attendant "My Auto Attendant" with Defaults
    TimeZone is UTC, Language is en-US and Schedule is Mon-Fri 9to5.
    Business hours and After Hours action is Disconnect
  .EXAMPLE
    Update-TeamsAutoAttendantBusinessHours -Name "My Auto Attendant" -TimeZone UTC-05:00 -LanguageId pt-BR -AfterHoursSchedule MonToFri8to12and13to18 -EnableVoiceResponse
 
    Creates a new Auto Attendant "My Auto Attendant" and sets the TimeZone to UTC-5 and the language to Portuguese (Brazil)
    The Schedule of Mon-Fri 8to12 and 13to18 will be applied. Also enables VoiceResponses
  .EXAMPLE
    Update-TeamsAutoAttendantBusinessHours -Name "My Auto Attendant" -Operator "tel:+1555123456"
 
    Creates a new Auto Attendant "My Auto Attendant" with default TimeZone and Language, but defines an Operator as a Callable Entity (Forward to Pstn)
  .EXAMPLE
    Update-TeamsAutoAttendantBusinessHours -Name "My Auto Attendant" -BusinessHoursGreeting "Welcome to Contoso" -BusinessHoursCallFlowOption TransferCallToTarget -BusinessHoursCallTarget $CallTarget
 
    Creates a new Auto Attendant "My Auto Attendant" with defaults, but defines a Text-to-Voice Greeting, then forwards the Call to the Call Target.
    The CallTarget is queried based on input and created as required. UserPrincipalname for Users or ResourceAccount, Group Name for SharedVoicemail, provided as a string in the Variable $UPN
    This example is equally applicable to AfterHours.
  .EXAMPLE
    Update-TeamsAutoAttendantBusinessHours -Name "My Auto Attendant" -DefaultCallFlow $DefaultCallFlow -InclusionScope $InGroups -ExclusionScope $OutGroups
 
    Creates a new Auto Attendant "My Auto Attendant" and passes through all objects provided.
    In this example, provided Objects are passed on through tto New-CsAutoAttendant and override other respective Parmeters provided:
    A DefaultCallFlow Object is passed on which overrides all "-BusinessHours"-Parmeters. One or more CallFlows and
    one or more CallHandlingAssociation Objects are passed on overriding all "-AfterHours" and "-HolidaySet" Parameters
    An InclusionScope and an ExclusionScope are defined. These are passed on as-is
    All other values, like Language and TimeZone are defined with their defaults and can still be defined with the Objects.
  .INPUTS
    System.String
  .OUTPUTS
    System.Object
  .NOTES
    BusinessHours Parameters aim to simplify input for the Default Call Flow
    BusinessHoursMenu expects a AutoAttendantMenuObject created with New-TeamsAutoAttendantMenu or New-CsAutoAttendantMenu
    AfterHours Parameters aim to simplify input for the After Hours Call Flow
    HolidaySet Parameters aim to simplify input for the Holiday Set Call Flow
    Use of CsAutoAttendant Parameters will override the respective '-BusinessHours', '-AfterHours' and '-HolidaySet' Parameters
 
    InclusionScope and ExclusionScope Objects can be created with New-TeamsAutoAttendantDialScope and the Group Names
    This was deliberately not integrated into this CmdLet
  .COMPONENT
    TeamsAutoAttendant
  .FUNCTIONALITY
    Creates a Auto Attendant with custom settings and friendly names as input
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/Update-TeamsAutoAttendantBusinessHours.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/about_TeamsAutoAttendant.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/
  #>


  [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
  [Alias('Update-TeamsAABusinessHours')]
  [OutputType([System.Void])]
  param(
    [Parameter(Mandatory, ValueFromPipeline, HelpMessage = 'Name of the Auto Attendant')]
    [string]$Name,

    [Parameter(HelpMessage = 'Business Hours Greeting - Text String or Recording')]
    [ArgumentCompleter( { '<Your Text-to-speech-string>', 'C:\Temp\' })]
    [string]$BusinessHoursGreeting,

    [Parameter(HelpMessage = 'Business Hours Call Flow - Default options')]
    [ValidateSet('Disconnect', 'TransferCallToTarget', 'Menu')]
    [string]$BusinessHoursCallFlowOption,

    [Parameter(HelpMessage = 'Business Hours Call Target - BusinessHoursCallFlowOption = TransferCallToTarget')]
    [string]$BusinessHoursCallTarget,

    [Parameter(HelpMessage = 'Business Hours Call Target - BusinessHoursCallFlowOption = Menu')]
    [object]$BusinessHoursMenu,

    [Parameter(HelpMessage = 'Default Call Flow')]
    [object]$DefaultCallFlow,

    [Parameter(HelpMessage = 'Tries to Enable Transcription wherever possible')]
    [switch]$EnableTranscription,

    [Parameter(HelpMessage = 'By default, no output is generated, PassThru will display the Object changed')]
    [switch]$PassThru,

    [Parameter(HelpMessage = 'Suppresses confirmation prompt to enable Users for Enterprise Voice, if Users are specified')]
    [switch]$Force

  ) #param

  begin {
    Show-FunctionStatus -Level RC
    Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand)"
    Write-Verbose -Message "Need help? Online: $global:TeamsFunctionsHelpURLBase$($MyInvocation.MyCommand)`.md"

    # Asserting AzureAD Connection
    if ( -not $script:TFPSSA) { $script:TFPSSA = Assert-AzureADConnection; if ( -not $script:TFPSSA ) { break } }

    # Asserting MicrosoftTeams Connection
    if ( -not (Assert-MicrosoftTeamsConnection) ) { break }

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

    #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

    $StatusID0 = 'Verifying input'
    $CurrentOperationID0 = 'Validating DefaultCallFlow'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    #region Processing DefaultCallFlow VS BusinessHours* Parameters
    if ($DefaultCallFlow) {
      # DefaultCallFlow
      Write-Information 'DefaultCallFlow - Overriding all BusinessHours-Parameters'

      if ($PSBoundParameters.ContainsKey('BusinessHoursGreeting')) { $PSBoundParameters.Remove('BusinessHoursGreeting') }
      if ($PSBoundParameters.ContainsKey('BusinessHoursCallFlowOption')) { $PSBoundParameters.Remove('BusinessHoursCallFlowOption') }
      if ($PSBoundParameters.ContainsKey('BusinessHoursCallTarget')) { $PSBoundParameters.Remove('BusinessHoursCallTarget') }

      # Testing provided Object Type
      if (($DefaultCallFlow | Get-Member | Select-Object TypeName -First 1).TypeName -ne 'Deserialized.Microsoft.Rtc.Management.Hosted.OAA.Models.CallFlow') {
        Write-Error "DefaultCallFlow - Type is not of 'Microsoft.Rtc.Management.Hosted.OAA.Models.CallFlow'. Please provide a Call Flow Object" -Category InvalidType
        break
      }
    }
    else {
      # BusinessHours Parameters
      if (-not $PSBoundParameters.ContainsKey('BusinessHoursCallFlowOption')) {
        Write-Verbose -Message "BusinessHoursCallFlowOption - Parameter not specified. Defaulting to 'Disconnect' No other 'BusinessHours'-Parameters are processed!"
        $BusinessHoursCallFlowOption = 'Disconnect'
      }
      elseif ($BusinessHoursCallFlowOption -eq 'TransferCallToTarget') {
        # Must contain Target
        if (-not $PSBoundParameters.ContainsKey('BusinessHoursCallTarget')) {
          Write-Error -Message "BusinessHoursCallFlowOption (TransferCallToTarget) - Parameter 'BusinessHoursCallTarget' missing"
          break
        }

        # Must not contain a Menu
        if ($PSBoundParameters.ContainsKey('BusinessHoursMenu')) {
          Write-Verbose -Message 'BusinessHoursCallFlowOption (TransferCallToTarget) - Parameter BusinessHoursMenu cannot be used and will be omitted!' -Verbose
          $PSBoundParameters.Remove('BusinessHoursMenu')
        }
      }
      elseif ($BusinessHoursCallFlowOption -eq 'Menu') {
        # Must contain a Menu
        if (-not $PSBoundParameters.ContainsKey('BusinessHoursMenu')) {
          Write-Error -Message 'BusinessHoursCallFlowOption (Menu) - BusinessHoursMenu missing'
          break
        }
        else {
          # Testing provided Object Type
          if (($BusinessHoursMenu | Get-Member | Select-Object -First 1).TypeName -notmatch 'Microsoft.Rtc.Management.Hosted.OAA.Models.Menu') {
            Write-Error -Message "BusinessHoursCallFlowOption (Menu) - BusinessHoursMenu not of the Type 'Microsoft.Rtc.Management.Hosted.OAA.Models.Menu'" -Category InvalidType
            break
          }
        }

        # Must not contain Target
        if ($PSBoundParameters.ContainsKey('BusinessHoursCallTarget')) {
          Write-Verbose -Message "BusinessHoursCallFlowOption (Menu) - Parameter 'BusinessHoursCallTarget' cannot be used and will be omitted!" -Verbose
          $PSBoundParameters.Remove('BusinessHoursCallTarget')
        }
      }
    }
    #endregion

    # Preparing Splatting Object for New-TeamsCallableEntity
    $TeamsCallableEntityParams = @{ 'ErrorAction' = 'Stop' }
    if ( $PSBoundParameters.ContainsKey('EnableTranscription') ) {
      $TeamsCallableEntityParams = @{ 'EnableTranscription' = $true }
    }
  } #begin

  process {
    Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)"
    #region PREPARATION
    $StatusID0 = 'Querying Object'
    # preparing Splatting Object
    $Parameters = $null

    #region Query Unique Element
    $CurrentOperationID0 = "Finding unique result for provided Name '$Name'"
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    if ( $Name -match $script:TFMatchGuid ) {
      #Identity or ObjectId
      Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand) - ID - '$Name'"
      $AAInstance = Get-CsAutoAttendant -Identity "$Name" -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
    }
    else {
      #Name
      Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand) - Name - '$Name'"
      # Initial Query to determine unique result (single object)
      $AAInstance = Get-CsAutoAttendant -NameFilter "$Name" -WarningAction SilentlyContinue
      $AAInstance = $AAInstance | Where-Object Name -EQ "$Name"
    }

    if ($null -eq $AAInstance) {
      Write-Error "'$Name' No Object found" -Category ParserError -RecommendedAction "Please check 'Name' provided" -ErrorAction Stop
    }
    elseif ($AAInstance.GetType().BaseType.Name -eq 'Array') {
      Write-Error "'$Name' Multiple Results found! Cannot determine unique result." -Category ParserError -RecommendedAction 'Please provide Auto Attendant GUID instead of name' -ErrorAction Stop
    }
    else {
      $AAID = $AAInstance.Identity
      Write-Information "INFO: '$Name' Auto Attendant found: Identity: $AAID"
      if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
        " Function: $($MyInvocation.MyCommand.Name) - Auto Attendant", ($AAInstance | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
      }
    }
    #endregion

    # Defining Name for reference below
    $AAName = "$($AAInstance.Name)"
    #endregion


    #region SETTINGS - Business Hours Call Flow
    #NOTE All Options in Region Settings use ID 1 for showing progress
    $ActivityID1 = 'Business Hours Call Flow'
    $StatusID1 = 'Preparing Parameters'
    if ( $DefaultCallFlow ) {
      $CurrentOperationID1 = 'Business Hours Call Flow - Default Call Flow'
      Write-BetterProgress -Id 1 -Activity $ActivityID1 -Status $StatusID1 -CurrentOperation $CurrentOperationID1 -Step ($private:CountID1++) -Of $private:StepsID1
      # Using As-Is
      Write-Information "INFO: '$AAName' DefaultCallFlow - Custom Object provided. Over-riding other options (like switch 'BusinessHoursCallFlow')"
      $AAInstance.DefaultCallFlow = $DefaultCallFlow
      Write-Information 'INFO: Pending Application: DefaultCallFlow changed: Full Call Flow applied'
    }
    else {
      Write-Verbose -Message "'$AAName' DefaultCallFlow - No Custom Object - Processing 'BusinessHours'-Parameters"
      #region BusinessHoursGreeting
      if ( $PSBoundParameters.ContainsKey('BusinessHoursGreeting') ) {
        $CurrentOperationID1 = 'Business Hours Call Flow - Greeting'
        Write-BetterProgress -Id 1 -Activity $ActivityID1 -Status $StatusID1 -CurrentOperation $CurrentOperationID1 -Step ($private:CountID1++) -Of $private:StepsID1
        try {
          $BusinessHoursGreetingObject = New-TeamsAutoAttendantPrompt -String "$BusinessHoursGreeting"
          if ( -not $BusinessHoursGreetingObject) { throw } # Continuing only if CallTarget is created
          Write-Information "INFO: '$AAName' Business Hours Call Flow - Greeting created"
          $AAInstance.DefaultCallFlow.Greetings = @($BusinessHoursGreetingObject)
          Write-Information 'INFO: Pending Application: DefaultCallFlow changed: Greeting'
        }
        catch {
          Write-Warning -Message "'$AAName' CallFlow - BusinessHoursCallFlow - Greeting not enumerated. Omitting Greeting"
        }
      }
      #endregion

      #region Processing BusinessHoursCallFlowOption
      if ( $PSBoundParameters.ContainsKey('BusinessHoursCallFlowOption') ) {
        $BusinessHoursMenuObject = $null
        $CurrentOperationID1 = 'Business Hours Call Flow - Call Flow Option'
        Write-BetterProgress -Id 1 -Activity $ActivityID1 -Status $StatusID1 -CurrentOperation $CurrentOperationID1 -Step ($private:CountID1++) -Of $private:StepsID1
        switch ($BusinessHoursCallFlowOption) {
          'TransferCallToTarget' {
            Write-Verbose -Message "'$AAName' DefaultCallFlow - Transferring to Target"
            try {
              $BusinessHoursCallTargetEntity = New-TeamsCallableEntity "$BusinessHoursCallTarget"@TeamsCallableEntityParams
              if ( -not $BusinessHoursCallTargetEntity) { throw } # Building Menu Only if Successful
              $BusinessHoursMenuOptionTransfer = New-CsAutoAttendantMenuOption -Action TransferCallToTarget -CallTarget $BusinessHoursCallTargetEntity -DtmfResponse Automatic
              $BusinessHoursMenuObject = New-CsAutoAttendantMenu -Name 'Business Hours Menu' -MenuOptions @($BusinessHoursMenuOptionTransfer)
              Write-Information "INFO: '$AAName' Business Hours Call Flow - Menu (TransferCallToTarget) created"
            }
            catch {
              Write-Warning -Message 'BusinessHoursCallTarget - Error creating Call Target - Omiting change to DefaultCallFlow'
            }
          }
          'Menu' {
            Write-Verbose -Message "'$AAName' DefaultCallFlow - Menu"
            if ( $PSBoundParameters.ContainsKey('BusinessHoursMenu') ) {
              # Menu is passed on as-is - $BusinessHoursMenu is defined and attached
              $BusinessHoursMenuObject = $BusinessHoursMenu
              Write-Information "INFO: '$AAName' Business Hours Call Flow - Menu (BusinessHoursMenu) used"
            }
            else {
              # No custom / default Menu is currently created
              # $BusinessHoursMenu is Mandatory. If this is built out, the check against this must also be removed!
            }
          }
          'Disconnect' {
            Write-Verbose -Message "'$AAName' DefaultCallFlow not provided or 'Disconnect' - Using Disconnect"
            $BusinessHoursMenuOptionDefault = New-CsAutoAttendantMenuOption -Action DisconnectCall -DtmfResponse Automatic
            $BusinessHoursMenuObject = New-CsAutoAttendantMenu -Name 'Business Hours Menu' -MenuOptions @($BusinessHoursMenuOptionDefault)
          }
        }

        # Applying $BusinessHoursMenuObject
        if ( $BusinessHoursMenuObject ) {
          $AAInstance.DefaultCallFlow.Menu = $BusinessHoursMenuObject
          Write-Information 'INFO: Pending Application: DefaultCallFlow changed: CallFlowOption/Menu'
        }
        else {
          Write-Warning -Message 'BusinessHoursCallTarget - Error creating Menu - Omiting change to DefaultCallFlow'
        }
      }
      #endregion
    }

    Write-Progress -Id 1 -Activity $ActivityID1 -Completed
    #endregion


    #region Preparing Splatting Object
    $Parameters = $null
    $Parameters += @{'Instance' = $AAInstance }

    $Parameters += @{'WarningAction' = if ( $PSBoundParameters.ContainsKey('WarningAction') ) { $PSCmdlet.SessionState.PSVariable.GetValue('WarningAction') } else { 'Continue' } }
    $Parameters += @{'ErrorAction' = if ( $PSBoundParameters.ContainsKey('ErrorAction') ) { $PSCmdlet.SessionState.PSVariable.GetValue('ErrorAction') } else { 'Stop' } }
    #endregion


    #region ACTION
    Write-Verbose -Message '[PROCESS] Updating Auto Attendant'
    if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
      " Function: $($MyInvocation.MyCommand.Name) - Parameters (Set-CsAutoAttendant)", ($Parameters | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
    }

    # Updating Auto Attendant (Set-CsAutoAttendant)
    $StatusID0 = 'Updating Auto Attendant'
    $CurrentOperationID0 = "'$AAName'"
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    if ($PSCmdlet.ShouldProcess("$AAName", 'Set-CsAutoAttendant')) {
      try {
        # Create the Auto Attendant with all enumerated Parameters passed through splatting
        $null = (Set-CsAutoAttendant @Parameters)
        #Write-Information "INFO: All pending changes applied to Auto Attendant Instance for '$AAName'"
        Write-Host "INFO: All pending changes applied to Auto Attendant Instance for '$AAName'" -ForegroundColor Magenta
      }
      catch {
        Write-Error -Message "Error updating the Auto Attendant: $($_.Exception.Message)" -Category InvalidResult
        return
      }
    }
    else {
      return
    }
    #endregion


    #region OUTPUT
    if ( $PassThru ) {
      $StatusID0 = 'Validation/PassThru'
      $CurrentOperationID0 = 'Re-Querying Object'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      $AAFinal = Get-TeamsAutoAttendant -Name $AAInstance.Identity -WarningAction SilentlyContinue
      Write-Output $AAFinal
    }
    #endregion
    Write-Progress -Id 0 -Activity $ActivityID0 -Completed

  } #process

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

  } #end
} # Update-TeamsAutoAttendantBusinessHours