Functions/Public/NCXReporting.ps1

# NCX cloud reporting functions

Function Connect-NCXCloud {
    <#
        .SYNOPSIS
        Connects to Nectar CX cloud and store the credentials for later use.

        .DESCRIPTION
        Connects to Nectar CX cloud and store the credentials for later use.
        
        .PARAMETER CloudFQDN
        The FQDN of the Nectar CX cloud.

        .PARAMETER TenantName
        The name of a Nectar DXP cloud tenant to connect to and use for subsequent commands. Only useful for multi-tenant deployments
        
        .PARAMETER Credential
        The credentials used to access the Nectar DXP UI. Normally in username@domain.com format
        
        .PARAMETER CredSecret
        Use stored credentials saved via Set-Secret. Requires prior installation of Microsoft.PowerShell.SecretManagement PS module and an appropriate
        secret vault, such as Microsoft.PowerShell.SecretStore. Locally, the Microsoft.PowerShell.SecretStore can be used to store secrets securely on
        the local machine. This is the minimum requirement for using this feature.
        Install the modules by running:
        Install-Module Microsoft.PowerShell.SecretManagement
        Install-Module Microsoft.PowerShell.SecretStore

        Register a credential secret by doing something like: Set-Secret -Name NCXCreds -Vault SecretStore -Secret (Get-Credential)

        .PARAMETER EnvFromFile
        Use a CSV file called CXEnvList.csv located in the user's default Documents folder to show a list of environments to select from. Run [Environment]::GetFolderPath("MyDocuments") to find your default document folder.
        This parameter is only available if CXEnvList.csv is found in the user's default Documents folder (ie: C:\Users\username\Documents)
        Also sets the default credentials to use for the selected environment. This feature uses the Microsoft.PowerShell.SecretManagement PS module,
        which must be installed and configured with a secret store prior to using this option.
        CXEnvList.csv must have a header with three columns defined as "Environment, DefaultTenant, CredSecret".
        Each environment and CredSecret (if used) should be on their own separate lines

        .EXAMPLE
        $Cred = Get-Credential
        Connect-NCX -Credential $cred -CloudFQDN contoso.nectar.services
        Connects to the contoso.nectar.services Nectar CX cloud using the credentials supplied to the Get-Credential command

        .EXAMPLE
        Connect-NCX-CloudFQDN contoso.nectar.services -CredSecret MyCXCreds
        Connects to contoso.nectar.services Nectar CX cloud using previously stored secret called MyCXCreds

        .NOTES
        Version 2.0
    #>


    Param (
        [Parameter(ValueFromPipeline, Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") {
                $True
            }
            Else {
                Throw "ERROR: Nectar CX cloud name must be in FQDN format."
            }
        })]
        [string]$CloudFQDN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.Credential()]
        [PSCredential]$Credential,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$CredSecret
    )
    DynamicParam {
        $DefaultDocPath = [Environment]::GetFolderPath("MyDocuments")
        $EnvPath = "$DefaultDocPath\CXEnvList.csv"
        If (Test-Path $EnvPath -PathType Leaf) {
            # Set the dynamic parameters' name
            $ParameterName = 'EnvFromFile'
            
            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
         
            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                    
            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
            $ParameterAttribute.Mandatory = $False
            $ParameterAttribute.Position = 1
         
            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)
         
            # Generate and set the ValidateSet
            $EnvSet = Import-Csv -Path $EnvPath
            $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($EnvSet.Environment)
         
            # Add the ValidateSet to the attributes collection
            $AttributeCollection.Add($ValidateSetAttribute)
         
            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            Return $RuntimeParameterDictionary
        }
    }
    
    Begin {
        # Bind the dynamic parameter to a friendly variable
        If (Test-Path $EnvPath -PathType Leaf) {
            If ($PsBoundParameters[$ParameterName]) {
                $CloudFQDN = $PsBoundParameters[$ParameterName]
                Write-Verbose "CloudFQDN: $CloudFQDN"
                
                # Get the array position of the selected environment
                $EnvPos = $EnvSet.Environment.IndexOf($CloudFQDN)
                
                # Check for default tenant in CXEnvList.csv and use if available, but don't override if user explicitly set the TenantName
                If (!$PsBoundParameters['TenantName']) {
                    $TenantName = $EnvSet[$EnvPos].DefaultTenant
                    Write-Verbose "DefaultTenant: $TenantName"
                }
                
                # Check for secret in CXEnvList.csv and use if available
                $CredSecret = $EnvSet[$EnvPos].CredSecret
                Write-Verbose "Secret: $CredSecret"
            }
        }
    }
    Process {
        # Need to force TLS 1.2, if not already set
        If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
        
        # Ask for the tenant name if global Nectar tenant variable not available and not entered on command line
        If ((-not $Global:NCXCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = Read-Host "Enter the Nectar DXP cloud FQDN"
        }
        ElseIf (($Global:NCXCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = $Global:NCXCloud
        }
        
        $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+)$"
        $FQDNMatch = Select-String -Pattern $Regex -InputObject $CloudFQDN
        $CloudFQDN = $FQDNMatch.Matches.Groups[2].Value
        
        # Ask for credentials if global Nectar creds aren't available
        If (((-not $Global:NCXCred) -And (-not $Credential)) -Or (($Global:NCXCloud -ne $CloudFQDN) -And (-Not $Credential)) -And (-Not $CredSecret)) {
            $Credential = Get-Credential
        }
        ElseIf ($Global:NCXCred -And (-not $Credential)) {
            $Credential = $Global:NCXCred
        }

        # Pull credentials from secret if specified
        If ($CredSecret) {
            Try {
                $Credential = Get-Secret $CredSecret
            }
            Catch {
                Throw "Cannot find secret: $CredSecret"
            }
        }
        
        If ((-not $Global:NCXCred) -Or (-not $Global:NCXCloud) -Or ($Global:NCXCloud -ne $CloudFQDN)) {
            # First check and notify if updated Nectar PS module available
            [string]$InstalledNectarPSVer = (Get-InstalledModule -Name Nectar10 -ErrorAction SilentlyContinue).Version
            
            If ($InstalledNectarPSVer -gt 0) {
                [string]$LatestNectarPSVer = (Find-Module Nectar10).Version
                If ($LatestNectarPSVer -gt $InstalledNectarPSVer) {
                    Write-Host "=============== Nectar PowerShell module version $LatestN10Ver available ===============" -ForegroundColor Yellow
                    Write-Host "You are running version $InstalledNectarPSVer. Type " -ForegroundColor Yellow -NoNewLine
                    Write-Host 'Update-Module Nectar10' -ForegroundColor Green -NoNewLine
                    Write-Host ' to update.' -ForegroundColor Yellow
                }
            }
            
            # Build Basic auth header explicitly. The Liferay portal returns 401 without a WWW-Authenticate challenge,
            # so Invoke-WebRequest -Credential never sends the credentials at all.
            $UserName = $Credential.UserName
            $Password = $Credential.GetNetworkCredential().Password
            $Base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($UserName):$($Password)"))
            $Headers = @{Authorization = "Basic $Base64AuthInfo"}

            # Attempt connection to tenant. Current CX builds serve the API from /o/nectarcx-rest, while older
            # deployments use the legacy /cyclone-portlet/api base. Try the current path first and fall back.
            $WebRequest = $Null
            ForEach ($APIBase in @('/o/nectarcx-rest', '/cyclone-portlet/api')) {
                $URI = "https://$CloudFQDN$APIBase/organisation/"
                Write-Verbose $URI

                Try {
                    $WebRequest = Invoke-WebRequest -Uri $URI -Method GET -Headers $Headers -UseBasicParsing -SessionVariable NectarSession
                    $Global:NCXAPIBase = $APIBase
                    Break
                }
                Catch {
                    # Windows PowerShell throws WebException, PowerShell 7 throws HttpResponseException. Both expose Response.StatusCode.
                    # Only fall through to the legacy base when this one does not exist. Anything else (401, 403, etc) is fatal.
                    If ([int]$_.Exception.Response.StatusCode -ne 404) { Throw }
                    Write-Verbose "$URI returned 404, trying legacy API base"
                }
            }

            If (-Not $WebRequest) {
                Throw "Could not find the Nectar CX API on https://$CloudFQDN. Tried both /o/nectarcx-rest and /cyclone-portlet/api."
            }

            If ($WebRequest.StatusCode -ne 200) {
                Write-Error "Could not connect to $CloudFQDN using $($Credential.UserName)"
            }
            Else {
                Write-Host -ForegroundColor Green "Successful connection to " -NoNewLine
                Write-Host -ForegroundColor Yellow "https://$CloudFQDN" -NoNewLine
                Write-Host -ForegroundColor Green " using " -NoNewLine
                Write-Host -ForegroundColor Yellow ($Credential).UserName
                $Global:NCXCloud = $CloudFQDN
                $Global:NCXCred = $Credential
                $Global:NCXAuthHeader = $Headers
                $Global:NCXSession = $NectarSession

                # If there is only one available tenant, assign that to the NCXTenantName global variable
                $TenantList = $WebRequest | ConvertFrom-Json
                If ($TenantList.Count -eq 1) {
                    $Global:NCXTenantName = $TenantList.name
                    $Global:NCXOrgID = $TenantList.ID
                }
            }
        }

        # Check to see if tenant name was entered and set global variable, if valid.
        If ($TenantName) {
            $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/organisation/"
            Write-Verbose $URI

            $TenantList = Invoke-RestMethod -Method GET -Headers $Global:NCXAuthHeader -WebSession $Global:NCXSession -URI $URI
            Try {
                If ($TenantList.name -Contains $TenantName) {
                    $Global:NCXTenantName = ($TenantList | Where-Object {$_.name -eq $TenantName}).name
                    $Global:NCXOrgID = ($TenantList | Where-Object {$_.name -eq $TenantName}).id
                    Write-Host -ForegroundColor Green "Successsfully set the tenant name to " -NoNewLine
                    Write-Host -ForegroundColor Yellow $Global:NCXTenantName -NoNewLine
                    Write-Host -ForegroundColor Green " (OrgID=" -NoNewLine
                    Write-Host -ForegroundColor Yellow $Global:NCXOrgID -NoNewLine 
                    Write-Host -ForegroundColor Green "). This tenantname will be used in all subsequent commands."
                }
                Else {
                    $TenantList | ForEach-Object{ $TList += ($(If($TList){", "}) + $_.name) }
                    Write-Error "Could not find a tenant with the name $TenantName on https://$Global:NCXCloud. Select one of $TList"
                }
            }
            Catch {
                Write-Error "Invalid tenant name on https://$Global:NCXCloud"
            }
        }
        ElseIf ($PSBoundParameters.ContainsKey('TenantName')) { # Remove the NCXTenantName global variable only if TenantName is explicitly set to NULL
            Remove-Variable NCXTenantName -Scope Global -ErrorAction:SilentlyContinue
            Remove-Variable NCXOrgID -Scope Global -ErrorAction:SilentlyContinue
        }
    }
}


