Public/Support/AutoAttendant/New-TeamsHolidaySchedule.ps1

# Module: TeamsFunctions
# Function: AutoAttendant
# Author: David Eberhardt
# Updated: 13-JUN-2021
# Status: Live


#TODO Add If Called, run with -inmemory, otherwise return the created object!
#TODO Add Name Parameter for 63 characters (minus the index, so 60) worth of custom name.
function New-TeamsHolidaySchedule {
  <#
  .SYNOPSIS
    Creates a Teams Schedule for each Country and Year specified
  .DESCRIPTION
    Queries the Nager.Date API for public Holidays for Country and year and creates a CsOnlineSchedule object for each.
  .PARAMETER CountryCode
    Required. ISO3166-Alpha-2 Country Code. One or more Countries from the list of Get-PublicHolidayCountry
  .PARAMETER Year
    Optional. Year for which the Holidays are to be listed. One or more Years between 2000 and 3000
    If not provided, the current year is taken. If the current month is December, the coming year is taken.
  .EXAMPLE
    New-TeamsHolidaySchedule -CountryCode CA -Year 2022
 
    Creates Schedule Objects in Teams for Canada for the year 2022.
    One Schedule object per 10 Holidays. They are named as "CA 2022 #1" and "CA 2022 #2" respectively
  .EXAMPLE
    New-TeamsHolidaySchedule -CountryCode CA -Year 2022,2023,2024
 
    Creates Schedule Objects in Teams for Canada for the years 2022 to 2024. One Schedule object per 10 Holidays
  .EXAMPLE
    New-TeamsHolidaySchedule -CountryCode CA,MX,GB,DE -Year 2022
 
    Creates Schedule Objects in Teams for Canada, Mexico, Great Britain & Germany for the year 2022.
    One Schedule object per 10 Holidays. They are named as "CA 2022 #1", "CA 2022 #2", "MX 2022 #1", etc. respectively
  .EXAMPLE
    New-TeamsHolidaySchedule -CountryCode CA,MX,GB,DE -Year 2022,2023,2024
 
    Creates Schedule Objects in Teams for Canada, Mexico, Great Britain & Germany for the years 2022 to 2024.
    One Schedule object per 10 Holidays. They are named as "CA 2022 #1", "CA 2022 #2", "CA 2023 #1", etc. respectively
  .EXAMPLE
    New-TeamsHolidaySchedule -CountryCode CA -Year 2022 -FilterPastDates
 
    Creates Schedule Objects in Teams for Canada, Mexico, Great Britain & Germany for the years 2022 to 2024.
    Holidays for this year that are in the past are not considered, if more than 10 holidays remain, one Schedule object
    is created per 10 Holidays.
  .INPUTS
    System.String
  .OUTPUTS
    System.Object
  .NOTES
    The Nager.Date API currently supports a bit over 100 Countries. Please query with Get-PublicHolidayCountry
    Evaluated the following APIs:
    Nager.Date: Decent coverage (100+ Countries). Free & Used Coverage: https://date.nager.at/Home/RegionStatistic
    TimeAndDate: Great coverage. Requires license. Also a bit clunky. Not considering implementation.
    Calendarific: Great coverage. Requires license for commercial use. Currently not considering development
    Utilising the Calendarific API could be integrated if licensed and the API key is passed/registered locally.
 
    Teams only supports fixed schedules with maximum 10 dates. This CmdLet therefore splits all found holidays for a year
    into multiple Schedules and returns each object.
    When running this CmdLet, the Schedule is created in the Teams tenant.
  .COMPONENT
    SupportingFunction
    TeamsAutoAttendant
  .FUNCTIONALITY
    Queries available Holidays for a specific Country from the Nager.Date API
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/New-TeamsHolidaySchedule.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/about_TeamsAutoAttendant.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/
  #>


  [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
  #[Alias('')]
  [OutputType([PSCustomObject])]
  param (
    [Parameter(Mandatory, ValueFromPipelineByPropertyName, HelpMessage = 'ISO 3166-alpha2 Country Code (2-digit CC)')]
    [Alias('CC', 'Country')]
    [ValidateScript( {
        if ($_ -in $(&$global:TfAcSbNagerAPICountryCode)) { $True } else {
          throw [System.Management.Automation.ValidationMetadataException] "'Value must be a valid ISO3166-alpha2 Two-Letter CountryCode supported by Nager.API. Use Intellisense for options. Country '$_' not supported (yet), sorry."
        } })]
    [ArgumentCompleter({ &$global:TfAcSbNagerAPICountryCode })]
    [String[]]$CountryCode,

    [Parameter(ValueFromPipelineByPropertyName, HelpMessage = 'Year(s)')]
    [Alias('Y')]
    [ValidateRange(2000, 3000)]
    [int[]]$Year,

    [Parameter(ValueFromPipelineByPropertyName, HelpMessage = 'Only consideres dates that are in the future')]
    [switch]$FilterPastDates
  )

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

    # Setting Preference Variables according to Upstream settings
    if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') }
    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' }

    # Preparing Splatting Object
    $NewCsTeamsAutoAttendantScheduleParams = $null
    $NewCsTeamsAutoAttendantScheduleParams = @{
      'Fixed'             = $True
      'InformationAction' = 'SilentlyContinue'
      'ErrorAction'       = 'Stop'
    }

    # Handling Year
    if (-not $PSBoundParameters.ContainsKey('Year')) {
      $Today = Get-Date
      $Year = $Today.Year
      $null = $Today.Datetime -match '\d\d (.*?) \d'
      If ($Today.Month -eq 12) {
        $Year++
      }
      Write-Information "INFO: $($MyInvocation.MyCommand) - Parameter Year not provided, as it is $($matches[1]), using year: $Year"
    }

  } #begin

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

    foreach ($C in $CountryCode) {
      Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand) - Country '$C'"
      foreach ($Y in $Year) {
        Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand) - Country '$C', Year '$Y'"
        try {
          $AllFoundHolidays = $null
          #$AllFoundHolidays = Get-PublicHolidayList -CountryCode $C -Year $Y -ErrorAction Stop | Where-Object Global # Limiting to universal holidays only.
          $AllFoundHolidays = Get-PublicHolidayList -CountryCode $C -Year $Y -ErrorAction Stop
          if ($PSBoundParameters.ContainsKey('Debug')) {
            " Function: $($MyInvocation.MyCommand.Name) - AllFoundHolidays:", ($AllFoundHolidays | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
          }
        }
        catch {
          Write-Warning -Message "Get-PublicHolidayList for Country '$C' failed with: $($_.Exception.Message)"
        }

        #Manually adding DateTimeRanges from Parameter
        #TEST whether this would enhance the function

        #Filtering
        if ( $FilterPastDates ) { $AllFoundHolidays = $AllFoundHolidays | Where-Object Date -GT (Get-Date -Format u) }

        #region Slicing and Creating Schedules
        #TEST Grouping of 20 based on Microsoft Documentation with what is delivered in Admin Center (administrability!)
        #Slicing in groups of 10 as a holiday Set only supports max 10 Holidays per set
        #$group = if ( $AllFoundHolidays.Count -ge 10 ) { 10 } else { $AllFoundHolidays.Count }
        $group = 10 # was 10 to be in harmony with Teams Admin Center
        #$group = 20 # is now 20 as supported by PowerShell
        $i = 0
        do {
          $Hols = $AllFoundHolidays[$i..(($i += $group) - 1)]
          if ($PSBoundParameters.ContainsKey('Debug')) {
            " Function: $($MyInvocation.MyCommand.Name) - Current Set: Hols:", ($Hols | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
          }
          [System.Collections.Generic.List[object]]$Holidays = @()
          foreach ($H in $Hols) {
            $Date = $H.date | Get-Date -UFormat '%d/%m/%Y'
            $DateTimeRange = New-CsOnlineDateTimeRange -Start $Date
            if ( $Holidays.Start -notcontains $DateTimeRange.Start ) {
              Write-Verbose -Message "Country '$C', Year '$Y': Date: $Date`: $($H.Name) - OK, adding Date"
              [void]$Holidays.Add($DateTimeRange)
            }
            else {
              Write-Verbose -Message "Country '$C', Year '$Y': Date: $Date`: $($H.Name) - Already present, skipping"
            }
          }
          # Filtering Unique DateTimeRanges
          if ($PSCmdlet.ShouldProcess("Creating Online Schedule '$C $Y' with $($Holidays.Count) Holidays", "$($Schedule.Name)", 'New-TeamsAutoAttendantSchedule')) {
            try {
              $Name = "$C $Y" + $(if ( $i / $group -ge 1 ) { ' #' + [math]::Round($($i / $group), 0) })
              $Schedule = New-TeamsAutoAttendantSchedule -Name $Name -DateTimeRanges $Holidays @NewCsTeamsAutoAttendantScheduleParams
              Write-Information "INFO: Schedule '$($Schedule.Name)' created with $($Holidays.Count) entries"
              Write-Output $Schedule
            }
            catch {
              Write-Error -Message "Schedule '$C $Y' failed to create. Exception: $($_.Exception.Message)"
            }
          }
        }
        until ($i -ge $AllFoundHolidays.count - 1) # Loops for all Holidays
        #endregion
      }
    }
  } #process

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