Function Disconnect-NCXCloud {
    <#
        .SYNOPSIS
        Disconnects from any active Nectar CX connection
        
        .DESCRIPTION
        Essentially deletes any stored credentials and FQDN from global variables

        .EXAMPLE
        Disconnect-NCXCloud
        Disconnects from all active connections to Nectar DXP tenants

        .NOTES
        Version 1.0
    #>

    [Alias("dnc")]
    [cmdletbinding()]
    param ()
    
    Remove-Variable NCXCred -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NCXCloud -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NCXAuthHeader -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NCXAPIBase -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NCXSession -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NCXTenantName -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NCXOrgID -Scope Global -ErrorAction:SilentlyContinue

    Write-Verbose "Successfully disconnected from Nectar CX cloud"
}


Function Get-NCXCloudInfo {
    <#
        .SYNOPSIS
        Shows information about the active Nectar CX connection
        
        .DESCRIPTION
        Shows information about the active Nectar CX connection

        .EXAMPLE
        Get-NCXCloud

        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    param ()
    
    $CloudInfo = "" | Select-Object -Property CloudFQDN, Credential
    $CloudInfo.CloudFQDN = $Global:NCXCloud
    $CloudInfo.Credential = ($Global:NCXCred).UserName
    $CloudInfo | Add-Member -TypeName 'Nectar.CloudInfo'
    
    Try {
        $TenantCount = Get-NCXTenantNames
        If ($TenantCount.Count -gt 1) {
            If ($Global:NCXTenantName) {
                $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $Global:NCXTenantName
                $CloudInfo | Add-Member -NotePropertyName 'OrgID' -NotePropertyValue $Global:NCXOrgID
            }
            Else {
                $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue '<Not Set>'
                $CloudInfo | Add-Member -NotePropertyName 'OrgID' -NotePropertyValue '<Not Set>'
            }
        }
    }
    Catch {
    }
    
    Return $CloudInfo
}




#################################################################################################################################################
# #
# Other CX Functions #
# #


Function Get-NCXTenantNames {
    <#
        .SYNOPSIS
        Shows all the available Nectar CX tenants on the cloud host.
        
        .DESCRIPTION
        Shows all the available Nectar CX tenants on the cloud host. Only available for multi-tenant deployments.

        .EXAMPLE
        Get-NCXTenantNames

        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    [alias('Get-NCXOrganization')]
    param ()
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/organisation/"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -URI $URI
            Return $JSON
        }
        Catch {
            Write-Error "No data found or insufficient permissions. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NCXCampaign {
    <#
        .SYNOPSIS
        Shows all Nectar CX campaigns
        
        .DESCRIPTION
        Shows all Nectar CX campaigns

        .EXAMPLE
        Get-NCXCampaign

        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    param (
        [Parameter(Mandatory=$True)]
        [ValidateSet('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND', IgnoreCase=$True)]
        [string]$PlanType
    )
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/campaigns/getCampaigns/$Global:NCXOrgID/$PlanType"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -URI $URI
            Return $JSON.data
        }
        Catch {
            Write-Error "No data or insufficient permissions. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NCXTestCase {
    <#
        .SYNOPSIS
        Shows all Nectar CX test cases within the tenant
        
        .DESCRIPTION
        Shows all Nectar CX test cases within the tenant

        .EXAMPLE
        Get-NCXTestCase

        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    param (
        [Parameter(Mandatory=$True)]
        [ValidateSet('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND', IgnoreCase=$True)]
        [string]$PlanType
    )
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/testCases/getTestCases/$Global:NCXOrgID/$PlanType"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -URI $URI
            Return $JSON.data
        }
        Catch {
            Write-Error "No data or insufficient permissions. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NCXAlarm {
    <#
        .SYNOPSIS
        Shows NCX alarms
        
        .DESCRIPTION
        Shows NCX alarms

        .EXAMPLE
        Get-NCXAlarm

        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False, DefaultParameterSetName = 'Summary')]
    Param (
        [Parameter(Mandatory=$True, ParameterSetName = 'Summary', Position = 0)]
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime', Position = 0)]
        [ValidateSet('Organization', 'TestCase', 'CalledNumber', 'TestSuite', IgnoreCase=$True)]
        [string]$AlarmType,
        [Parameter(Mandatory=$False, ParameterSetName = 'Summary')]
        [ValidateSet('LAST_1HR', 'LAST_4HR', 'LAST_12HR', 'LAST_24HR', 'CURRENT_WEEK', 'CURRENT_MONTH', 'LAST_30DAYS', IgnoreCase=$True)]
        [string]$TimePeriod,
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime')]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime')]
        [DateTime]$TimePeriodTo
    )
    DynamicParam {
        $ParamDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
        Switch ($AlarmType) {
            {$_ -in 'Organization', 'CalledNumber'} {
                # Define parameter attributes for PlanType attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 1
                $ValidateSetAttributes = New-Object System.Management.Automation.ValidateSetAttribute('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND')
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $ParamAttributesCollect.Add($ValidateSetAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('PlanType', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('PlanType', $DynParam1)
            }
            'TestCase' {
                # Define parameter attributes for TestCaseID attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 1
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('TestCaseID', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('TestCaseID', $DynParam1)
            }
            'CalledNumber' {
                # Define parameter attributes for CalledNumber attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 2
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('CalledNumber', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('CalledNumber', $DynParam1)
            }            
            'TestSuite' {
                # Define parameter attributes for TestSuiteID attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 2
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('TestSuiteID', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('TestSuiteID', $DynParam1)
            }
        }
        Return $ParamDictionary
    }
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        $Body = @{}
        Try {
            Switch ($AlarmType) {
                'Organization' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/email-notification/result/organization/$Global:NCXOrgID/$($PSBoundParameters['PlanType'])"; Break }
                'TestCase' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/email-notification/result/test-case/$($PSBoundParameters['TestCaseID'])"; Break }
                'CalledNumber' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/email-notification/result/called-number/$($PSBoundParameters['CalledNumber'])/$($PSBoundParameters['PlanType'])"; Break }
                'TestSuite' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/email-notification/result/test-suite/$($PSBoundParameters['TestSuiteID'])" }
            }
            Write-Verbose $URI
            
            If ($TimePeriod) { $Body.Add('duration', $TimePeriod) }
            If ($TimePeriodFrom) { 
                $Body.Add('startDate', $TimePeriodFrom.ToString('dd-MM-yyyy'))
                $Body.Add('endDate', $TimePeriodTo.ToString('dd-MM-yyyy'))
            }

            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -URI $URI -Body $Body        
            Return $JSON.data
        }
        Catch {
            Write-Error "No data or insufficient permissions. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NCXHistoricalReport {
    <#
        .SYNOPSIS
        Shows NCX historical reports
        
        .DESCRIPTION
        Shows NCX historical reports

        .EXAMPLE
        Get-NCXHistoricalReports

        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False, DefaultParameterSetName = 'Summary')]
    Param (
        [Parameter(Mandatory=$True, ParameterSetName = 'Summary', Position = 0)]
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime', Position = 0)]
        [ValidateSet('Organization', 'SingleCampaignRun', 'AllCampaignRun', 'Campaign', IgnoreCase=$True)]
        [string]$ReportType,
        [Parameter(Mandatory=$False, ParameterSetName = 'Summary')]
        [ValidateSet('LAST_1HR', 'LAST_4HR', 'LAST_12HR', 'LAST_24HR', 'CURRENT_WEEK', 'CURRENT_MONTH', 'LAST_30DAYS', IgnoreCase=$True)]
        [string]$TimePeriod,
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime')]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime')]
        [DateTime]$TimePeriodTo
    )
    DynamicParam {
        $ParamDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
        Switch ($ReportType) {
            'Organization' {
                # Define parameter attributes for PlanType attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 1
                $ValidateSetAttributes = New-Object System.Management.Automation.ValidateSetAttribute('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND')
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $ParamAttributesCollect.Add($ValidateSetAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('PlanType', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('PlanType', $DynParam1)
                Break
            }
            {$_ -like '*CampaignRun'} {
                # Define parameter attributes for TestCaseID attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 1
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('TestSuiteRunResultID', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('TestSuiteRunResultID', $DynParam1)
                Break
            }
            'Campaign' {
                # Define parameter attributes for CalledNumber attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 2
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('CampaignID', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('CampaignID', $DynParam1)
            }            
        }
        Return $ParamDictionary
    }
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        $Body = @{}
        Try {
            Switch ($ReportType) {
                'Organization' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/test-suite-run-result/organisation/$Global:NCXOrgID/$($PSBoundParameters['PlanType'])"; Break }
                'SingleCampaignRun' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/test-suite-run-result/summary/$($PSBoundParameters['TestSuiteRunResultID'])"; Break }
                'AllCampaignRun' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/test-suite-run-result/test-case-result/$($PSBoundParameters['TestSuiteRunResultID'])"; Break }
                'Campaign' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/test-suite-run-result/test-case-run-result/$($PSBoundParameters['CampaignID'])" }
            }

            Write-Verbose $URI
            
            If ($TimePeriod) { $Body.Add('duration', $TimePeriod) }
            If ($TimePeriodFrom) { 
                $Body.Add('startDate', $TimePeriodFrom.ToString('dd-MM-yyyy'))
                $Body.Add('endDate', $TimePeriodTo.ToString('dd-MM-yyyy'))
            }

            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -URI $URI -Body $Body        
            Return $JSON.data
        }
        Catch {
            Write-Error "No data or insufficient permissions. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NCXLiveReport {
    <#
        .SYNOPSIS
        Shows CX live report information.
        
        .DESCRIPTION
        Shows CX live report information.

        .EXAMPLE
        Get-NCXLiveReport

        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False, DefaultParameterSetName = 'Summary')]
    Param (
        [Parameter(Mandatory=$True, ParameterSetName = 'Summary')]
        [ValidateSet('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND', IgnoreCase=$True)]
        [string]$PlanType,
        [Parameter(Mandatory=$True, ParameterSetName = 'Detail')]
        [ValidateSet('Voice', 'PESQ', 'MOS', IgnoreCase=$True)]
        [string]$ReportType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True, ParameterSetName = 'Detail')]
        [Alias("id")]
        [string]$DashboardID    
    )
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        Try {
            Switch ($ReportType) {
                'Voice' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/dashboard/realtime-voice-channel/$DashboardID"; Break }
                'PESQ' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/dashboard/realtime-pesq/$DashboardID"; Break }
                'MOS' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/dashboard/realtime-mos/$DashboardID"; Break }
                default { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/dashboard/$Global:NCXOrgID/$PlanType"}
            }
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -URI $URI
            Return $JSON
        }
        Catch {
            Write-Error "No data or insufficient permissions. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NCXAlarmConfig {
    <#
        .SYNOPSIS
        Shows CX email alarm configuration.
        
        .DESCRIPTION
        Shows CX email alarm configuration.

        .EXAMPLE
        Get-NCXAlarmConfig

        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False, DefaultParameterSetName = 'Summary')]
    Param (
        [Parameter(Mandatory=$True, ParameterSetName = 'Summary')]
        [ValidateSet('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND', IgnoreCase=$True)]
        [string]$PlanType,
        [Parameter(Mandatory=$True, ParameterSetName = 'Detail')]
        [ValidateSet('TestCase', 'CalledNumber', 'TestSuite', IgnoreCase=$True)]
        [string]$AlarmType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True, ParameterSetName = 'Detail')]
        [Alias('TestCaseID')]
        [string]$ID    
    )
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        Try {
            Switch ($ReportType) {
                'TestCase' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/email-alarm/test-case/$ID"; Break }
                'CalledNumber' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/email-alarm/called-number/$ID"; Break }
                'TestSuite' { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/email-alarm/test-suite/$ID"; Break }
                default { $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/email-alarm/organization/$Global:NCXOrgID/$PlanType"}
            }
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -URI $URI
            Return $JSON
        }
        Catch {
            Write-Error "No data or insufficient permissions. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NCXPortUsage {
    <#
        .SYNOPSIS
        Shows CX port consumption reports.

        .DESCRIPTION
        Shows the number of ports consumed by the organization for a given plan type, optionally filtered
        by time period, campaign and/or test case.

        .PARAMETER PlanType
        The campaign plan type to report on.

        .PARAMETER TimePeriod
        A pre-defined relative time period to report on. Cannot be combined with TimePeriodFrom/TimePeriodTo.

        .PARAMETER TimePeriodFrom
        The start of an explicit reporting window. Must be used with TimePeriodTo.

        .PARAMETER TimePeriodTo
        The end of an explicit reporting window. Must be used with TimePeriodFrom.

        .PARAMETER CampaignID
        One or more campaign IDs to limit the report to.

        .PARAMETER TestCaseID
        One or more test case IDs to limit the report to.

        .PARAMETER TenantName
        The Nectar CX tenant to report on. Defaults to the tenant selected by Connect-NCXCloud. Only useful for multi-tenant deployments.

        .EXAMPLE
        Get-NCXPortUsage -PlanType RADAR
        Shows port consumption for all RADAR campaigns in the currently selected tenant

        .EXAMPLE
        Get-NCXPortUsage -PlanType EXPRESS -TimePeriod LAST_24HR
        Shows port consumption for EXPRESS campaigns over the last 24 hours

        .EXAMPLE
        Get-NCXPortUsage -PlanType RADAR -TimePeriodFrom '2026-07-01' -TimePeriodTo '2026-07-30' -CampaignID 1234,5678
        Shows port consumption for two specific campaigns between the two supplied dates

        .EXAMPLE
        Get-NCXPortUsage -PlanType RADAR -TenantName Contoso
        Shows port consumption for RADAR campaigns on the Contoso tenant, without changing the currently connected tenant

        .NOTES
        Version 1.1
    #>


    [CmdletBinding(PositionalBinding=$False, DefaultParameterSetName = 'Summary')]
    [alias('Get-NCXPortsConsumed')]
    Param (
        [Parameter(Mandatory=$True, ParameterSetName = 'Summary', Position = 0)]
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime', Position = 0)]
        [ValidateSet('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND', IgnoreCase=$True)]
        [string]$PlanType,
        [Parameter(Mandatory=$False, ParameterSetName = 'Summary')]
        [ValidateSet('LAST_15MIN', 'LAST_30MIN', 'LAST_1HR', 'LAST_12HR', 'LAST_24HR', 'LAST_48HR', 'LAST_72HR', 'CURRENT_WEEK', 'CURRENT_MONTH', IgnoreCase=$True)]
        [string]$TimePeriod,
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime')]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime')]
        [DateTime]$TimePeriodTo,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CampaignID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$TestCaseID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )

    Begin {
        Connect-NCXCloud
    }
    Process {
        $Body = @{}

        # Resolved outside the Try below so an unknown tenant name reports its own error rather than a permissions one
        $OrgID = Get-NCXOrgID -TenantName $TenantName

        Try {
            $URI = "https://$Global:NCXCloud$Global:NCXAPIBase/port-consumes/$OrgID/$PlanType"
            Write-Verbose $URI

            If ($TimePeriod) { $Body.Add('duration', $TimePeriod) }
            If ($TimePeriodFrom) {
                $Body.Add('startDate', $TimePeriodFrom.ToString('dd-MM-yyyy'))
                $Body.Add('endDate', $TimePeriodTo.ToString('dd-MM-yyyy'))
            }
            If ($CampaignID) { $Body.Add('campaignId', ($CampaignID -Join ',')) }
            If ($TestCaseID) { $Body.Add('testCaseId', ($TestCaseID -Join ',')) }

            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -URI $URI -Body $Body
            Return $JSON
        }
        Catch {
            Write-Error "No data or insufficient permissions. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}
# SIG # Begin signature block
# MIIlcwYJKoZIhvcNAQcCoIIlZDCCJWACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBrQvzRZbj8gJuj
# znnYAzU5BPQBjaVT5E0hCWC+CXlA9aCCEvMwggXdMIIDxaADAgECAgh7LJvTFoAy
# mTANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx
# EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8G
# A1UEAwwoU1NMLmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTAe
# Fw0xNjAyMTIxNzM5MzlaFw00MTAyMTIxNzM5MzlaMHwxCzAJBgNVBAYTAlVTMQ4w
# DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENv
# cnBvcmF0aW9uMTEwLwYDVQQDDChTU0wuY29tIFJvb3QgQ2VydGlmaWNhdGlvbiBB
# dXRob3JpdHkgUlNBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+Q/d
# oyt9y9Aq/uxnhabnLhu6d+Hj9a+k7PpKXZHEV0drGHdrdvL9k+Q9D8IWngtmw1aU
# nheDhc5W7/IW/QBi9SIJVOhlF05BueBPRpeqG8i4bmJeabFf2yoCfvxsyvNB2O3Q
# 6Pw/YUjtsAMUHRAOSxngu07shmX/NvNeZwILnYZVYf16OO3+4hkAt2+hUGJ1dDyg
# +sglkrRueiLH+B6h47LdkTGrKx0E/6VKBDfphaQzK/3i1lU0fBmkSmjHsqjTt8qh
# k4jrwZe8jPkd2SKEJHTHBD1qqSmTzOu4W+H+XyWqNFjIwSNUnRuYEcM4nH49hmyl
# D0CGfAL0XAJPKMuucZ8POsgz/hElNer8usVgPdl8GNWyqdN1eANyIso6wx/vLOUu
# qfqeLLZRRv2vA9bqYGjqhRY2a4XpHsCz3cQk3IAqgUFtlD7I4MmBQQCeXr9/xQiY
# ohgsQkCz+W84J0tOgPQ9gUfgiHzqHM61dVxRLhwrfxpyKOcAtdF0xtfkn60Hk7ZT
# NTX8N+TD9l0WviFz3pIK+KBjaryWkmo++LxlVZve9Q2JJgT8JRqmJWnLwm3KfOJZ
# X5es6+8uyLzXG1k8K8zyGciTaydjGc/86Sb4ynGbf5P+NGeETpnr/LN4CTNwumam
# du0bc+sapQ3EIhMglFYKTixsTrH9z5wJuqIz7YcCAwEAAaNjMGEwHQYDVR0OBBYE
# FN0ECQei9Xp9UlMSkpXuOIAlDaZZMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgw
# FoAU3QQJB6L1en1SUxKSle44gCUNplkwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3
# DQEBCwUAA4ICAQAgGBGUKfsmnRweHnBh8ZVyk3EkrWiTWI4yrxuzcAP8JSt0hZA9
# eGr0uYullzu1GJG7Hqf5QFuR+VWZrx4R0Fwdp2bjsZQHDDI5puobsHnYHZxwROOK
# 3cT5lR+KOEM/AYWlR6c9RrK85SJo93uc2Cw+CiHILTOsv8WBmTF0wXVxxb6x8CNF
# 9J1r/BljnaO8BMYYCyW7U4kPs4BQ3kXuRH+rlHhkmNP2KN2H2HBldPsOuRPrpw9h
# qTKWzN677WNMGLupQPegVG4giHF1GOp6tDRy4CMnd1y2kOqGJUCr7zMPy5+CvqIg
# +/a1LRrmwoWxdA/7yGUCpFIBR91JIsG/2OtrrH7e7GMzFbcjCI/GD41BWt2OxbmP
# 5UU/eNu60htAsf5xTT/ggaK6XrTsFeCT3QgffuFVmQsh3pOeCvvmo0m9NjD+53ey
# oHWXtS2BiBdlIPfakACfyVLMMso1fPU9D9gr1/UmbMkGNJYW6nBZGjJ5eQu2iH8P
# Ukg9v2zYokQu0U63cljTiROV/kSr+NeLG26cvCygW9VqAK9fN+HV+hALmJyG5yaP
# zvDsbopXC4DjTrLAoGNhkLpVaDd0araS25+hhiK2ZScO7LafQmDkZ8K12kELxNOL
# YRu8+h+RK9dEB166KazZxenvU0ha64DxKFghzbAGVfsnP1OQcKkEHlcnuTCCBnIw
# ggRaoAMCAQICCGQzUdPHOJ8IMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVT
# MQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NM
# IENvcnBvcmF0aW9uMTEwLwYDVQQDDChTU0wuY29tIFJvb3QgQ2VydGlmaWNhdGlv
# biBBdXRob3JpdHkgUlNBMB4XDTE2MDYyNDIwNDQzMFoXDTMxMDYyNDIwNDQzMFow
# eDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9u
# MREwDwYDVQQKDAhTU0wgQ29ycDE0MDIGA1UEAwwrU1NMLmNvbSBDb2RlIFNpZ25p
# bmcgSW50ZXJtZWRpYXRlIENBIFJTQSBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
# ADCCAgoCggIBAJ+DE3OqsMZtIcvbi3qHdNBx3I6Xcprku4g0tN2AA8YvRaR0mr8e
# D1Dqnm1485/6USapPZ3RspRXPvs5iRuRK1bvZ8vmC+MOOYzGNfSMPd0l6QGsF0J9
# WBZA3PnVKEQdlWQwYTpk8pfXc0x9eyMCbfN161U9b6otxK++dKxd/mq2/OpceekP
# Q5y1UgUP7z6xsY/QSa2m40IZVD/zLw6hy3z+E/kjOdolHLg+AEo6bzIwN2Qex651
# B9hV0hjJDoq8o1zwfAqnhYHCDq+PmVzTYCW8g1ppHCUTzXL165yAm9wsZ8TdyQmY
# 1XPrxCGj5TKOPi9SmMZgN2SMsm9KVHIYzCeH+s11omMhTLU9ZP0rpptVryZMYLS5
# XP6rQ72t0BNmUB8L0omm/9eABvHDEQIzM2EX91Yfji87aOcV8XdWSimeA9rCKyZh
# MlugVuVJKY02p/XHUqJWAyAvOHiAvfYGrkE0y5RFvZvHiRgfC7r/qa5qQJkT3e9Q
# 3wG68gTW0DHfNDheV1vIOB5W1KxIpu3/+bjBO+3CJL5EYKd3zdU9mFm0Q+qqYH3N
# wuUv8ev11CDVlzRuXQRrBRHS05KMCSdE7U81MUZ+dBkFYuyJ4+ojcJjk0S/UihMY
# RpNl5Vhz00w9J3oiP8P4o1W3+eaHguxFHsVuOnyxTrmraPebY9WRQbypAgMBAAGj
# gfswgfgwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTdBAkHovV6fVJTEpKV
# 7jiAJQ2mWTAwBggrBgEFBQcBAQQkMCIwIAYIKwYBBQUHMAGGFGh0dHA6Ly9vY3Nw
# cy5zc2wuY29tMBEGA1UdIAQKMAgwBgYEVR0gADATBgNVHSUEDDAKBggrBgEFBQcD
# AzA7BgNVHR8ENDAyMDCgLqAshipodHRwOi8vY3Jscy5zc2wuY29tL3NzbC5jb20t
# cnNhLVJvb3RDQS5jcmwwHQYDVR0OBBYEFFTC/hCVAJPNavXnwNfZsku4jwzjMA4G
# A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA9Q8mh3CvmaLK9dbJ8I1m
# PTmC04gj2IK/j1SEJ7bTgwfXnieJTYSOVNEg7mBD21dCPMewlfa+zOqjPY5PBsYr
# WYZ/63MbyuVAJuA9b8z2vXHGzX0OIEA51gXSr5QIv3/CUbcrtXuDIfBj2uWc4Wku
# dR1Oy2Ee9aUz3wKdFdntaZNXukZFLoC8Zb7nEj7eR/+QnBCt9laypNT61vwuvJch
# s3aD0pH6BlDRsYAogP7brQ9n7fh93NlwW3q6aLWzSmYXj+fw51fdaf68XuHVjJ8T
# u5WaFft5K4XVbT5nR24bB1z7VEUPFhEuEcOwvLVuHDNXlB7+QjRGjjFQTtszV5X6
# OOTmEturWC5Ft9kiyvRaR0ksKOhPjEI8ZGjp5kOsGZGpxxOCX/xxCje3nVB7PF33
# olKCNeS159MKb2v+jfmk19UdS+d9Ygj42desmUnbtYRBFC72LmCXU0ua/vGIenS6
# nnXp4NqnycwsO3tMCnjPlPc2YLaDPIpUy04NaCqUEXUmFOogN8zreRd2VXhxbeJJ
# ODM32+RsWccjYua8zi5US/1eAyrI3R5LcUTQdT4xYmWLKabtJOF6HYQ0f6QXfLSs
# fT81WMvDvxrdn1RWbUXlU/OIiisxo8o+UNEANOwnCMNnxlzoaL/PLhZluDxm/zuy
# lauajZ3MlPDteFB/7GRHo50wggaYMIIEgKADAgECAhBHaHS/7B9x7N3XWwMIISW7
# MA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQ
# MA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxNDAyBgNVBAMMK1NT
# TC5jb20gQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBDQSBSU0EgUjEwHhcNMjYw
# NzA5MTQzMTE4WhcNMjcxMDEwMTQzMTE4WjB/MQswCQYDVQQGEwJVUzERMA8GA1UE
# CAwITmV3IFlvcmsxEDAOBgNVBAcMB0plcmljaG8xHjAcBgNVBAoMFU5lY3RhciBT
# ZXJ2aWNlcyBDb3JwLjELMAkGA1UECwwCSVQxHjAcBgNVBAMMFU5lY3RhciBTZXJ2
# aWNlcyBDb3JwLjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJmOMPRo
# al1bEwe/HPHB533mkxLFf2LGg8R3yZQfDMkVWS0ME52aHJT33z61eMLqFamuvqY7
# qeb6jcOjqenYUhNmFcnl1Oq2hFWWE7McdgYAc0boWHemqxHWrUXN53sCp1DFVTh3
# NFwTo2PaY/KLdIjsXXvmA9qQBytzxEmeaF5HyCmmTlQ/N4eDeqpMOAbQn1n7lLi7
# xiHq1UAll3uvB+y7Vq/3V/dCO33/aiQif1t6h2R+N2JhoW0vtvd+yMyY58klGfN6
# O9WHto+MAiGoyP8rs9XWAzhIRZ2JjwFjfTqQOxVv0lUH+ZolhJ9uQHvybL3wHwLN
# Qu8szoII3tqksiosdPc4Xk1nSyzrBfETafVVpey19HD6RhSIHrnPPHwWOzrdsT9j
# O9szScE3i+cfJcKiL98lJkhFq/ZGOIwAPgxPvZZQZqbzEcsR0iddA+KNLxKqzkv9
# 1GtETHdFoU07xY7+2NsY1xhs9jFFNhoeCVh+dk3lIBvpWkuMwM8mZKziUQIDAQAB
# o4IBlTCCAZEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRUwv4QlQCTzWr158DX
# 2bJLuI8M4zB6BggrBgEFBQcBAQRuMGwwSAYIKwYBBQUHMAKGPGh0dHA6Ly9jZXJ0
# LnNzbC5jb20vU1NMY29tLVN1YkNBLUNvZGVTaWduaW5nLVJTQS00MDk2LVIxLmNl
# cjAgBggrBgEFBQcwAYYUaHR0cDovL29jc3BzLnNzbC5jb20wUQYDVR0gBEowSDAI
# BgZngQwBBAEwPAYMKwYBBAGCqTABAwMBMCwwKgYIKwYBBQUHAgEWHmh0dHBzOi8v
# d3d3LnNzbC5jb20vcmVwb3NpdG9yeTATBgNVHSUEDDAKBggrBgEFBQcDAzBNBgNV
# HR8ERjBEMEKgQKA+hjxodHRwOi8vY3Jscy5zc2wuY29tL1NTTGNvbS1TdWJDQS1D
# b2RlU2lnbmluZy1SU0EtNDA5Ni1SMS5jcmwwHQYDVR0OBBYEFB7iZgW8t2SQ3I8n
# dKd+fgqBvDYxMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAhCFC
# ydfNbwEfqJhrp612HM4Qh7JUphMm5ZLNomSEsonhjz1ltmMpB/0nGVH9Ond23hac
# 12+xzfnDAB2HsNhA9vVWYbgbxcH1duQbdRatjrtWKd1+YOfsvAS+O652yRQf26OL
# +2efdAnzaqsTYFoEJwFcgl1M5Uo+uSht961Jm7McHouRm/zYOnC+NjvGOgIL40KN
# P1pLQTxK5zc54+yFQANjiCPNI07AnkYCSgHDJCoKZbJTcUk26kQNqNE9AvjsIlpY
# uY8sTXHSIkN3WNuSPYr66GHaYj9m0AGUKGt/ZZePKmOsLu+nON5hwqXx+QX75yc2
# AIlKw/ItfZvBLeRuejoKnbOh6QiyDxn0lfAH8f/upDPCUfvBVADF56itElnCo+fC
# 36rzFnfBaVPbLVzqlAGSs7e6Yn669snsTHkKQm1fALEiZyt3dqOvppr8AO1f+oCc
# r/0jVezz0VyNUemChoAMBosREctn/fOYfL5NClq/mnI5m+tAHOajVk8LI6h7mlNv
# tIFKwfHCIH2EJM4ze+ZrSXWt8bfY+6pQwd4xCB9UmCw2DBBMQ11rvu1rLD+u3kIL
# NBAXHQGEnCoP+sY6h17wc/2Jb8BPg+yAvPXevqXmAdAKJrhvMmgsbttRxOQuVjrq
# 42h1me0i3Jy0obhODmpXsrVPm02ZBIYgiRcK/q8xghHWMIIR0gIBATCBjDB4MQsw
# CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xETAP
# BgNVBAoMCFNTTCBDb3JwMTQwMgYDVQQDDCtTU0wuY29tIENvZGUgU2lnbmluZyBJ
# bnRlcm1lZGlhdGUgQ0EgUlNBIFIxAhBHaHS/7B9x7N3XWwMIISW7MA0GCWCGSAFl
# AwQCAQUAoHwwEAYKKwYBBAGCNwIBDDECMAAwGQYJKoZIhvcNAQkDMQwGCisGAQQB
# gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkE
# MSIEIHSB/aiB2cy3JFJyEaiE5Ua0De/Z3VRbEcHxDK9RUqDYMA0GCSqGSIb3DQEB
# CwUABIIBgGvvrKfFTvdZ2hdx9QDR1QueneX4vq4sAu/iFgUAxpNEq1vrCW/Uaxmr
# ax31Zd0TZWYUdvXCqKpeRHWa3H3+E4kYvo8j64MN4zs/zYn0w9T2CfpV9UaayCsL
# VG4q2tDAJb5Ywbi1h9VbBQZboIsf1RlLKlOAcl579U/U168soBKyFu+SXPP/cgqX
# WLFn1vqxA662GvPCZ+SWog4PnZMPXN3sMB9G0uX+5kv2l1t+324OgqVPJyZBekC3
# yeQD5Z+RtzXJAHCClNyCLdzha37tEjKywujUydqFxcJ3UOHzcuU/b790K+SztU68
# 12Ba3rsY1c06reF3fQFIyhSTPPeX8d77W1YlvzVDrdmWlKwqLV8DPh8G3hNdn/v8
# cXQ389jQwqxZQX1uzvCqesTqK3+V61BTtXrDkCX2Z5sRgKhDDlmkmjNBHB6Ni/d/
# 919Fau7pknm6wK7rlN1sdPYz9ox6vwAj4uHc6urKcISHGKCfOMyKLXxvzY05EaPg
# X/CFtrGg16GCDxwwgg8YBgorBgEEAYI3AwMBMYIPCDCCDwQGCSqGSIb3DQEHAqCC
# DvUwgg7xAgEDMQ0wCwYJYIZIAWUDBAIBMH0GCyqGSIb3DQEJEAEEoG4EbDBqAgEB
# BgwrBgEEAYKpMAEDBgEwLzALBglghkgBZQMEAgEEIKKnPRnu+eJNKZrzL2iqE3yp
# 4ljycWUAIS2w7wHMKTUlAgg/aO8ag1gAkBgPMjAyNjA5MTUxNzMwMjNaMAMCAQEC
# BgGgph6s7KCCDAAwggT8MIIC5KADAgECAhAfaxZi0i4bbF3xwMGgYA44MA0GCSqG
# SIb3DQEBCwUAMHMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UE
# BwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxLzAtBgNVBAMMJlNTTC5jb20g
# VGltZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIxMB4XDTI1MDIxODE2MzIwMloX
# DTM0MTExMjE4NTAwNVowbjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAw
# DgYDVQQHDAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDEqMCgGA1UEAwwhU1NM
# LmNvbSBUaW1lc3RhbXBpbmcgVW5pdCAyMDI1IEUxMFkwEwYHKoZIzj0CAQYIKoZI
# zj0DAQcDQgAEG/tRUcdv5lWW7E9eV8Tczq2DReerx2Jz47e884JGlqVQzW870D4Z
# HNJVWPLKAeFisHDrZcsWHWS/t77JF39pNqOCAVowggFWMB8GA1UdIwQYMBaAFAyd
# ECWOmqcbmYdDzwh+4b2BkPTPMFEGCCsGAQUFBwEBBEUwQzBBBggrBgEFBQcwAoY1
# aHR0cDovL2NlcnQuc3NsLmNvbS9TU0wuY29tLXRpbWVTdGFtcGluZy1JLVJTQS1S
# MS5jZXIwUQYDVR0gBEowSDA8BgwrBgEEAYKpMAEDBgEwLDAqBggrBgEFBQcCARYe
# aHR0cHM6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5MAgGBmeBDAEEAjAWBgNVHSUB
# Af8EDDAKBggrBgEFBQcDCDBGBgNVHR8EPzA9MDugOaA3hjVodHRwOi8vY3Jscy5z
# c2wuY29tL1NTTC5jb20tdGltZVN0YW1waW5nLUktUlNBLVIxLmNybDAdBgNVHQ4E
# FgQUznzZwASAxSQQagnqHKslPRH9qNIwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3
# DQEBCwUAA4ICAQCAc3Ukhb1mU2KnTsV9j2vUsnAspOXWH/L2vUGMOAcwTPtTsnuY
# DLfYnEDUovKMIImo2S5F+EMcYUR9m2NM6u7sBAwNIOJQO8IJzeNrPmnL2Ma/Ah7m
# emQttepeED5KoLMbvX1RKKDCEeRivu/w2JehpjRe7TenQGJlmt5mWmeCYYH37zo3
# 3gWogXHYjlnmK67t3iPtoA5kE3F9T2MUMggYO1Z9Z4KkXRDyssT/cMcOXMkqzkiX
# eL9Wg6XutNT3fyhKvEzDDDoYMGUpfysYfG+SOAhv0xeRWCUlIMew0BkN4JL+KdrE
# ocD4KG4Hwrg7EjFrqTV754cHKlqQBjfC43vDs+U+aE3qTkh2pmfcdkezZWOhHzjV
# n3CZU8V0YN2QFntc6Zvk5lRoq5+y+0RHRVtjYOTNqoBoi23WRz4j4VTPs+JXPY9T
# Ol6CR+1FHG+s/IgvTxuUlOdsxDReuoM3SsR+5Mu/heGGcrIlpeHEJR2M79xG6YzN
# nflBNQwi0FbLXEanSLKgVWcDJrak+xUy4Aj6zLXPGU5L2XmJLG8onyCmek6COphN
# ru7V7Jmj7gmVwaiKJHXsu2ExOsXWrra07nE6kjy3FRnqC0oa2QlXrB2P69ktzApn
# Yz3capWk6jpQGUaPHWwqxVnsAhTMlmWLg0nQzYphyt82eV5uRgqkOdKpbDCCBvww
# ggTkoAMCAQICEG1SGHCH6CNNhWAA0ICPk1YwDQYJKoZIhvcNAQELBQAwfDELMAkG
# A1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYD
# VQQKDA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0
# aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTkxMTEzMTg1MDA1WhcNMzQxMTEy
# MTg1MDA1WjBzMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcM
# B0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMS8wLQYDVQQDDCZTU0wuY29tIFRp
# bWVzdGFtcGluZyBJc3N1aW5nIFJTQSBDQSBSMTCCAiIwDQYJKoZIhvcNAQEBBQAD
# ggIPADCCAgoCggIBAK5REBPS+TwgoCCF3slQHGTJ4f3F6TT/Cn8xSOhyWsVeqGH9
# 8Yf3UVz7t+bQwcITsD7CY6KoGP04OskBgareubfeMKcdKwIE1YBBjKhq4urwiOqx
# LUmVcvb2oM0wx3BnxQ3NBLu9ZkwMnjQlIY2mEwZMgDaqfZuiEa2BFzinXf3kRLKl
# Q5oa8ne3QU0vcG4qZvphy0xxBQXayqigzN3z2HQTq6N28EOjpnA2dajGPtiZ9aNJ
# eDfcDka5j3KbhBkzk4RWCjx5vP8H6DKHIIs02GHgxv/jG8JMIxWY1isG+IaB09li
# vKbxlvzhNAKZK5fQmUstrpYrVo7qqXAhJtv1tUaHzrp6QpuUL9dE/bSAC7UKO9xh
# yJSA1OsYWDx/wAmBA84JzX8IJ1olJjCEmlJ2F4o6dCARKA2Zhk+EU4LogpowBReT
# lTW2NNwUKAW+8Cte0rhrMBZQ47Vjd92V0gEvouOTMtQJgk2QVeqGwFVw8y4HSdQN
# a8sl8+Kay2MnyUXhLoQLFaeVaLs4SVXBOe3Ua1Gp5j3J2+8Yue1T4V5wrsNuocNR
# 3frpSt4yRIG3N68Bz1qqhk+eNUyO8WpXWlg6POZOJUdm0BzzRsB8V7kst8nM8joO
# e03KqhunBN69Ckeo8M32qo07zeveRrDwD2P4dmJLDYBflwZ1A/SQbS+HN+AHAgMB
# AAGjggGBMIIBfTASBgNVHRMBAf8ECDAGAQH/AgEAMB8GA1UdIwQYMBaAFN0ECQei
# 9Xp9UlMSkpXuOIAlDaZZMIGDBggrBgEFBQcBAQR3MHUwUQYIKwYBBQUHMAKGRWh0
# dHA6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5L1NTTGNvbVJvb3RDZXJ0aWZpY2F0
# aW9uQXV0aG9yaXR5UlNBLmNydDAgBggrBgEFBQcwAYYUaHR0cDovL29jc3BzLnNz
# bC5jb20wPwYDVR0gBDgwNjA0BgRVHSAAMCwwKgYIKwYBBQUHAgEWHmh0dHBzOi8v
# d3d3LnNzbC5jb20vcmVwb3NpdG9yeTATBgNVHSUEDDAKBggrBgEFBQcDCDA7BgNV
# HR8ENDAyMDCgLqAshipodHRwOi8vY3Jscy5zc2wuY29tL3NzbC5jb20tcnNhLVJv
# b3RDQS5jcmwwHQYDVR0OBBYEFAydECWOmqcbmYdDzwh+4b2BkPTPMA4GA1UdDwEB
# /wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAkhl1DaZaQs8ZB9ny/JT6wJvwFelE
# llovcTPdUOUTe5mTdw/E+3JtV8u6ppyLRbpIHbYlMy20KJAychU6xdaci4BsP9oV
# NxSRMsEjfHKz7ARqPNdpclhYAINLjsFGMO1iUNbXiAsnF/xboNCgfeMcMYbLyQYk
# U6UMobv9isrtQZ8e0EAQNV7qXJn4W0KyuTt0P8iIv/5DdDpIUBIktDZcjz2KEW6B
# 1gvvsKIM1esjYwWylAazBcQAake5pANMdSn8t1HdPKsiwuWfOguyRQazAX8oXz6S
# lZSIok0Lis9a02vGVtdhEaB0R3HxIyNRMMKWV1yuSeUXFuoexWav3GRPZC0WYb50
# SrW/l+wgrS8doetaMwyZon2L7ioYlIPSy1h9Dq/Q911PsSkbEZ3zrsB1roVnIfBu
# 5BJp0xvQrQ/Q4LavuvCoFR7QFoypNrotbNYi2AGMZw5td4zGZtCqUTPZi0BwSuRm
# +HRYAEMMThTwbJX/fYV1oC8mBN970yIvadIGKhh7+DmYdRJYBrL8inVFCZAK+YX2
# w1+qWEnCSPL/VTWJtSRMhQFfceDKbJC+pBNksvKzqkva0J1ZyMj1i4vDfSuBmbz4
# rfzsvvJxS+quZDdkmW6MeXevWGBXvqzdbAw+AqTVsAQUyP6tFeKZIL4S/fSFdl2r
# Ix2X+KXkqx3S+EYxggJYMIICVAIBATCBhzBzMQswCQYDVQQGEwJVUzEOMAwGA1UE
# CAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMS8w
# LQYDVQQDDCZTU0wuY29tIFRpbWVzdGFtcGluZyBJc3N1aW5nIFJTQSBDQSBSMQIQ
# H2sWYtIuG2xd8cDBoGAOODALBglghkgBZQMEAgGgggFhMBoGCSqGSIb3DQEJAzEN
# BgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwOTE1MTczMDIzWjAoBgkq
# hkiG9w0BCTQxGzAZMAsGCWCGSAFlAwQCAaEKBggqhkjOPQQDAjAvBgkqhkiG9w0B
# CQQxIgQggVfitnmSQqGx6eatcwGWQ0W4wCAI8h1qAyjv8yTG2aMwgckGCyqGSIb3
# DQEJEAIvMYG5MIG2MIGzMIGwBCBUKvmhao1yLmYRSXiK6ZTBipqu5aZcs0SiVJr5
# bHnHizCBizB3pHUwczELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYD
# VQQHDAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDEvMC0GA1UEAwwmU1NMLmNv
# bSBUaW1lc3RhbXBpbmcgSXNzdWluZyBSU0EgQ0EgUjECEB9rFmLSLhtsXfHAwaBg
# DjgwCgYIKoZIzj0EAwIERzBFAiEAtjFjgyw8PDKESRRrvSFYI5neD1tN8cjgMhLt
# xJQna0QCIGdBsWIjbDZKzvrAaIKBXzwDH4Kw6/uEaTEiyIscKOEQ
# SIG # End signature block